Subversion Repositories eFlore/Applications.del

Rev

Rev 1413 | Rev 1451 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

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