Subversion Repositories eFlore/Applications.del

Rev

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