Subversion Repositories eFlore/Applications.del

Rev

Rev 1422 | Rev 1431 | 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) {
181
			header('HTTP/1.0 404 Not Found');
182
			// don't die (phpunit)
183
			throw(new Exception());
1390 raphael 184
		}
185
 
186
 
1422 raphael 187
		// idobs est une liste (toujours ordonnée) des id d'observations recherchées
188
		$idobs = array_values(array_map(create_function('$a', 'return $a["id_image"];'), $idobs_tab));
189
		$total = $db->recuperer('SELECT FOUND_ROWS() AS c'); $total = intval($total['c']);
1390 raphael 190
 
1422 raphael 191
		$liaisons = self::chargerImages($db, $idobs);
192
 
193
		/*
194
		   // Q&D
195
		   $images = array();
196
		   $o = new Observation($this->conteneur);
197
		   foreach($idobs as $i) {
198
		   $images[$i] = $o->consulter(array($i), array('justthrow' => 1));
199
		   }
200
		*/
201
		list($images, $images_keyed_by_id_image) = ListeObservations2::reformateImagesDoubleIndex(
202
			$liaisons,
203
			$this->conteneur->getParametre('url_images'),
204
			$params['format']);
205
 
206
 
207
		// on charge les votes pour ces images et pour *tous* les protocoles
208
		$votes = Observation::chargerVotesImage($db, $liaisons, NULL);
209
 
210
		// subtilité, nous passons ici le tableau d'images indexé par id_image qui est bien plus pratique pour
211
		// associer les vote à un tableau, puisque nous ne connaissons pas les id d'observation.
212
		// Mais magiquement (par référence), cela va remplir notre tableau indexé par couple d'id (id_image, id_observation)
213
		// cf reformateImagesDoubleIndex() à qui revient la tâche de créer ces deux versions simultanément lorsque
214
		// c'est encore possible.
215
		if($votes) Observation::mapVotesToImages($votes, $images_keyed_by_id_image);
216
 
1390 raphael 217
		$resultat = new ResultatService();
218
		$resultat->corps = array('entete' => ListeObservations2::makeJSONHeader($total, $params, Config::get('url_service')),
219
								 'resultats' => $images);
220
		return $resultat;
1422 raphael 221
	}
1390 raphael 222
 
1422 raphael 223
	/**
224
	 * TODO: partie spécifique liées à la complexité de PictoFlora:
225
	 * génération de la clause ORDER BY (génère la valeur de la clef orderby' de $req)
226
	 * nécessaire ? tableau sprintf(key (tri) => value (ordre), key => value ...).
227
	 * Cependant il est impensable de joindre sur un AVG() des valeurs des votes pour
228
	 * *chaque* couple (id_image, protocole) de la base afin de trouver les images
229
	 * les "mieux notées", ou bien les images ayant le "plus de tags" (COUNT())
230
	 */
231
	static function sqlOrderBy($p, $db, &$req) {
232
		// parmi self::$tri_possible
233
		if($p['tri'] == 'votes') { // LEFT JOIN sur "dis" ci-dessous
1430 raphael 234
			$req['orderby'] = 'dis.moyenne ' . $p['ordre'] . ', dis.nb_votes ' . $p['ordre'];
1422 raphael 235
			return;
236
		}
237
 
238
		if($p['tri'] == 'tags') { // LEFT JOIN sur "dis" ci-dessous
239
			$req['orderby'] = 'dis.nb_tags ' . $p['ordre'];
240
			return;
241
		}
242
 
1430 raphael 243
		if($p['tri'] == 'date_observation') {
244
			$req['orderby'] = 'date_observation ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
245
			return;
246
		}
247
 
1422 raphael 248
		// tri == 'date_transmission'
249
		// avant cel:r1860, date_transmission pouvait être NULL
250
		// or nous voulons de la consistence (notamment pour phpunit)
1430 raphael 251
		$req['orderby'] = 'date_transmission ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
1390 raphael 252
	}
253
 
1422 raphael 254
	/*
255
	 * in $p: un tableau de paramètres, dont:
256
	 * - 'masque.tag_cel': *tableau* de mots-clefs à chercher parmi cel_image.mots_clefs_texte
257
	 * - 'masque.tag_pictoflora': *tableau* de mots-clefs à chercher parmi del_image_tag.tag_normalise
258
	 * - 'tag_explode_semantic': défini si les éléments sont tous recherchés ou NON
259
	 *
260
	 * in/ou: $req: un tableau de structure de requête MySQL
261
	 *
262
	 * Attention, le fait que nous cherchions masque.tag_cel OU/ET masque.tag_cel
263
	 * ne dépend pas de nous, mais du niveau supérieur de construction de la requête:
264
	 * Soit directement $this->consulter() si des masque.tag* sont passés
265
	 * (split sur ",", "AND" entre chaque condition, "OR" pour chaque valeur de tag)
266
	 * Soit via sqlAddMasqueConstraint():
267
	 * (pas de split, "OR" entre chaque condition) [ comportement historique ]
268
	 * équivalent à:
269
	 * (split sur " ", "OR" entre chaque condition, "AND" pour chaque valeur de tag)
270
	 *
271
	 */
272
	static function sqlAddConstraint($p, $db, &$req, Conteneur $c = NULL) {
273
		// TODO implement dans ListeObservations2 ?
274
		if(!empty($p['masque.milieu'])) {
275
			$req['where'][] = 'vdi.milieu LIKE '.$db->proteger('%' . $p['masque.milieu'].'%');
276
		}
277
 
278
 
279
		/* Pour le tri par AVG() des votes nous avons toujours un protocole donné,
280
		   celui-ci indique sur quels votes porte l'AVG.
281
		   (c'est un *vote* qui porte sur un protocole et non l'image elle-même) */
282
		/* TODO: perf problème:
283
		   1) SQL_CALC_FOUND_ROWS: fixable en:
284
		   	- dissociant le comptage de la récup d'id + javascript async
285
			- ou ne rafraîchir le total *que* pour les requête impliquant un changement de pagination
286
				(paramètre booléen "with-total" par exemple)
287
		   2) jointure forcées: en utilisant `del_imagè`, nous forçons les 2 premiers
288
		   	JOIN sur cel_obs_images et cel_obs pour filtrer sur "transmission".
289
			Dénormaliser cette valeur et l'intégrer à `cel_images` ferait économiser cette couteuse
290
			jointure, ... lorsqu'aucun masque portant sur `cel_obs` n'est utilisé
291
		   3) non-problème: l'ordre des joins est forcé par l'usage de la vue:
292
		   	(cel_images/cel_obs_images/cel_obs/del_image_stat)
293
			Cependant c'est à l'optimiseur de définir son ordre préféré. */
294
		if($p['tri'] == 'votes') {
295
			// $p['protocole'] *est* défini (cf requestFilterParams())
296
			// petite optimisation: INNER JOIN si ordre DESC car les 0 à la fin
297
			if($p['ordre'] == 'desc') {
298
				// pas de group by nécessaire pour cette jointure
299
				// PRIMARY KEY (`ce_image`, `ce_protocole`)
300
				$req['join'][] = sprintf('INNER JOIN del_image_stat dis'.
301
										 ' ON vdi.id_image = dis.ce_image'.
302
										 ' AND dis.ce_protocole = %d',
303
										 $p['protocole']);
304
			} else {
305
				$req['join'][] = sprintf('LEFT JOIN del_image_stat dis'.
306
										 ' ON vdi.id_image = dis.ce_image'.
307
										 ' AND dis.ce_protocole = %d',
308
										 $p['protocole']);
309
				// nécessaire (dup ce_image dans del_image_stat)
310
				$req['groupby'][] = 'vdi.id_observation';
311
			}
312
		}
313
 
314
		if($p['tri'] == 'tags') {
315
			$req['join'][] = sprintf('%s JOIN del_image_stat dis ON vdi.id_image = dis.ce_image',
316
									 ($p['ordre'] == 'desc') ? 'INNER' : 'LEFT');
317
			// nécessaire (dup ce_image dans del_image_stat)
318
			$req['groupby'][] = 'vdi.id_observation';
319
		}
320
 
1430 raphael 321
		// car il ne sont pas traités par la générique requestFilterParams() les clefs "masque.tag_*"
322
		// sont toujours présentes; bien que parfois NULL.
323
		if($p['masque.tag_cel']) {
324
			if(isset($p['masque.tag_cel']['AND'])) {
1422 raphael 325
				// TODO: utiliser les tables de mots clefs normaliées dans tb_cel ?
326
				// et auquel cas laisser au client le choix du couteux "%" ?
1430 raphael 327
				$tags = $p['masque.tag_cel']['AND'];
1422 raphael 328
				array_walk($tags, create_function('&$val, $k, $db',
329
												  '$val = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) LIKE %s",
330
 																  $db->proteger("%".$val."%"));'),
331
						   $db);
1430 raphael 332
				$req['where'][] = '(' . implode(' AND ', $tags) . ')';
1422 raphael 333
			}
1430 raphael 334
			else {
335
				$req['where'][] = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) REGEXP %s",
336
										  $db->proteger(implode('|', $p['masque.tag_cel']['OR'])));
1422 raphael 337
			}
338
		}
339
 
340
 
341
		// XXX: utiliser tag plutôt que tag_normalise ?
1430 raphael 342
		if($p['masque.tag_pictoflora']) {
1422 raphael 343
			// pas de LEFT JOIN ? ou bien peut-être en cas de tri, mais nous parlons bien ici d'un masque
344
			/* $req['join'][] = 'LEFT JOIN del_image_tag dit ON dit.ce_image = vdi.id_image';
345
			   $req['where'][] = 'dit.actif = 1'; */
346
 
1430 raphael 347
			if(isset($p['masque.tag_pictoflora']['AND'])) {
1422 raphael 348
				// optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
349
				// donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
1430 raphael 350
				sort($p['masque.tag_pictoflora']['AND']);
1422 raphael 351
				$req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
352
										  " GROUP BY ce_image".
353
										  " HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s)",
1430 raphael 354
										  $db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND'])));
1422 raphael 355
			}
1430 raphael 356
			else {
1422 raphael 357
				$req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
358
										  " GROUP BY ce_image".
359
										  " HAVING GROUP_CONCAT(tag_normalise) REGEXP %s)",
1430 raphael 360
										  $db->proteger(implode('|', $p['masque.tag_pictoflora']['OR'])));
1422 raphael 361
			}
362
		}
363
	}
364
 
1430 raphael 365
 
1422 raphael 366
	static function getIdImages($p, $req, $db) {
367
		return $db->recupererTous(sprintf(
368
			'SELECT SQL_CALC_FOUND_ROWS id_image' .
369
			' FROM v_del_image vdi'.
370
			' %s' . // LEFT JOIN if any
371
			' WHERE %s'. // where-clause ou TRUE
372
			' %s'. // group-by
373
			' ORDER BY %s'.
374
			' LIMIT %d, %d -- %s',
375
 
376
			$req['join'] ? implode(' ', array_unique($req['join'])) : '',
377
			$req['where'] ? implode(' AND ', $req['where']) : 'TRUE',
378
 
379
			$req['groupby'] ? ('GROUP BY ' . implode(', ', array_unique($req['groupby']))) : '',
380
 
381
			$req['orderby'],
382
 
383
			$p['navigation.depart'], $p['navigation.limite'], __FILE__ . ':' . __LINE__));
384
 
385
	}
386
 
387
	static function chargerImages($db, $idImg) {
388
		$obs_fields = Observation::sqlFieldsToAlias(self::$mappings['observations'], NULL);
389
		$image_fields = Observation::sqlFieldsToAlias(self::$mappings['images'], NULL);
390
 
391
		return $db->recupererTous(sprintf('SELECT '.
392
										  ' CONCAT(id_image, "-", id_observation) AS jsonindex,'.
393
										  ' %1$s, %2$s FROM v_del_image '.
394
										  ' WHERE %3$s'.
395
										  ' -- %4$s',
396
										  $obs_fields, $image_fields,
397
										  sprintf('id_image IN (%s)', implode(',', $idImg)),
398
										  __FILE__ . ':' . __LINE__));
399
 
400
	}
401
 
402
	/* "masque" ne fait jamais que faire une requête sur la plupart des champs, (presque) tous traités
403
	   de manière identique à la seule différence que:
404
	   1) ils sont combinés par des "OU" logiques plutôt que des "ET".
405
	   2) les tags sont traités différemment pour conserver la compatibilité avec l'utilisation historique:
406
	   	  Tous les mots-clefs doivent matcher et sont séparés par des espaces
407
		  (dit autrement, str_replace(" ", ".*", $mots-clefs-requête) =~ $mots-clefs-mysql)
408
	   Pour plus d'information: ListeObservations2::sqlAddMasqueConstraint() */
409
	static function sqlAddMasqueConstraint($p, $db, &$req, Conteneur $c = NULL) {
410
		if(!empty($p['masque'])) {
411
			$or_params = array('masque.auteur' => $p['masque'],
412
							   'masque.departement' => $p['masque'],
413
							   'masque.commune' => $p['masque'], // TODO/XXX ?
414
							   'masque.id_zone_geo' => $p['masque'],
415
 
1430 raphael 416
							   /* tous-deux remplacent masque.tag
417
								  mais sont traité séparément des requestFilterParams() */
418
							   // 'masque.tag_cel' => $p['masque'],
419
							   // 'masque.tag_pictoflora' => $p['masque'],
1422 raphael 420
 
421
							   'masque.ns' => $p['masque'],
422
							   'masque.famille' => $p['masque'],
423
							   'masque.date' => $p['masque'],
424
							   'masque.genre' => $p['masque'],
425
							   'masque.milieu' => $p['masque'],
426
 
427
							   // tri est aussi nécessaire car affecte les contraintes de JOIN
428
							   'tri' => $p['tri'],
429
							   'ordre' => $p['ordre']);
430
 
431
			$or_masque = array_merge(
432
				ListeObservations2::requestFilterParams($or_params, NULL, $c /* pour masque.departement */),
1430 raphael 433
				self::requestFilterParams($or_params));
1422 raphael 434
 
1430 raphael 435
			/* Lorsqu'on utilise le masque général pour chercher des tags, ils sont
436
			   postulés comme séparés par des espaces, et doivent être tous matchés. */
437
			$or_masque['masque.tag_cel'] = self::buildTagsAST($p['masque'], 'AND', ' ');
438
			$or_masque['masque.tag_pictoflora'] = self::buildTagsAST($p['masque'], 'AND', ' ');
439
 
440
 
441
			// pas de select, groupby & co ici: uniquement 'join' et 'where'
1422 raphael 442
			$or_req = array('join' => array(), 'where' => array());
443
			ListeObservations2::sqlAddConstraint($or_masque, $db, $or_req);
444
			self::sqlAddConstraint($or_masque, $db, $or_req);
445
 
446
			if($or_req['where']) {
447
				$req['where'][] = '(' . implode(' OR ', $or_req['where']) . ')';
448
				// utile au cas ou des jointures seraient rajoutées
449
				$req['join'] = array_unique(array_merge($req['join'], $or_req['join']));
450
			}
451
		}
452
	}
453
 
1430 raphael 454
	// complete & override ListeObservations2::requestFilterParams() (même usage)
1422 raphael 455
	static function requestFilterParams(Array $params, $parametres_autorises = NULL) {
456
		if($parametres_autorises) { // filtrage de toute clef inconnue
457
			$params = array_intersect_key($params, array_flip($parametres_autorises));
458
		}
459
 
460
		$p = array();
461
		$p['tri'] = ListeObservations2::unsetIfInvalid($params, 'tri', self::$tri_possible);
1390 raphael 462
		$p['format'] = ListeObservations2::unsetIfInvalid($params, 'format', self::$format_image_possible);
1422 raphael 463
 
1430 raphael 464
		// "milieu" inutile pour IdentiPlantes ?
1422 raphael 465
		if(isset($params['masque.milieu'])) $p['masque.milieu'] = trim($params['masque.milieu']);
466
 
467
		// compatibilité
468
		if(isset($params['masque.tag'])) {
469
			$params['masque.tag_cel'] = $params['masque.tag_pictoflora'] = $params['masque.tag'];
470
		}
471
 
472
		if($p['tri'] == 'votes' || $p['tri'] == 'tags') {
473
			// ces critère de tri des image à privilégier ne s'applique qu'à un protocole donné
474
			if(!isset($params['protocole']) || !is_numeric($params['protocole']))
475
				$p['protocole'] = self::$default_proto;
476
			else
477
				$p['protocole'] = intval($params['protocole']);
478
		}
479
 
480
		return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
1390 raphael 481
	}
1422 raphael 482
 
483
 
1430 raphael 484
	/* Construit un (vulgaire) abstract syntax tree:
485
	   "AND" => [ "tag1", "tag2" ]
486
	   Idéalement (avec un parser simple comme proposé par http://hoa-project.net/Literature/Hack/Compiler.html#Langage_PP)
487
	   nous aurions:
488
	   "AND" => [ "tag1", "tag2", "OR" => [ "tag3", "tag4" ] ]
1422 raphael 489
 
1430 raphael 490
	   Ici nous devons traiter les cas suivants:
491
	   tags séparés par des "ET/AND OU/OR", séparés par des espaces ou des virgules.
492
	   Mais la chaîne peut aussi avoir été issue du "masque général" (la barre de recherche générique).
493
	   ce qui implique des comportement par défaut différents afin de préserver la compatibilité.
494
 
495
	   Théorie:
496
	   1) tags passés par "champ tag":
497
	   - support du ET/OU, et explode par virgule.
498
	   - si pas d'opérande détectée: "OU"
499
 
500
	   2) tags passés par "recherche générale":
501
	   - support du ET/OU, et explode par whitespace.
502
	   - si pas d'opérande détectée: "ET"
503
 
504
	   La présence de $additional_sep s'explique car ET/OU sous-entendent une séparation par des espaces.
505
	   Mais ce n'est pas toujours pertinent car: 1) la compatibilité suggère de considérer parfois
506
	   la virgule comme séparateur et 2) les tags *peuvent* contenir des espaces. Par conséquent:
507
	   * a,b,c => "a" $default_op "b" $default_op "c"
508
	   * a,b AND c => "a" AND "b" AND "c"
509
	   * a OR b AND c,d => "a" AND "b" AND "c" AND "d"
510
	   C'est à dire par ordre décroissant de priorité:
511
	   1) opérande contenu dans la chaîne
512
	   2) opérande par défaut
513
	   3) les séparateurs présents sont substitués par l'opérande déterminée par 1) ou 2)
514
 
515
	   // TODO: support des parenthèses, imbrications & co: "(", ")"
516
	   // http://codehackit.blogspot.fr/2011/08/expression-parser-in-php.html
517
	   // http://blog.angeloff.name/post/2012/08/05/php-recursive-patterns/
518
 
519
	   @param $str: la chaîne à "parser"
520
	   @param $default_op: "AND" ou "OR"
521
	   @param $additional_sep: séparateur de mots:
522
	*/
523
	static function buildTagsAST($str = NULL, $default_op, $additional_sep = ',') {
524
		if(!$str) return;
525
		$words = preg_split('/ (OR|AND|ET|OU) /', $str, -1, PREG_SPLIT_NO_EMPTY);
526
 
527
		if(preg_match('/\b(ET|AND)\b/', $str)) $op = 'AND';
528
		elseif(preg_match('/\b(OU|OR)\b/', $str)) $op = 'OR';
529
		else $op = $default_op;
530
 
531
		if($additional_sep) {
532
			array_walk($words,
533
					   create_function('&$v, $k, $sep', '$v = preg_split("/".$sep."/", $v, -1, PREG_SPLIT_NO_EMPTY);'),
534
					   $additional_sep);
535
		}
536
		$words = self::array_flatten($words);
537
		$words = array_map('trim', $words);
538
		return array($op => array_filter($words));
539
	}
540
 
541
 
1422 raphael 542
	// met à jour *toutes* les stats de nombre de tags et de moyenne des votes
543
	static function _update_statistics($db) {
544
		$db->requeter("TRUNCATE TABLE del_image_stat");
545
		$db->requeter(<<<EOF
546
INSERT INTO `del_image_stat` (
1430 raphael 547
	SELECT id_image, divo.ce_protocole, divo.moyenne, divo.nb_votes, dit.ctags
548
	FROM `tb_cel`.`cel_images` ci
549
	LEFT JOIN
550
	( SELECT ce_image, ce_protocole, AVG(valeur) AS moyenne, COUNT(valeur) AS nb_votes FROM del_image_vote
551
	  GROUP BY ce_image, ce_protocole ) AS divo
552
	ON ci.id_image = divo.ce_image
553
	LEFT JOIN
554
	( SELECT ce_image, COUNT(id_tag) as ctags FROM del_image_tag
555
	  GROUP BY ce_image ) AS dit
556
	ON ci.id_image = dit.ce_image )
1422 raphael 557
EOF
558
		);
559
	}
1430 raphael 560
 
561
	static function revOrderBy($orderby) {
562
		return $orderby == 'asc' ? 'desc' : 'asc';
563
	}
564
 
565
	static function array_flatten($arr) {
566
		$arr = array_values($arr);
567
		while (list($k,$v)=each($arr)) {
568
			if (is_array($v)) {
569
				array_splice($arr,$k,1,$v);
570
				next($arr);
571
			}
572
		}
573
		return $arr;
574
	}
575
}