Subversion Repositories eFlore/Applications.del

Rev

Rev 1491 | Go to most recent revision | Only display areas with differences | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 1491 Rev 1492
1
<?php
1
<?php
2
/**
2
/**
3
 * @author		Raphaël Droz <raphael@tela-botanica.org>
3
 * @author		Raphaël Droz <raphael@tela-botanica.org>
4
 * @copyright	Copyright (c) 2013, Tela Botanica (accueil@tela-botanica.org)
4
 * @copyright	Copyright (c) 2013, Tela Botanica (accueil@tela-botanica.org)
5
 * @license	http://www.cecill.info/licences/Licence_CeCILL_V2-fr.txt Licence CECILL
5
 * @license	http://www.cecill.info/licences/Licence_CeCILL_V2-fr.txt Licence CECILL
6
 * @license	http://www.gnu.org/licenses/gpl.html Licence GNU-GPL
6
 * @license	http://www.gnu.org/licenses/gpl.html Licence GNU-GPL
7
 * @see http://www.tela-botanica.org/wikini/eflore/wakka.php?wiki=ApiIdentiplante01Images
7
 * @see http://www.tela-botanica.org/wikini/eflore/wakka.php?wiki=ApiIdentiplante01Images
8
 *
8
 *
9
 * Backend pour PictoFlora (del.html#page_recherche_images)
9
 * Backend pour PictoFlora (del.html#page_recherche_images)
10
 *
10
 *
11
 *
11
 *
12
 * == Notes ==
12
 * == Notes ==
13
 *
13
 *
14
 * tri=votes et tri=tags: affectent le choix des images affichées (donc getIdImages())
14
 * tri=votes et tri=tags: affectent le choix des images affichées (donc getIdImages())
15
 * Cependant ce total ne nous intéresse même pas (MoyenneVotePresenteur.java s'en occupe).
15
 * Cependant ce total ne nous intéresse même pas (MoyenneVotePresenteur.java s'en occupe).
16
 * Seul tri=date_transmission nous évite l'AVG() + GROUP BY
16
 * Seul tri=date_transmission nous évite l'AVG() + GROUP BY
17
 *
17
 *
18
 * protocole: il affecte l'affichage des information, mais le JSON contient déjà
18
 * protocole: il affecte l'affichage des information, mais le JSON contient déjà
19
 * l'intégralité (chercher les données de vote pour 1 ou plusieurs protocoles) est quasi-identique.
19
 * l'intégralité (chercher les données de vote pour 1 ou plusieurs protocoles) est quasi-identique.
20
 * Par contre, le tri par moyenne des votes, sous-entend "pour un protocole donné".
20
 * Par contre, le tri par moyenne des votes, sous-entend "pour un protocole donné".
21
 * Dès lors le choix d'un protocole doit avoir été fait afin de régler le JOIN et ainsi l'ORDER BY.
21
 * Dès lors le choix d'un protocole doit avoir été fait afin de régler le JOIN et ainsi l'ORDER BY.
22
 * (cf requestFilterParams())
22
 * (cf requestFilterParams())
23
 *
23
 *
24
 * Histoire: auparavant (pré-r142x) un AVG + GROUP BY étaient utilisés pour générer on-the-fly les valeurs
24
 * Histoire: auparavant (pré-r142x) un AVG + GROUP BY étaient utilisés pour générer on-the-fly les valeurs
25
 * utilisées ensuite pour l'ORDER BY. La situation à base de del_image_stat
25
 * utilisées ensuite pour l'ORDER BY. La situation à base de del_image_stat
26
 * est déjà bien meilleure sans être pour autant optimale. cf commentaire de sqlAddConstraint()
26
 * est déjà bien meilleure sans être pour autant optimale. cf commentaire de sqlAddConstraint()
27
 *
27
 *
28
 *
28
 *
29
 * Tags:
29
 * Tags:
30
 * Le comportement habituel dans le masque *général*: les mots sont séparés par des espaces,
30
 * Le comportement habituel dans le masque *général*: les mots sont séparés par des espaces,
31
 * implod()ed par des AND (tous les mots doivent matcher).
31
 * implod()ed par des AND (tous les mots doivent matcher).
32
 * Et le test effectué doit matcher sur:
32
 * Et le test effectué doit matcher sur:
33
 * %(les tags d'observations)% *OU* %(les tags d'images)% *OU* %(les tags publics)%
33
 * %(les tags d'observations)% *OU* %(les tags d'images)% *OU* %(les tags publics)%
34
 *
34
 *
35
 * Le comportement habituel dans le masque *tag*: les mots ne sont *pas* splittés (1 seule expression),
35
 * Le comportement habituel dans le masque *tag*: les mots ne sont *pas* splittés (1 seule expression),
36
 * Et le test effectué doit matcher sur:
36
 * Et le test effectué doit matcher sur:
37
 * ^(expression)% *OU* %(expression)% [cf getConditionsImages()]
37
 * ^(expression)% *OU* %(expression)% [cf getConditionsImages()]
38
 *
38
 *
39
 * Par défaut les tags sont comma-separated (OU logique).
39
 * Par défaut les tags sont comma-separated (OU logique).
40
 * Cependant pour conserver le comportement du masque général qui sous-entend un ET logique sur
40
 * Cependant pour conserver le comportement du masque général qui sous-entend un ET logique sur
41
 * des tags séparés par des espaces recherche 
41
 * des tags séparés par des espaces recherche 
42
 *
42
 *
43
 * TODO:
43
 * TODO:
44
 * -affiner la gestion de passage de mots-clefs dans le masque général.
44
 * -affiner la gestion de passage de mots-clefs dans le masque général.
45
 * - subqueries dans le FROM pour les critère WHERE portant directement sur v_del_image
45
 * - subqueries dans le FROM pour les critère WHERE portant directement sur v_del_image
46
 * plutôt que dans WHERE (qui nécessite dès lors un FULL-JOIN)
46
 * plutôt que dans WHERE (qui nécessite dès lors un FULL-JOIN)
47
 * (http://www.mysqlperformanceblog.com/2007/04/06/using-delayed-join-to-optimize-count-and-limit-queries/)
47
 * (http://www.mysqlperformanceblog.com/2007/04/06/using-delayed-join-to-optimize-count-and-limit-queries/)
48
 * - éviter de dépendre d'une jointure systématique sur `cel_obs`, uniquement pour `(date_)transmission
48
 * - éviter de dépendre d'une jointure systématique sur `cel_obs`, uniquement pour `(date_)transmission
49
 * (cf VIEW del_image)
49
 * (cf VIEW del_image)
50
 * - poursuivre la réorganisation des méthodes statiques parmis Observation, ListeObservations et ListImages2
50
 * - poursuivre la réorganisation des méthodes statiques parmis Observation, ListeObservations et ListeImages2
51
 * - *peut-être*: passer requestFilterParams() en méthode de classe
51
 * - *peut-être*: passer requestFilterParams() en méthode de classe
52
 *
52
 *
53
 *
53
 *
54
 * MySQL sux:
54
 * MySQL sux:
55
 * EXPLAIN SELECT  id_image FROM v_del_image vdi WHERE vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1 LIMIT 1);
55
 * EXPLAIN SELECT  id_image FROM v_del_image vdi WHERE vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1 LIMIT 1);
56
 *	MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery
56
 *	MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery
57
 * EXPLAIN SELECT * FROM del_image WHERE id_image IN (SELECT 3);
57
 * EXPLAIN SELECT * FROM del_image WHERE id_image IN (SELECT 3);
58
 *	PRIMARY
58
 *	PRIMARY
59
 * EXPLAIN SELECT * FROM del_image WHERE id_image IN (SELECT MIN(3));
59
 * EXPLAIN SELECT * FROM del_image WHERE id_image IN (SELECT MIN(3));
60
 *	DEPENDENT SUBQUERY ... ... ... mwarf !
60
 *	DEPENDENT SUBQUERY ... ... ... mwarf !
61
 * EXPLAIN SELECT  id_image FROM v_del_image vdi WHERE vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1);
61
 * EXPLAIN SELECT  id_image FROM v_del_image vdi WHERE vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1);
62
 *	5.5: MATERIALIZED		del_image_tag	ALL				ce_image NULL NULL NULL 38276 Using where
62
 *	5.5: MATERIALIZED		del_image_tag	ALL				ce_image NULL NULL NULL 38276 Using where
63
 *	5.1: DEPENDENT SUBQUERY	del_image_tag	index_subquery	ce_image ce_image 8 func 1 Using where
63
 *	5.1: DEPENDENT SUBQUERY	del_image_tag	index_subquery	ce_image ce_image 8 func 1 Using where
64
 * FORCE INDEX/IGNORE INDEX semble incapable de résoudre le problème de l'optimiseur MySQL
64
 * FORCE INDEX/IGNORE INDEX semble incapable de résoudre le problème de l'optimiseur MySQL
65
 *
65
 *
66
 */
66
 */
67
 
67
 
68
require_once(dirname(__FILE__) . '/../DelTk.php');
68
require_once(dirname(__FILE__) . '/../DelTk.php');
69
require_once(dirname(__FILE__) . '/../observations/ListeObservations.php');
-
 
70
require_once(dirname(__FILE__) . '/../observations/Observation.php');
69
require_once(dirname(__FILE__) . '/../observations/Observation.php');
71
restore_error_handler();
70
restore_error_handler();
72
restore_exception_handler();
71
restore_exception_handler();
73
error_reporting(E_ALL);
72
error_reporting(E_ALL);
74
 
73
 
75
// del/services/0.1/images?navigation.depart=0&navigation.limite=12&tri=votes&ordre=desc
74
// del/services/0.1/images?navigation.depart=0&navigation.limite=12&tri=votes&ordre=desc
76
// del/services/0.1/images?navigation.depart=0&navigation.limite=12&tri=votes&ordre=desc&masque=plop
75
// del/services/0.1/images?navigation.depart=0&navigation.limite=12&tri=votes&ordre=desc&masque=plop
77
// del/services/0.1/images?navigation.depart=0&navigation.limite=12&tri=votes&ordre=desc&protocole=3
76
// del/services/0.1/images?navigation.depart=0&navigation.limite=12&tri=votes&ordre=desc&protocole=3
78
// del/services/0.1/images?navigation.depart=0&navigation.limite=12&tri=votes&ordre=desc&protocole=3&masque=plop
77
// del/services/0.1/images?navigation.depart=0&navigation.limite=12&tri=votes&ordre=desc&protocole=3&masque=plop
79
 
78
 
80
class ListeImages2 {
79
class ListeImages2 {
81
 
80
 
82
	// TODO: PHP-x.y, ces variables devrait être des "const"
81
	// TODO: PHP-x.y, ces variables devrait être des "const"
83
	static $format_image_possible = array('O','CRX2S','CRS','CXS','CS','XS','S','M','L','XL','X2L','X3L');
82
	static $format_image_possible = array('O','CRX2S','CRS','CXS','CS','XS','S','M','L','XL','X2L','X3L');
84
 
83
 
85
	static $tri_possible = array('date_transmission', 'date_observation', 'votes', 'tags');
84
	static $tri_possible = array('date_transmission', 'date_observation', 'votes', 'tags');
86
 
85
 
87
	// en plus de ceux dans DelTk
86
	// en plus de ceux dans DelTk
88
	static $parametres_autorises = array('protocole', 'masque.tag_cel', 'masque.tag_pictoflora', 'masque.milieu');
87
	static $parametres_autorises = array('protocole', 'masque.tag_cel', 'masque.tag_pictoflora', 'masque.milieu');
89
 
88
 
90
	static $default_params = array('navigation.depart' => 0, 'navigation.limite' => 10,
89
	static $default_params = array('navigation.depart' => 0, 'navigation.limite' => 10,
91
    'tri' => 'date_transmission', 'ordre' => 'desc',
90
    'tri' => 'date_transmission', 'ordre' => 'desc',
92
    // spécifiques à PictoFlora:
91
    // spécifiques à PictoFlora:
93
    'format' => 'XL');
92
    'format' => 'XL');
94
 
93
 
95
	static $default_proto = 3; // proto par défaut: capitalisation d'img (utilisé uniquement pour tri=(tags|votes))
94
	static $default_proto = 3; // proto par défaut: capitalisation d'img (utilisé uniquement pour tri=(tags|votes))
96
 
95
 
97
	static $mappings = array(
96
	static $mappings = array(
98
		'observations' => array( // v_del_image
97
		'observations' => array( // v_del_image
99
			"id_observation" => 1,
98
			"id_observation" => 1,
100
			"date_observation" => 1,
99
			"date_observation" => 1,
101
			"date_transmission" => 1, 
100
			"date_transmission" => 1, 
102
			"famille" => "determination.famille",
101
			"famille" => "determination.famille",
103
			"nom_sel" => "determination.ns",
102
			"nom_sel" => "determination.ns",
104
			"nom_sel_nn" => "determination.nn",
103
			"nom_sel_nn" => "determination.nn",
105
			"nom_referentiel" => "determination.referentiel",
104
			"nom_referentiel" => "determination.referentiel",
106
			"nt" => "determination.nt",
105
			"nt" => "determination.nt",
107
			"ce_zone_geo" => "id_zone_geo",
106
			"ce_zone_geo" => "id_zone_geo",
108
			"zone_geo" => 1,
107
			"zone_geo" => 1,
109
			"lieudit" => 1,
108
			"lieudit" => 1,
110
			"station" => 1,
109
			"station" => 1,
111
			"milieu" => 1,
110
			"milieu" => 1,
112
			"mots_cles_texte" => "mots_cles_texte",
111
			"mots_cles_texte" => "mots_cles_texte",
113
			"commentaire" => 1,
112
			"commentaire" => 1,
114
			"ce_utilisateur" => "auteur.id",
113
			"ce_utilisateur" => "auteur.id",
115
			"nom_utilisateur" => "auteur.nom",
114
			"nom_utilisateur" => "auteur.nom",
116
			"prenom_utilisateur" => "auteur.prenom",
115
			"prenom_utilisateur" => "auteur.prenom",
117
		),
116
		),
118
		'images' => array( // v_del_image
117
		'images' => array( // v_del_image
119
			'id_image' => 1,
118
			'id_image' => 1,
120
			// l'alias suivant est particulier: in-fine il doit s'appeler mots_cles_texte
119
			// l'alias suivant est particulier: in-fine il doit s'appeler mots_cles_texte
121
			// mais nous afin d'éviter un conflit d'alias nous le renommons plus tard (reformateImagesDoubleIndex)
120
			// mais nous afin d'éviter un conflit d'alias nous le renommons plus tard (reformateImagesDoubleIndex)
122
			'i_mots_cles_texte' => 1
121
			'i_mots_cles_texte' => 1
123
		));
122
		));
124
 
123
 
125
 
124
 
126
	public function __construct(Conteneur $conteneur = null) {
125
	public function __construct(Conteneur $conteneur = null) {
127
		$this->conteneur = $conteneur == null ? new Conteneur() : $conteneur;
126
		$this->conteneur = $conteneur == null ? new Conteneur() : $conteneur;
128
		$this->conteneur->chargerConfiguration('config_images.ini');
127
		$this->conteneur->chargerConfiguration('config_images.ini');
129
		$this->gestionBdd = $conteneur->getGestionBdd();
128
		$this->gestionBdd = $conteneur->getGestionBdd();
130
		$this->bdd = $this->gestionBdd->getBdd();	
129
		$this->bdd = $this->gestionBdd->getBdd();	
131
	}
130
	}
132
 
131
 
133
	public function consulter($ressources, $parametres) {
132
	public function consulter($ressources, $parametres) {
134
		/* Certes nous sélectionnons ici (nom|prenom|courriel)_utilisateur de cel_obs, mais il ne nous intéressent pas
133
		/* Certes nous sélectionnons ici (nom|prenom|courriel)_utilisateur de cel_obs, mais il ne nous intéressent pas
135
		   Par contre, ci-dessous nous prenons i_(nom|prenom|courriel)_utilisateur.
134
		   Par contre, ci-dessous nous prenons i_(nom|prenom|courriel)_utilisateur.
136
		   Notons cependant qu'aucun moyen ne devrait permettre que i_*_utilisateur != *_utilisateur
135
		   Notons cependant qu'aucun moyen ne devrait permettre que i_*_utilisateur != *_utilisateur
137
		   Le propriétaire d'une obs et de l'image associée est *toujours* le même. */
136
		   Le propriétaire d'une obs et de l'image associée est *toujours* le même. */
138
		array_walk(self::$mappings['observations'], create_function('&$val, $k', 'if($val==1) $val = $k;'));
137
		array_walk(self::$mappings['observations'], create_function('&$val, $k', 'if($val==1) $val = $k;'));
139
		array_walk(self::$mappings['images'], create_function('&$val, $k', 'if($val==1) $val = $k;'));
138
		array_walk(self::$mappings['images'], create_function('&$val, $k', 'if($val==1) $val = $k;'));
140
		// pour les votes, les mappings de "Observation" nous suffisent
139
		// pour les votes, les mappings de "Observation" nous suffisent
141
		array_walk(Observation::$mappings['votes'], create_function('&$val, $k', 'if($val==1) $val = $k;'));
140
		array_walk(Observation::$mappings['votes'], create_function('&$val, $k', 'if($val==1) $val = $k;'));
142
 
141
 
143
		// la nécessité du 'groupby' dépend des 'join's utilisés (LEFT ou INNER) ainsi que de la cardinalité
142
		// la nécessité du 'groupby' dépend des 'join's utilisés (LEFT ou INNER) ainsi que de la cardinalité
144
		// de `ce_image` dans ces tables jointes.
143
		// de `ce_image` dans ces tables jointes.
145
		// Contrairement à IdentiPlantes, nous n'avons de HAVING pour PictoFlora, mais par contre un ORDER BY
144
		// Contrairement à IdentiPlantes, nous n'avons de HAVING pour PictoFlora, mais par contre un ORDER BY
146
		$req = array('select' => array(), 'join' => array(), 'where' => array(), 'groupby' => array(), 'orderby' => array());
145
		$req = array('select' => array(), 'join' => array(), 'where' => array(), 'groupby' => array(), 'orderby' => array());
147
 
146
 
148
 
147
 
149
		$db = $this->bdd;
148
		$db = $this->bdd;
150
 
149
 
151
		// filtrage de l'INPUT général, on réutilise 90% de identiplante en terme de paramètres autorisés
150
		// filtrage de l'INPUT général, on réutilise 90% de identiplante en terme de paramètres autorisés
152
		// ($parametres_autorises) sauf... masque.type qui fait des modif' de WHERE sur les mots-clefs.
151
		// ($parametres_autorises) sauf... masque.type qui fait des modif' de WHERE sur les mots-clefs.
153
		// Évitons ce genre de chose pour PictoFlora et les risques de conflits avec masque.tag
152
		// Évitons ce genre de chose pour PictoFlora et les risques de conflits avec masque.tag
154
		// même si ceux-ci sont improbables (pas d'<input> pour cela).
153
		// même si ceux-ci sont improbables (pas d'<input> pour cela).
155
		$params_ip = DelTk::requestFilterParams($parametres,
154
		$params_ip = DelTk::requestFilterParams($parametres,
156
        array_diff(DelTk::$parametres_autorises,
155
        array_diff(DelTk::$parametres_autorises,
157
        array('masque.type')),
156
        array('masque.type')),
158
        $this->conteneur);
157
        $this->conteneur);
159
 
158
 
160
		// notre propre filtrage sur l'INPUT
159
		// notre propre filtrage sur l'INPUT
161
		$params_pf = self::requestFilterParams($parametres,
160
		$params_pf = self::requestFilterParams($parametres,
162
        array_merge(DelTk::$parametres_autorises,
161
        array_merge(DelTk::$parametres_autorises,
163
        self::$parametres_autorises));
162
        self::$parametres_autorises));
164
 
163
 
165
		/* filtrage des tags + sémantique des valeurs multiples:
164
		/* filtrage des tags + sémantique des valeurs multiples:
166
		   Lorsqu'on utilise masque.tag* pour chercher des tags, ils sont
165
		   Lorsqu'on utilise masque.tag* pour chercher des tags, ils sont
167
		   postulés comme séparés par des virgule, et l'un au moins des tags doit matcher. */
166
		   postulés comme séparés par des virgule, et l'un au moins des tags doit matcher. */
168
		$params_pf['masque.tag_cel'] = DelTk::buildTagsAST(@$parametres['masque.tag_cel'], 'OR', ',');
167
		$params_pf['masque.tag_cel'] = DelTk::buildTagsAST(@$parametres['masque.tag_cel'], 'OR', ',');
169
		$params_pf['masque.tag_pictoflora'] = DelTk::buildTagsAST(@$parametres['masque.tag_pictoflora'], 'OR', ',');
168
		$params_pf['masque.tag_pictoflora'] = DelTk::buildTagsAST(@$parametres['masque.tag_pictoflora'], 'OR', ',');
170
 
169
 
171
		$params = array_merge(
170
		$params = array_merge(
172
            DelTk::$default_params, // paramètre par défaut Identiplante
171
            DelTk::$default_params, // paramètre par défaut Identiplante
173
            self::$default_params, // paramètres par défaut PictoFlora
172
            self::$default_params, // paramètres par défaut PictoFlora
174
            $params_ip, // les paramètres passés, traités par Identiplante
173
            $params_ip, // les paramètres passés, traités par Identiplante
175
            $params_pf); // les paramètres passés, traités par PictoFlora
174
            $params_pf); // les paramètres passés, traités par PictoFlora
176
 
175
 
177
		// XXX: temp tweak
176
		// XXX: temp tweak
178
		/* $this->conteneur->setParametre('url_images', sprintf($this->conteneur->getParametre('url_images'),
177
		/* $this->conteneur->setParametre('url_images', sprintf($this->conteneur->getParametre('url_images'),
179
		   "%09d", $params['format']));*/
178
		   "%09d", $params['format']));*/
180
 
179
 
181
		// création des contraintes (génériques de DelTk)
180
		// création des contraintes (génériques de DelTk)
182
		DelTk::sqlAddConstraint($params, $db, $req);
181
		DelTk::sqlAddConstraint($params, $db, $req);
183
		// création des contraintes héritées de Identiplante (TODO: needed ??)
-
 
184
		ListeObservations::sqlAddConstraint($params, $db, $req, $this->conteneur);
-
 
185
		// création des contraintes spécifiques (sur les tags essentiellement)
182
		// création des contraintes spécifiques (sur les tags essentiellement)
186
		self::sqlAddConstraint($params, $db, $req, $this->conteneur);
183
		self::sqlAddConstraint($params, $db, $req, $this->conteneur);
187
		// création des contraintes spécifiques impliquées par le masque général
184
		// création des contraintes spécifiques impliquées par le masque général
188
		self::sqlAddMasqueConstraint($params, $db, $req, $this->conteneur);
185
		self::sqlAddMasqueConstraint($params, $db, $req, $this->conteneur);
189
		// l'ORDER BY s'avére complexe
186
		// l'ORDER BY s'avére complexe
190
		self::sqlOrderBy($params, $db, $req);
187
		self::sqlOrderBy($params, $db, $req);
191
 
188
 
192
		// 1) grunt-work: *la* requête de récupération des id valides (+ SQL_CALC_FOUND_ROWS)
189
		// 1) grunt-work: *la* requête de récupération des id valides (+ SQL_CALC_FOUND_ROWS)
193
		// $idobs_tab = ListeObservations::getIdObs($params, $req, $db);
190
		// $idobs_tab = ListeObservations::getIdObs($params, $req, $db);
194
		$idobs_tab = self::getIdImages($params, $req, $db);
191
		$idobs_tab = self::getIdImages($params, $req, $db);
195
 
192
 
196
		// Ce n'est pas la peine de continuer s'il n'y a pas eu de résultats dans la table del_obs_images
193
		// Ce n'est pas la peine de continuer s'il n'y a pas eu de résultats dans la table del_obs_images
197
		if(!$idobs_tab) {
194
		if(!$idobs_tab) {
198
			$resultat = new ResultatService();
195
			$resultat = new ResultatService();
199
			$resultat->corps = array('entete' => DelTk::makeJSONHeader(0, $params, Config::get('url_service')),
196
			$resultat->corps = array('entete' => DelTk::makeJSONHeader(0, $params, Config::get('url_service')),
200
            'resultats' => array());
197
            'resultats' => array());
201
			return $resultat;
198
			return $resultat;
202
			/*
199
			/*
203
              header('HTTP/1.0 404 Not Found');
200
              header('HTTP/1.0 404 Not Found');
204
              // don't die (phpunit)
201
              // don't die (phpunit)
205
              throw(new Exception()); */
202
              throw(new Exception()); */
206
		}
203
		}
207
 
204
 
208
 
205
 
209
		// idobs est une liste (toujours ordonnée) des id d'observations recherchées
206
		// idobs est une liste (toujours ordonnée) des id d'observations recherchées
210
		$idobs = array_values(array_map(create_function('$a', 'return $a["id_image"];'), $idobs_tab));
207
		$idobs = array_values(array_map(create_function('$a', 'return $a["id_image"];'), $idobs_tab));
211
		$total = $db->recuperer('SELECT FOUND_ROWS() AS c'); $total = intval($total['c']);
208
		$total = $db->recuperer('SELECT FOUND_ROWS() AS c'); $total = intval($total['c']);
212
 
209
 
213
		$liaisons = self::chargerImages($db, $idobs);
210
		$liaisons = self::chargerImages($db, $idobs);
214
 
211
 
215
		/* 
212
		/* 
216
        // Q&D
213
        // Q&D
217
        $images = array();
214
        $images = array();
218
        $o = new Observation($this->conteneur);
215
        $o = new Observation($this->conteneur);
219
        foreach($idobs as $i) {
216
        foreach($idobs as $i) {
220
        $images[$i] = $o->consulter(array($i), array('justthrow' => 1));
217
        $images[$i] = $o->consulter(array($i), array('justthrow' => 1));
221
        }
218
        }
222
		*/
219
		*/
223
		list($images, $images_keyed_by_id_image) = self::reformateImagesDoubleIndex(
220
		list($images, $images_keyed_by_id_image) = self::reformateImagesDoubleIndex(
224
			$liaisons,
221
			$liaisons,
225
			$this->conteneur->getParametre('url_images'),
222
			$this->conteneur->getParametre('url_images'),
226
			$params['format']);
223
			$params['format']);
227
 
224
 
228
 
225
 
229
		// on charge les votes pour ces images et pour *tous* les protocoles
226
		// on charge les votes pour ces images et pour *tous* les protocoles
230
		$votes = Observation::chargerVotesImage($db, $liaisons, NULL);
227
		$votes = Observation::chargerVotesImage($db, $liaisons, NULL);
231
 
228
 
232
		// subtilité, nous passons ici le tableau d'images indexé par id_image qui est bien plus pratique pour
229
		// subtilité, nous passons ici le tableau d'images indexé par id_image qui est bien plus pratique pour
233
		// associer les vote à un tableau, puisque nous ne connaissons pas les id d'observation.
230
		// associer les vote à un tableau, puisque nous ne connaissons pas les id d'observation.
234
		// Mais magiquement (par référence), cela va remplir notre tableau indexé par couple d'id (id_image, id_observation)
231
		// Mais magiquement (par référence), cela va remplir notre tableau indexé par couple d'id (id_image, id_observation)
235
		// cf reformateImagesDoubleIndex() à qui revient la tâche de créer ces deux versions simultanément lorsque
232
		// cf reformateImagesDoubleIndex() à qui revient la tâche de créer ces deux versions simultanément lorsque
236
		// c'est encore possible.
233
		// c'est encore possible.
237
		if($votes) Observation::mapVotesToImages($votes, $images_keyed_by_id_image);
234
		if($votes) Observation::mapVotesToImages($votes, $images_keyed_by_id_image);
238
 
235
 
239
		// les deux masques de tags sont transformés en AST dans le processus de construction de la requête.
236
		// les deux masques de tags sont transformés en AST dans le processus de construction de la requête.
240
		// Reprenous les paramètres originaux non-nettoyés (ils sont valables car le nettoyage est déterministe)
237
		// Reprenous les paramètres originaux non-nettoyés (ils sont valables car le nettoyage est déterministe)
241
		$params_header = array_merge($params, array_filter(array('masque.tag_cel' => @$parametres['masque.tag_cel'],
238
		$params_header = array_merge($params, array_filter(array('masque.tag_cel' => @$parametres['masque.tag_cel'],
242
        'masque.tag_pictoflora' => @$parametres['masque.tag_pictoflora'])));
239
        'masque.tag_pictoflora' => @$parametres['masque.tag_pictoflora'])));
243
		$resultat = new ResultatService();
240
		$resultat = new ResultatService();
244
		$resultat->corps = array('entete' => DelTk::makeJSONHeader($total, $params_header, Config::get('url_service')),
241
		$resultat->corps = array('entete' => DelTk::makeJSONHeader($total, $params_header, Config::get('url_service')),
245
        'resultats' => $images);
242
        'resultats' => $images);
246
		return $resultat;
243
		return $resultat;
247
	}
244
	}
248
 
245
 
249
	/**
246
	/**
250
	 * TODO: partie spécifique liées à la complexité de PictoFlora:
247
	 * TODO: partie spécifique liées à la complexité de PictoFlora:
251
	 * génération de la clause ORDER BY (génère la valeur de la clef orderby' de $req)
248
	 * génération de la clause ORDER BY (génère la valeur de la clef orderby' de $req)
252
	 * nécessaire ? tableau sprintf(key (tri) => value (ordre), key => value ...).
249
	 * nécessaire ? tableau sprintf(key (tri) => value (ordre), key => value ...).
253
	 * Cependant il est impensable de joindre sur un AVG() des valeurs des votes pour
250
	 * Cependant il est impensable de joindre sur un AVG() des valeurs des votes pour
254
	 * *chaque* couple (id_image, protocole) de la base afin de trouver les images
251
	 * *chaque* couple (id_image, protocole) de la base afin de trouver les images
255
	 * les "mieux notées", ou bien les images ayant le "plus de tags" (COUNT())
252
	 * les "mieux notées", ou bien les images ayant le "plus de tags" (COUNT())
256
	 */
253
	 */
257
	static function sqlOrderBy($p, $db, &$req) {
254
	static function sqlOrderBy($p, $db, &$req) {
258
		// parmi self::$tri_possible
255
		// parmi self::$tri_possible
259
		if($p['tri'] == 'votes') { // LEFT JOIN sur "dis" ci-dessous
256
		if($p['tri'] == 'votes') { // LEFT JOIN sur "dis" ci-dessous
260
			$req['orderby'] = 'dis.moyenne ' . $p['ordre'] . ', dis.nb_votes ' . $p['ordre'];
257
			$req['orderby'] = 'dis.moyenne ' . $p['ordre'] . ', dis.nb_votes ' . $p['ordre'];
261
			return;
258
			return;
262
		}
259
		}
263
		
260
		
264
		if($p['tri'] == 'tags') { // LEFT JOIN sur "dis" ci-dessous
261
		if($p['tri'] == 'tags') { // LEFT JOIN sur "dis" ci-dessous
265
			$req['orderby'] = 'dis.nb_tags ' . $p['ordre'];
262
			$req['orderby'] = 'dis.nb_tags ' . $p['ordre'];
266
			return;
263
			return;
267
		}
264
		}
268
 
265
 
269
		if($p['tri'] == 'date_observation') {
266
		if($p['tri'] == 'date_observation') {
270
			$req['orderby'] = 'date_observation ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
267
			$req['orderby'] = 'date_observation ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
271
			return;
268
			return;
272
		}
269
		}
273
 
270
 
274
		// tri == 'date_transmission'
271
		// tri == 'date_transmission'
275
		// avant cel:r1860, date_transmission pouvait être NULL
272
		// avant cel:r1860, date_transmission pouvait être NULL
276
		// or nous voulons de la consistence (notamment pour phpunit)
273
		// or nous voulons de la consistence (notamment pour phpunit)
277
		$req['orderby'] = 'date_transmission ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
274
		$req['orderby'] = 'date_transmission ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
278
	}
275
	}
279
 
276
 
280
	/*
277
	/*
281
	 * in $p: un tableau de paramètres, dont:
278
	 * in $p: un tableau de paramètres, dont:
282
	 * - 'masque.tag_cel': *tableau* de mots-clefs à chercher parmi cel_image.mots_clefs_texte
279
	 * - 'masque.tag_cel': *tableau* de mots-clefs à chercher parmi cel_image.mots_clefs_texte
283
	 * - 'masque.tag_pictoflora': *tableau* de mots-clefs à chercher parmi del_image_tag.tag_normalise
280
	 * - 'masque.tag_pictoflora': *tableau* de mots-clefs à chercher parmi del_image_tag.tag_normalise
284
	 * - 'tag_explode_semantic': défini si les éléments sont tous recherchés ou NON
281
	 * - 'tag_explode_semantic': défini si les éléments sont tous recherchés ou NON
285
	 *
282
	 *
286
	 * in/ou: $req: un tableau de structure de requête MySQL
283
	 * in/ou: $req: un tableau de structure de requête MySQL
287
	 *
284
	 *
288
	 * Attention, le fait que nous cherchions masque.tag_cel OU/ET masque.tag_cel
285
	 * Attention, le fait que nous cherchions masque.tag_cel OU/ET masque.tag_cel
289
	 * ne dépend pas de nous, mais du niveau supérieur de construction de la requête:
286
	 * ne dépend pas de nous, mais du niveau supérieur de construction de la requête:
290
	 * Soit directement $this->consulter() si des masque.tag* sont passés
287
	 * Soit directement $this->consulter() si des masque.tag* sont passés
291
	 * (split sur ",", "AND" entre chaque condition, "OR" pour chaque valeur de tag)
288
	 * (split sur ",", "AND" entre chaque condition, "OR" pour chaque valeur de tag)
292
	 * Soit via sqlAddMasqueConstraint():
289
	 * Soit via sqlAddMasqueConstraint():
293
	 * (pas de split, "OR" entre chaque condition) [ comportement historique ]
290
	 * (pas de split, "OR" entre chaque condition) [ comportement historique ]
294
	 * équivalent à:
291
	 * équivalent à:
295
	 * (split sur " ", "OR" entre chaque condition, "AND" pour chaque valeur de tag)
292
	 * (split sur " ", "OR" entre chaque condition, "AND" pour chaque valeur de tag)
296
	 *
293
	 *
297
	 */
294
	 */
298
	static function sqlAddConstraint($p, $db, &$req, Conteneur $c = NULL) {
295
	static function sqlAddConstraint($p, $db, &$req, Conteneur $c = NULL) {
299
		// TODO implement dans DelTk ?
296
		// TODO implement dans DelTk ?
300
		if(!empty($p['masque.milieu'])) {
297
		if(!empty($p['masque.milieu'])) {
301
			$req['where'][] = 'vdi.milieu LIKE '.$db->proteger('%' . $p['masque.milieu'].'%');
298
			$req['where'][] = 'vdi.milieu LIKE '.$db->proteger('%' . $p['masque.milieu'].'%');
302
		}
299
		}
303
 
300
 
304
 
301
 
305
		/* Pour le tri par AVG() des votes nous avons toujours un protocole donné,
302
		/* Pour le tri par AVG() des votes nous avons toujours un protocole donné,
306
		   celui-ci indique sur quels votes porte l'AVG.
303
		   celui-ci indique sur quels votes porte l'AVG.
307
		   (c'est un *vote* qui porte sur un protocole et non l'image elle-même) */
304
		   (c'est un *vote* qui porte sur un protocole et non l'image elle-même) */
308
		/* TODO: perf problème:
305
		/* TODO: perf problème:
309
		   1) SQL_CALC_FOUND_ROWS: fixable en:
306
		   1) SQL_CALC_FOUND_ROWS: fixable en:
310
           - dissociant le comptage de la récup d'id + javascript async
307
           - dissociant le comptage de la récup d'id + javascript async
311
           - ou ne rafraîchir le total *que* pour les requête impliquant un changement de pagination
308
           - ou ne rafraîchir le total *que* pour les requête impliquant un changement de pagination
312
           (paramètre booléen "with-total" par exemple)
309
           (paramètre booléen "with-total" par exemple)
313
		   2) jointure forcées: en utilisant `del_imagè`, nous forçons les 2 premiers
310
		   2) jointure forcées: en utilisant `del_imagè`, nous forçons les 2 premiers
314
           JOIN sur cel_obs_images et cel_obs pour filtrer sur "transmission".
311
           JOIN sur cel_obs_images et cel_obs pour filtrer sur "transmission".
315
           Dénormaliser cette valeur et l'intégrer à `cel_images` ferait économiser cette couteuse
312
           Dénormaliser cette valeur et l'intégrer à `cel_images` ferait économiser cette couteuse
316
           jointure, ... lorsqu'aucun masque portant sur `cel_obs` n'est utilisé
313
           jointure, ... lorsqu'aucun masque portant sur `cel_obs` n'est utilisé
317
		   3) non-problème: l'ordre des joins est forcé par l'usage de la vue:
314
		   3) non-problème: l'ordre des joins est forcé par l'usage de la vue:
318
           (cel_images/cel_obs_images/cel_obs/del_image_stat)
315
           (cel_images/cel_obs_images/cel_obs/del_image_stat)
319
           Cependant c'est à l'optimiseur de définir son ordre préféré. */
316
           Cependant c'est à l'optimiseur de définir son ordre préféré. */
320
		if($p['tri'] == 'votes') {
317
		if($p['tri'] == 'votes') {
321
			// $p['protocole'] *est* défini (cf requestFilterParams())
318
			// $p['protocole'] *est* défini (cf requestFilterParams())
322
			// petite optimisation: INNER JOIN si ordre DESC car les 0 à la fin
319
			// petite optimisation: INNER JOIN si ordre DESC car les 0 à la fin
323
			if($p['ordre'] == 'desc') {
320
			if($p['ordre'] == 'desc') {
324
				// pas de group by nécessaire pour cette jointure
321
				// pas de group by nécessaire pour cette jointure
325
				// PRIMARY KEY (`ce_image`, `ce_protocole`)
322
				// PRIMARY KEY (`ce_image`, `ce_protocole`)
326
				$req['join'][] = sprintf('INNER JOIN del_image_stat dis'.
323
				$req['join'][] = sprintf('INNER JOIN del_image_stat dis'.
327
                ' ON vdi.id_image = dis.ce_image'.
324
                ' ON vdi.id_image = dis.ce_image'.
328
                ' AND dis.ce_protocole = %d',
325
                ' AND dis.ce_protocole = %d',
329
                $p['protocole']);
326
                $p['protocole']);
330
			} else {
327
			} else {
331
				$req['join'][] = sprintf('LEFT JOIN del_image_stat dis'.
328
				$req['join'][] = sprintf('LEFT JOIN del_image_stat dis'.
332
                ' ON vdi.id_image = dis.ce_image'.
329
                ' ON vdi.id_image = dis.ce_image'.
333
                ' AND dis.ce_protocole = %d',
330
                ' AND dis.ce_protocole = %d',
334
                $p['protocole']);
331
                $p['protocole']);
335
				// nécessaire (dup ce_image dans del_image_stat)
332
				// nécessaire (dup ce_image dans del_image_stat)
336
				$req['groupby'][] = 'vdi.id_observation';
333
				$req['groupby'][] = 'vdi.id_observation';
337
			}
334
			}
338
		}
335
		}
339
 
336
 
340
		if($p['tri'] == 'tags') {
337
		if($p['tri'] == 'tags') {
341
			$req['join'][] = sprintf('%s JOIN del_image_stat dis ON vdi.id_image = dis.ce_image',
338
			$req['join'][] = sprintf('%s JOIN del_image_stat dis ON vdi.id_image = dis.ce_image',
342
            ($p['ordre'] == 'desc') ? 'INNER' : 'LEFT');
339
            ($p['ordre'] == 'desc') ? 'INNER' : 'LEFT');
343
			// nécessaire (dup ce_image dans del_image_stat)
340
			// nécessaire (dup ce_image dans del_image_stat)
344
			$req['groupby'][] = 'vdi.id_observation';
341
			$req['groupby'][] = 'vdi.id_observation';
345
		}
342
		}
346
 
343
 
347
		// car il ne sont pas traités par la générique requestFilterParams() les clefs "masque.tag_*"
344
		// car il ne sont pas traités par la générique requestFilterParams() les clefs "masque.tag_*"
348
		// sont toujours présentes; bien que parfois NULL.
345
		// sont toujours présentes; bien que parfois NULL.
349
		if($p['masque.tag_cel']) {
346
		if($p['masque.tag_cel']) {
350
			if(isset($p['masque.tag_cel']['AND'])) {
347
			if(isset($p['masque.tag_cel']['AND'])) {
351
				// TODO: utiliser les tables de mots clefs normaliées dans tb_cel ?
348
				// TODO: utiliser les tables de mots clefs normaliées dans tb_cel ?
352
				// et auquel cas laisser au client le choix du couteux "%" ?
349
				// et auquel cas laisser au client le choix du couteux "%" ?
353
				$tags = $p['masque.tag_cel']['AND'];
350
				$tags = $p['masque.tag_cel']['AND'];
354
				array_walk($tags, create_function('&$val, $k, $db',
351
				array_walk($tags, create_function('&$val, $k, $db',
355
                '$val = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) LIKE %s",
352
                '$val = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) LIKE %s",
356
																  $db->proteger("%".$val."%"));'),
353
																  $db->proteger("%".$val."%"));'),
357
                $db);
354
                $db);
358
				$req['where'][] = '(' . implode(' AND ', $tags) . ')';
355
				$req['where'][] = '(' . implode(' AND ', $tags) . ')';
359
			}
356
			}
360
			else {
357
			else {
361
				$req['where'][] = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) REGEXP %s",
358
				$req['where'][] = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) REGEXP %s",
362
                $db->proteger(implode('|', $p['masque.tag_cel']['OR'])));
359
                $db->proteger(implode('|', $p['masque.tag_cel']['OR'])));
363
			}
360
			}
364
		}
361
		}
365
 
362
 
366
 
363
 
367
		// XXX: utiliser tag plutôt que tag_normalise ?
364
		// XXX: utiliser tag plutôt que tag_normalise ?
368
		if($p['masque.tag_pictoflora']) {
365
		if($p['masque.tag_pictoflora']) {
369
			// pas de LEFT JOIN ? ou bien peut-être en cas de tri, mais nous parlons bien ici d'un masque
366
			// pas de LEFT JOIN ? ou bien peut-être en cas de tri, mais nous parlons bien ici d'un masque
370
			/* $req['join'][] = 'LEFT JOIN del_image_tag dit ON dit.ce_image = vdi.id_image';
367
			/* $req['join'][] = 'LEFT JOIN del_image_tag dit ON dit.ce_image = vdi.id_image';
371
			   $req['where'][] = 'dit.actif = 1'; */
368
			   $req['where'][] = 'dit.actif = 1'; */
372
 
369
 
373
 
370
 
374
            // Note à propos des 4 "@ instruction" ci-dessous (notamment sur recupererTous())
371
            // Note à propos des 4 "@ instruction" ci-dessous (notamment sur recupererTous())
375
            // REGEXP permet un puissant mécanisme de sélection des obs/image à qui sait
372
            // REGEXP permet un puissant mécanisme de sélection des obs/image à qui sait
376
            // l'utiliser, mais peut sortir une erreur en cas de REGEXP invalide
373
            // l'utiliser, mais peut sortir une erreur en cas de REGEXP invalide
377
            // ex: REGEX "^(".
374
            // ex: REGEX "^(".
378
            // Pour l'heure nous ignorons ce type d'erreur car aucun de nos champ de recherche
375
            // Pour l'heure nous ignorons ce type d'erreur car aucun de nos champ de recherche
379
            // ne peuvent (ou ne devrait) comporter des meta-caractères
376
            // ne peuvent (ou ne devrait) comporter des meta-caractères
380
            // ([])?*+\\
377
            // ([])?*+\\
381
 
378
 
382
 
379
 
383
			// ==== commenté pour l'instant pour cause de soucis d'optimiseur MySQL (cf commentaire en intro) ====
380
			// ==== commenté pour l'instant pour cause de soucis d'optimiseur MySQL (cf commentaire en intro) ====
384
			/*
381
			/*
385
              if(isset($p['masque.tag_pictoflora']['AND'])) {
382
              if(isset($p['masque.tag_pictoflora']['AND'])) {
386
              // optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
383
              // optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
387
              // donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
384
              // donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
388
              sort($p['masque.tag_pictoflora']['AND']);
385
              sort($p['masque.tag_pictoflora']['AND']);
389
              $req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
386
              $req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
390
              " GROUP BY ce_image".
387
              " GROUP BY ce_image".
391
              " HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s)",
388
              " HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s)",
392
              $db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND'])));
389
              $db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND'])));
393
              }
390
              }
394
              else {
391
              else {
395
              $req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
392
              $req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
396
              " GROUP BY ce_image".
393
              " GROUP BY ce_image".
397
              " HAVING GROUP_CONCAT(tag_normalise) REGEXP %s)",
394
              " HAVING GROUP_CONCAT(tag_normalise) REGEXP %s)",
398
              $db->proteger(implode('|', $p['masque.tag_pictoflora']['OR'])));
395
              $db->proteger(implode('|', $p['masque.tag_pictoflora']['OR'])));
399
              }
396
              }
400
			*/
397
			*/
401
 
398
 
402
			// ==== XXX: puisque on est bassiné par cette "DEPENDENT SUBQUERY", nous la faisons donc indépendemment ====
399
			// ==== XXX: puisque on est bassiné par cette "DEPENDENT SUBQUERY", nous la faisons donc indépendemment ====
403
			if(isset($p['masque.tag_pictoflora']['AND'])) {
400
			if(isset($p['masque.tag_pictoflora']['AND'])) {
404
				// optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
401
				// optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
405
				// donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
402
				// donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
406
				sort($p['masque.tag_pictoflora']['AND']);
403
				sort($p['masque.tag_pictoflora']['AND']);
407
 
404
 
408
				// plutôt que db->connexion->query->fetchColumn(), une API pourrie nous oblige à ...
405
				// plutôt que db->connexion->query->fetchColumn(), une API pourrie nous oblige à ...
409
				$ids = @$db->recupererTous(sprintf(
406
				$ids = @$db->recupererTous(sprintf(
410
					"SELECT ce_image FROM del_image_tag WHERE actif = 1".
407
					"SELECT ce_image FROM del_image_tag WHERE actif = 1".
411
					" GROUP BY ce_image".
408
					" GROUP BY ce_image".
412
					" HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s",
409
					" HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s",
413
					$db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND']))));
410
					$db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND']))));
414
 
411
 
415
				// puis:
412
				// puis:
416
				$ids = @array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
413
				$ids = @array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
417
				if($ids) $req['where'][] = sprintf("vdi.id_image IN (%s)", implode(',', $ids));
414
				if($ids) $req['where'][] = sprintf("vdi.id_image IN (%s)", implode(',', $ids));
418
 
415
 
419
			}
416
			}
420
			else {
417
			else {
421
				$ids = @$db->recupererTous(sprintf(
418
				$ids = @$db->recupererTous(sprintf(
422
					"SELECT ce_image FROM del_image_tag WHERE actif = 1".
419
					"SELECT ce_image FROM del_image_tag WHERE actif = 1".
423
					" GROUP BY ce_image".
420
					" GROUP BY ce_image".
424
					" HAVING GROUP_CONCAT(tag_normalise) REGEXP %s",
421
					" HAVING GROUP_CONCAT(tag_normalise) REGEXP %s",
425
					$db->proteger(implode('|', $p['masque.tag_pictoflora']['OR']))));
422
					$db->proteger(implode('|', $p['masque.tag_pictoflora']['OR']))));
426
 
423
 
427
				$ids = @array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
424
				$ids = @array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
428
				if($ids) $req['where'][] = sprintf("vdi.id_image IN (%s)", implode(',', $ids));
425
				if($ids) $req['where'][] = sprintf("vdi.id_image IN (%s)", implode(',', $ids));
429
			}
426
			}
430
 
427
 
431
		}
428
		}
432
	}
429
	}
433
 
430
 
434
 
431
 
435
	static function getIdImages($p, $req, $db) {
432
	static function getIdImages($p, $req, $db) {
436
		return $db->recupererTous(sprintf(
433
		return $db->recupererTous(sprintf(
437
			'SELECT SQL_CALC_FOUND_ROWS id_image' .
434
			'SELECT SQL_CALC_FOUND_ROWS id_image' .
438
			' FROM v_del_image vdi'.
435
			' FROM v_del_image vdi'.
439
			' %s' . // LEFT JOIN if any
436
			' %s' . // LEFT JOIN if any
440
			' WHERE %s'. // where-clause ou TRUE
437
			' WHERE %s'. // where-clause ou TRUE
441
			' %s'. // group-by
438
			' %s'. // group-by
442
			' ORDER BY %s'.
439
			' ORDER BY %s'.
443
			' LIMIT %d, %d -- %s',
440
			' LIMIT %d, %d -- %s',
444
						 
441
						 
445
			$req['join'] ? implode(' ', array_unique($req['join'])) : '',
442
			$req['join'] ? implode(' ', array_unique($req['join'])) : '',
446
			$req['where'] ? implode(' AND ', $req['where']) : 'TRUE',
443
			$req['where'] ? implode(' AND ', $req['where']) : 'TRUE',
447
			
444
			
448
			$req['groupby'] ? ('GROUP BY ' . implode(', ', array_unique($req['groupby']))) : '',
445
			$req['groupby'] ? ('GROUP BY ' . implode(', ', array_unique($req['groupby']))) : '',
449
			
446
			
450
			$req['orderby'],
447
			$req['orderby'],
451
			
448
			
452
			$p['navigation.depart'], $p['navigation.limite'], __FILE__ . ':' . __LINE__));
449
			$p['navigation.depart'], $p['navigation.limite'], __FILE__ . ':' . __LINE__));
453
 
450
 
454
	}
451
	}
455
 
452
 
456
	static function chargerImages($db, $idImg) {
453
	static function chargerImages($db, $idImg) {
457
		$obs_fields = DelTk::sqlFieldsToAlias(self::$mappings['observations'], NULL);
454
		$obs_fields = DelTk::sqlFieldsToAlias(self::$mappings['observations'], NULL);
458
		$image_fields = DelTk::sqlFieldsToAlias(self::$mappings['images'], NULL);
455
		$image_fields = DelTk::sqlFieldsToAlias(self::$mappings['images'], NULL);
459
	
456
	
460
		return $db->recupererTous(sprintf('SELECT '.
457
		return $db->recupererTous(sprintf('SELECT '.
461
        ' CONCAT(id_image, "-", id_observation) AS jsonindex,'.
458
        ' CONCAT(id_image, "-", id_observation) AS jsonindex,'.
462
        ' %1$s, %2$s FROM v_del_image '.
459
        ' %1$s, %2$s FROM v_del_image '.
463
        ' WHERE %3$s'.
460
        ' WHERE %3$s'.
464
        ' -- %4$s',
461
        ' -- %4$s',
465
        $obs_fields, $image_fields,
462
        $obs_fields, $image_fields,
466
        sprintf('id_image IN (%s)', implode(',', $idImg)),
463
        sprintf('id_image IN (%s)', implode(',', $idImg)),
467
        __FILE__ . ':' . __LINE__));
464
        __FILE__ . ':' . __LINE__));
468
 
465
 
469
	}
466
	}
470
 
467
 
471
	/* "masque" ne fait jamais que faire une requête sur la plupart des champs, (presque) tous traités
468
	/* "masque" ne fait jamais que faire une requête sur la plupart des champs, (presque) tous traités
472
	   de manière identique à la seule différence que:
469
	   de manière identique à la seule différence que:
473
	   1) ils sont combinés par des "OU" logiques plutôt que des "ET".
470
	   1) ils sont combinés par des "OU" logiques plutôt que des "ET".
474
	   2) les tags sont traités différemment pour conserver la compatibilité avec l'utilisation historique:
471
	   2) les tags sont traités différemment pour conserver la compatibilité avec l'utilisation historique:
475
       Tous les mots-clefs doivent matcher et sont séparés par des espaces
472
       Tous les mots-clefs doivent matcher et sont séparés par des espaces
476
       (dit autrement, str_replace(" ", ".*", $mots-clefs-requête) =~ $mots-clefs-mysql)
473
       (dit autrement, str_replace(" ", ".*", $mots-clefs-requête) =~ $mots-clefs-mysql)
477
	   Pour plus d'information: ListeObservations::sqlAddMasqueConstraint() */
474
	   Pour plus d'information: (ListeObservations|DelTk)::sqlAddMasqueConstraint() */
478
	static function sqlAddMasqueConstraint($p, $db, &$req, Conteneur $c = NULL) {
475
	static function sqlAddMasqueConstraint($p, $db, &$req, Conteneur $c = NULL) {
479
		if(!empty($p['masque'])) {
476
		if(!empty($p['masque'])) {
480
			$or_params = array('masque.auteur' => $p['masque'],
477
			$or_params = array('masque.auteur' => $p['masque'],
481
            'masque.departement' => $p['masque'],
478
            'masque.departement' => $p['masque'],
482
            'masque.commune' => $p['masque'], // TODO/XXX ?
479
            'masque.commune' => $p['masque'], // TODO/XXX ?
483
            'masque.id_zone_geo' => $p['masque'],
480
            'masque.id_zone_geo' => $p['masque'],
484
 
481
 
485
            /* tous-deux remplacent masque.tag
482
            /* tous-deux remplacent masque.tag
486
               mais sont traité séparément des requestFilterParams() */
483
               mais sont traité séparément des requestFilterParams() */
487
            // 'masque.tag_cel' => $p['masque'],
484
            // 'masque.tag_cel' => $p['masque'],
488
            // 'masque.tag_pictoflora' => $p['masque'],
485
            // 'masque.tag_pictoflora' => $p['masque'],
489
 
486
 
490
            'masque.ns' => $p['masque'],
487
            'masque.ns' => $p['masque'],
491
            'masque.famille' => $p['masque'],
488
            'masque.famille' => $p['masque'],
492
            'masque.date' => $p['masque'],
489
            'masque.date' => $p['masque'],
493
            'masque.genre' => $p['masque'],
490
            'masque.genre' => $p['masque'],
494
            'masque.milieu' => $p['masque'],
491
            'masque.milieu' => $p['masque'],
495
 
492
 
496
            // tri est aussi nécessaire car affecte les contraintes de JOIN
493
            // tri est aussi nécessaire car affecte les contraintes de JOIN
497
            'tri' => $p['tri'],
494
            'tri' => $p['tri'],
498
            'ordre' => $p['ordre']);
495
            'ordre' => $p['ordre']);
499
 
496
 
500
			$or_masque = array_merge(
497
			$or_masque = array_merge(
501
				DelTk::requestFilterParams($or_params, NULL, $c /* pour masque.departement */),
498
				DelTk::requestFilterParams($or_params, NULL, $c /* pour masque.departement */),
502
				self::requestFilterParams($or_params));
499
				self::requestFilterParams($or_params));
503
 
500
 
504
			/* Lorsqu'on utilise le masque général pour chercher des tags, ils sont
501
			/* Lorsqu'on utilise le masque général pour chercher des tags, ils sont
505
			   postulés comme séparés par des espaces, et doivent être tous matchés. */
502
			   postulés comme séparés par des espaces, et doivent être tous matchés. */
506
			$or_masque['masque.tag_cel'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
503
			$or_masque['masque.tag_cel'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
507
			$or_masque['masque.tag_pictoflora'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
504
			$or_masque['masque.tag_pictoflora'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
508
 
505
 
509
 
506
 
510
			// pas de select, groupby & co ici: uniquement 'join' et 'where'
507
			// pas de select, groupby & co ici: uniquement 'join' et 'where'
511
			$or_req = array('join' => array(), 'where' => array());
508
			$or_req = array('join' => array(), 'where' => array());
512
			DelTk::sqlAddConstraint($or_masque, $db, $or_req);
509
			DelTk::sqlAddConstraint($or_masque, $db, $or_req);
513
			ListeObservations::sqlAddConstraint($or_masque, $db, $or_req);
-
 
514
			self::sqlAddConstraint($or_masque, $db, $or_req);
510
			self::sqlAddConstraint($or_masque, $db, $or_req);
515
 
511
 
516
			if($or_req['where']) {
512
			if($or_req['where']) {
517
				$req['where'][] = '(' . implode(' OR ', $or_req['where']) . ')';
513
				$req['where'][] = '(' . implode(' OR ', $or_req['where']) . ')';
518
				// utile au cas ou des jointures seraient rajoutées
514
				// utile au cas ou des jointures seraient rajoutées
519
				$req['join'] = array_unique(array_merge($req['join'], $or_req['join']));
515
				$req['join'] = array_unique(array_merge($req['join'], $or_req['join']));
520
			}
516
			}
521
		}
517
		}
522
	}
518
	}
523
 
519
 
524
 
520
 
525
	// cf Observation::reformateObservationSimpleIndex() et ListeObservations::reformateObservation()
521
	// cf Observation::reformateObservationSimpleIndex() et ListeObservations::reformateObservation()
526
    // (trop de variétés de formatage, à unifier côté client pour unifier côté backend ...)
522
    // (trop de variétés de formatage, à unifier côté client pour unifier côté backend ...)
527
	static function reformateImagesDoubleIndex($obs, $url_pattern = '', $image_format = 'XL') {
523
	static function reformateImagesDoubleIndex($obs, $url_pattern = '', $image_format = 'XL') {
528
		// XXX: cf Observation.php::consulter(), nous pourriouns ici
524
		// XXX: cf Observation.php::consulter(), nous pourriouns ici
529
		// conserver les valeurs vides (pour les phptests notamment, ou non)
525
		// conserver les valeurs vides (pour les phptests notamment, ou non)
530
		// $obs = array_map('array_filter', $obs);
526
		// $obs = array_map('array_filter', $obs);
531
		$obs_merged = $obs_keyed_by_id_image = array();
527
		$obs_merged = $obs_keyed_by_id_image = array();
532
		foreach($obs as $o) {
528
		foreach($obs as $o) {
533
			// ceci nous complique la tâche pour le reste du processing...
529
			// ceci nous complique la tâche pour le reste du processing...
534
			$id = $o['jsonindex'];
530
			$id = $o['jsonindex'];
535
			// ainsi nous utilisons deux tableaux: le final, indexé par couple d'id(image-obs)
531
			// ainsi nous utilisons deux tableaux: le final, indexé par couple d'id(image-obs)
536
			// et celui indexé par simple id_image qui est fort utile pour mapVotesToImages()
532
			// et celui indexé par simple id_image qui est fort utile pour mapVotesToImages()
537
			// mais tout deux partage leur référence à "protocole"
533
			// mais tout deux partage leur référence à "protocole"
538
			$image = array(
534
			$image = array(
539
				'id_image' => $o['id_image'],
535
				'id_image' => $o['id_image'],
540
				'binaire.href' => sprintf($url_pattern, $o['id_image'], $image_format),
536
				'binaire.href' => sprintf($url_pattern, $o['id_image'], $image_format),
541
				'mots_cles_texte' => @$o['i_mots_cles_texte'], // @, peut avoir été filtré par array_map() ci-dessus
537
				'mots_cles_texte' => @$o['i_mots_cles_texte'], // @, peut avoir été filtré par array_map() ci-dessus
542
			);
538
			);
543
			unset($o['id_image'], $o['i_mots_cles_texte'], $o['jsonindex']);
539
			unset($o['id_image'], $o['i_mots_cles_texte'], $o['jsonindex']);
544
			if(!isset($obs_merged[$id])) $obs_merged[$id] = $image;
540
			if(!isset($obs_merged[$id])) $obs_merged[$id] = $image;
545
			$obs_merged[$id]['observation'] = $o;
541
			$obs_merged[$id]['observation'] = $o;
546
			$obs_merged[$id]['protocoles_votes'] = array();
542
			$obs_merged[$id]['protocoles_votes'] = array();
547
			
543
			
548
			$obs_keyed_by_id_image[$image['id_image']]['protocoles_votes'] = &$obs_merged[$id]['protocoles_votes'];
544
			$obs_keyed_by_id_image[$image['id_image']]['protocoles_votes'] = &$obs_merged[$id]['protocoles_votes'];
549
		}
545
		}
550
 
546
 
551
		return array($obs_merged,$obs_keyed_by_id_image);
547
		return array($obs_merged,$obs_keyed_by_id_image);
552
	}
548
	}
553
 
549
 
554
 
550
 
555
 
551
 
556
	// complete & override DelTk::requestFilterParams() (même usage)
552
	// complete & override DelTk::requestFilterParams() (même usage)
557
	static function requestFilterParams(Array $params, $parametres_autorises = NULL) {
553
	static function requestFilterParams(Array $params, $parametres_autorises = NULL) {
558
		if($parametres_autorises) { // filtrage de toute clef inconnue
554
		if($parametres_autorises) { // filtrage de toute clef inconnue
559
			$params = array_intersect_key($params, array_flip($parametres_autorises));
555
			$params = array_intersect_key($params, array_flip($parametres_autorises));
560
		}
556
		}
561
 
557
 
562
		$p = array();
558
		$p = array();
563
		$p['tri'] = DelTk::unsetIfInvalid($params, 'tri', self::$tri_possible);
559
		$p['tri'] = DelTk::unsetIfInvalid($params, 'tri', self::$tri_possible);
564
		$p['format'] = DelTk::unsetIfInvalid($params, 'format', self::$format_image_possible);
560
		$p['format'] = DelTk::unsetIfInvalid($params, 'format', self::$format_image_possible);
565
 
561
 
566
		// "milieu" inutile pour IdentiPlantes ?
562
		// "milieu" inutile pour IdentiPlantes ?
567
		if(isset($params['masque.milieu'])) $p['masque.milieu'] = trim($params['masque.milieu']);
563
		if(isset($params['masque.milieu'])) $p['masque.milieu'] = trim($params['masque.milieu']);
568
 
564
 
569
		// compatibilité
565
		// compatibilité
570
		if(isset($params['masque.tag'])) {
566
		if(isset($params['masque.tag'])) {
571
			$params['masque.tag_cel'] = $params['masque.tag_pictoflora'] = $params['masque.tag'];
567
			$params['masque.tag_cel'] = $params['masque.tag_pictoflora'] = $params['masque.tag'];
572
		}
568
		}
573
 
569
 
574
		if($p['tri'] == 'votes' || $p['tri'] == 'tags') {
570
		if($p['tri'] == 'votes' || $p['tri'] == 'tags') {
575
			// ces critère de tri des image à privilégier ne s'applique qu'à un protocole donné
571
			// ces critère de tri des image à privilégier ne s'applique qu'à un protocole donné
576
			if(!isset($params['protocole']) || !is_numeric($params['protocole']))
572
			if(!isset($params['protocole']) || !is_numeric($params['protocole']))
577
				$p['protocole'] = self::$default_proto;
573
				$p['protocole'] = self::$default_proto;
578
			else
574
			else
579
				$p['protocole'] = intval($params['protocole']);
575
				$p['protocole'] = intval($params['protocole']);
580
		}
576
		}
581
 
577
 
582
		return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
578
		return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
583
	}
579
	}
584
 
580
 
585
 
581
 
586
 
582
 
587
	// met à jour *toutes* les stats de nombre de tags et de moyenne des votes
583
	// met à jour *toutes* les stats de nombre de tags et de moyenne des votes
588
	static function _update_statistics($db) {
584
	static function _update_statistics($db) {
589
		$db->requeter("TRUNCATE TABLE del_image_stat");
585
		$db->requeter("TRUNCATE TABLE del_image_stat");
590
		$db->requeter(<<<EOF
586
		$db->requeter(<<<EOF
591
INSERT INTO `del_image_stat` (
587
INSERT INTO `del_image_stat` (
592
	SELECT id_image, divo.ce_protocole, divo.moyenne, divo.nb_votes, dit.ctags 
588
	SELECT id_image, divo.ce_protocole, divo.moyenne, divo.nb_votes, dit.ctags 
593
	FROM `tb_cel`.`cel_images` ci 
589
	FROM `tb_cel`.`cel_images` ci 
594
	LEFT JOIN 
590
	LEFT JOIN 
595
	( SELECT ce_image, ce_protocole, AVG(valeur) AS moyenne, COUNT(valeur) AS nb_votes FROM del_image_vote 
591
	( SELECT ce_image, ce_protocole, AVG(valeur) AS moyenne, COUNT(valeur) AS nb_votes FROM del_image_vote 
596
	  GROUP BY ce_image, ce_protocole ) AS divo
592
	  GROUP BY ce_image, ce_protocole ) AS divo
597
	ON ci.id_image = divo.ce_image 
593
	ON ci.id_image = divo.ce_image 
598
	LEFT JOIN 
594
	LEFT JOIN 
599
	( SELECT ce_image, COUNT(id_tag) as ctags FROM del_image_tag 
595
	( SELECT ce_image, COUNT(id_tag) as ctags FROM del_image_tag 
600
	  GROUP BY ce_image ) AS dit 
596
	  GROUP BY ce_image ) AS dit 
601
	ON ci.id_image = dit.ce_image )
597
	ON ci.id_image = dit.ce_image )
602
EOF
598
EOF
603
		);
599
		);
604
	}
600
	}
605
 
601
 
606
	static function revOrderBy($orderby) {
602
	static function revOrderBy($orderby) {
607
		return $orderby == 'asc' ? 'desc' : 'asc';
603
		return $orderby == 'asc' ? 'desc' : 'asc';
608
	}
604
	}
609
}
605
}