Subversion Repositories eFlore/Applications.del

Rev

Rev 1433 | 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
 *
1438 raphael 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
 *
1390 raphael 66
 */
67
 
68
require_once(dirname(__FILE__) . '/../observations/ListeObservations2.php');
69
require_once(dirname(__FILE__) . '/../observations/Observation.php');
70
restore_error_handler();
71
restore_exception_handler();
72
error_reporting(E_ALL);
73
 
1422 raphael 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
77
// del/services/0.1/images?navigation.depart=0&navigation.limite=12&tri=votes&ordre=desc&protocole=3&masque=plop
78
 
1390 raphael 79
class ListeImages2 {
80
 
1430 raphael 81
	// TODO: PHP-x.y, ces variables devrait être des "const"
1390 raphael 82
	static $format_image_possible = array('O','CRX2S','CRS','CXS','CS','XS','S','M','L','XL','X2L','X3L');
83
 
1430 raphael 84
	static $tri_possible = array('date_transmission', 'date_observation', 'votes', 'tags');
1390 raphael 85
 
1422 raphael 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
120
			// mais nous afin d'éviter un conflit d'alias nous le renommons plus tard (reformateImagesDoubleIndex)
121
			'i_mots_cles_texte' => 1
122
		));
123
 
124
 
1390 raphael 125
	public function __construct(Conteneur $conteneur = null) {
126
		$this->conteneur = $conteneur == null ? new Conteneur() : $conteneur;
127
		$this->conteneur->chargerConfiguration('config_images.ini');
128
		$this->gestionBdd = $conteneur->getGestionBdd();
129
		$this->bdd = $this->gestionBdd->getBdd();
130
	}
131
 
132
	public function consulter($ressources, $parametres) {
1422 raphael 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. */
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
140
		array_walk(Observation::$mappings['votes'], create_function('&$val, $k', 'if($val==1) $val = $k;'));
1390 raphael 141
 
1422 raphael 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
145
		$req = array('select' => array(), 'join' => array(), 'where' => array(), 'groupby' => array(), 'orderby' => array());
146
 
147
 
1390 raphael 148
		$db = $this->bdd;
149
 
1422 raphael 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
 
1390 raphael 159
		// notre propre filtrage sur l'INPUT
1422 raphael 160
		$params_pf = self::requestFilterParams($parametres,
161
											   array_merge(ListeObservations2::$parametres_autorises,
162
														   self::$parametres_autorises));
1390 raphael 163
 
1430 raphael 164
		/* filtrage des tags + sémantique des valeurs multiples:
165
		   Lorsqu'on utilise masque.tag* pour chercher des tags, ils sont
166
		   postulés comme séparés par des virgule, et l'un au moins des tags doit matcher. */
167
		$params_pf['masque.tag_cel'] = self::buildTagsAST(@$parametres['masque.tag_cel'], 'OR', ',');
168
		$params_pf['masque.tag_pictoflora'] = self::buildTagsAST(@$parametres['masque.tag_pictoflora'], 'OR', ',');
1390 raphael 169
 
1422 raphael 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
1390 raphael 174
 
1422 raphael 175
		// XXX: temp tweak
176
		/* $this->conteneur->setParametre('url_images', sprintf($this->conteneur->getParametre('url_images'),
177
		   "%09d", $params['format']));*/
178
 
179
		// création des contraintes (génériques, de ListeObservations2)
1390 raphael 180
		ListeObservations2::sqlAddConstraint($params, $db, $req, $this->conteneur);
1422 raphael 181
		// création des contraintes spécifiques (sur les tags essentiellement)
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);
185
		// l'ORDER BY s'avére complexe
186
		self::sqlOrderBy($params, $db, $req);
1390 raphael 187
 
188
		// 1) grunt-work: *la* requête de récupération des id valides (+ SQL_CALC_FOUND_ROWS)
1422 raphael 189
		// $idobs_tab = ListeObservations2::getIdObs($params, $req, $db);
190
		$idobs_tab = self::getIdImages($params, $req, $db);
1390 raphael 191
 
1422 raphael 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) {
1433 raphael 194
			$resultat = new ResultatService();
195
			$resultat->corps = array('entete' => ListeObservations2::makeJSONHeader(0, $params, Config::get('url_service')),
196
									 'resultats' => array());
197
			return $resultat;
198
			/*
1422 raphael 199
			header('HTTP/1.0 404 Not Found');
200
			// don't die (phpunit)
1433 raphael 201
			throw(new Exception()); */
1390 raphael 202
		}
203
 
204
 
1422 raphael 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']);
1390 raphael 208
 
1422 raphael 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']);
223
 
224
 
225
		// on charge les votes pour ces images et pour *tous* les protocoles
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
 
1431 raphael 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'])));
1390 raphael 239
		$resultat = new ResultatService();
1431 raphael 240
		$resultat->corps = array('entete' => ListeObservations2::makeJSONHeader($total, $params_header, Config::get('url_service')),
1390 raphael 241
								 'resultats' => $images);
242
		return $resultat;
1422 raphael 243
	}
1390 raphael 244
 
1422 raphael 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)
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
1430 raphael 256
			$req['orderby'] = 'dis.moyenne ' . $p['ordre'] . ', dis.nb_votes ' . $p['ordre'];
1422 raphael 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
 
1430 raphael 265
		if($p['tri'] == 'date_observation') {
266
			$req['orderby'] = 'date_observation ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
267
			return;
268
		}
269
 
1422 raphael 270
		// tri == 'date_transmission'
271
		// avant cel:r1860, date_transmission pouvait être NULL
272
		// or nous voulons de la consistence (notamment pour phpunit)
1430 raphael 273
		$req['orderby'] = 'date_transmission ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
1390 raphael 274
	}
275
 
1422 raphael 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
 
1430 raphael 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'])) {
1422 raphael 347
				// TODO: utiliser les tables de mots clefs normaliées dans tb_cel ?
348
				// et auquel cas laisser au client le choix du couteux "%" ?
1430 raphael 349
				$tags = $p['masque.tag_cel']['AND'];
1422 raphael 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);
1430 raphael 354
				$req['where'][] = '(' . implode(' AND ', $tags) . ')';
1422 raphael 355
			}
1430 raphael 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'])));
1422 raphael 359
			}
360
		}
361
 
362
 
363
		// XXX: utiliser tag plutôt que tag_normalise ?
1430 raphael 364
		if($p['masque.tag_pictoflora']) {
1422 raphael 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'; */
368
 
1430 raphael 369
			if(isset($p['masque.tag_pictoflora']['AND'])) {
1422 raphael 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 "|"
1430 raphael 372
				sort($p['masque.tag_pictoflora']['AND']);
1422 raphael 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)",
1430 raphael 376
										  $db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND'])));
1422 raphael 377
			}
1430 raphael 378
			else {
1422 raphael 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)",
1430 raphael 382
										  $db->proteger(implode('|', $p['masque.tag_pictoflora']['OR'])));
1422 raphael 383
			}
384
		}
385
	}
386
 
1430 raphael 387
 
1422 raphael 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
 
1430 raphael 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'],
1422 raphael 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 */),
1430 raphael 455
				self::requestFilterParams($or_params));
1422 raphael 456
 
1430 raphael 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. */
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'
1422 raphael 464
			$or_req = array('join' => array(), 'where' => array());
465
			ListeObservations2::sqlAddConstraint($or_masque, $db, $or_req);
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
 
1430 raphael 476
	// complete & override ListeObservations2::requestFilterParams() (même usage)
1422 raphael 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);
1390 raphael 484
		$p['format'] = ListeObservations2::unsetIfInvalid($params, 'format', self::$format_image_possible);
1422 raphael 485
 
1430 raphael 486
		// "milieu" inutile pour IdentiPlantes ?
1422 raphael 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);'));
1390 raphael 503
	}
1422 raphael 504
 
505
 
1430 raphael 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" ] ]
1422 raphael 511
 
1430 raphael 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
 
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
 
1422 raphael 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` (
1430 raphael 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
577
	  GROUP BY ce_image ) AS dit
578
	ON ci.id_image = dit.ce_image )
1422 raphael 579
EOF
580
		);
581
	}
1430 raphael 582
 
583
	static function revOrderBy($orderby) {
584
		return $orderby == 'asc' ? 'desc' : 'asc';
585
	}
586
 
587
	static function array_flatten($arr) {
588
		$arr = array_values($arr);
589
		while (list($k,$v)=each($arr)) {
590
			if (is_array($v)) {
591
				array_splice($arr,$k,1,$v);
592
				next($arr);
593
			}
594
		}
595
		return $arr;
596
	}
597
}