Subversion Repositories eFlore/Applications.del

Rev

Rev 1431 | Rev 1438 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

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