Subversion Repositories eFlore/Applications.del

Rev

Rev 1451 | 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)
1451 raphael 50
 * - réorganiser les méthodes statiques parmis Observation, ListeObservations et ListImages2
1422 raphael 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);
1486 raphael 56
 *	MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery
1438 raphael 57
 * EXPLAIN SELECT * FROM del_image WHERE id_image IN (SELECT 3);
1486 raphael 58
 *	PRIMARY
1438 raphael 59
 * EXPLAIN SELECT * FROM del_image WHERE id_image IN (SELECT MIN(3));
1486 raphael 60
 *	DEPENDENT SUBQUERY ... ... ... mwarf !
1438 raphael 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);
1486 raphael 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
1438 raphael 64
 * FORCE INDEX/IGNORE INDEX semble incapable de résoudre le problème de l'optimiseur MySQL
65
 *
1390 raphael 66
 */
67
 
1451 raphael 68
require_once(dirname(__FILE__) . '/../observations/ListeObservations.php');
1390 raphael 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
 
1451 raphael 86
	// en plus de ceux dans ListeObservations
1422 raphael 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,
1486 raphael 90
    'tri' => 'date_transmission', 'ordre' => 'desc',
91
    // spécifiques à PictoFlora:
92
    'format' => 'XL');
1422 raphael 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).
1451 raphael 154
		$params_ip = ListeObservations::requestFilterParams($parametres,
1486 raphael 155
        array_diff(ListeObservations::$parametres_autorises,
156
        array('masque.type')),
157
        $this->conteneur);
1422 raphael 158
 
1390 raphael 159
		// notre propre filtrage sur l'INPUT
1422 raphael 160
		$params_pf = self::requestFilterParams($parametres,
1486 raphael 161
        array_merge(ListeObservations::$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
 
1451 raphael 170
		$params = array_merge(ListeObservations::$default_params, // paramètre par défaut Identiplante
1486 raphael 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
 
1451 raphael 179
		// création des contraintes (génériques, de ListeObservations)
180
		ListeObservations::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)
1451 raphael 189
		// $idobs_tab = ListeObservations::getIdObs($params, $req, $db);
1422 raphael 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();
1451 raphael 195
			$resultat->corps = array('entete' => ListeObservations::makeJSONHeader(0, $params, Config::get('url_service')),
1486 raphael 196
            'resultats' => array());
1433 raphael 197
			return $resultat;
198
			/*
1486 raphael 199
              header('HTTP/1.0 404 Not Found');
200
              // don't die (phpunit)
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
		/*
1486 raphael 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
        }
1422 raphael 218
		*/
1451 raphael 219
		list($images, $images_keyed_by_id_image) = ListeObservations::reformateImagesDoubleIndex(
1422 raphael 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'],
1486 raphael 238
        'masque.tag_pictoflora' => @$parametres['masque.tag_pictoflora'])));
1390 raphael 239
		$resultat = new ResultatService();
1451 raphael 240
		$resultat->corps = array('entete' => ListeObservations::makeJSONHeader($total, $params_header, Config::get('url_service')),
1486 raphael 241
        'resultats' => $images);
1390 raphael 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) {
1451 raphael 295
		// TODO implement dans ListeObservations ?
1422 raphael 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:
1486 raphael 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)
1422 raphael 309
		   2) jointure forcées: en utilisant `del_imagè`, nous forçons les 2 premiers
1486 raphael 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é
1422 raphael 313
		   3) non-problème: l'ordre des joins est forcé par l'usage de la vue:
1486 raphael 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é. */
1422 raphael 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'.
1486 raphael 323
                ' ON vdi.id_image = dis.ce_image'.
324
                ' AND dis.ce_protocole = %d',
325
                $p['protocole']);
1422 raphael 326
			} else {
327
				$req['join'][] = sprintf('LEFT JOIN del_image_stat dis'.
1486 raphael 328
                ' ON vdi.id_image = dis.ce_image'.
329
                ' AND dis.ce_protocole = %d',
330
                $p['protocole']);
1422 raphael 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',
1486 raphael 338
            ($p['ordre'] == 'desc') ? 'INNER' : 'LEFT');
1422 raphael 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',
1486 raphael 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",
1486 raphael 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
 
1486 raphael 369
 
370
 
371
			// ==== commenté pour l'instant pour cause de soucis d'optimiseur MySQL (cf commentaire en intro) ====
372
			/*
373
              if(isset($p['masque.tag_pictoflora']['AND'])) {
374
              // optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
375
              // donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
376
              sort($p['masque.tag_pictoflora']['AND']);
377
              $req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
378
              " GROUP BY ce_image".
379
              " HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s)",
380
              $db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND'])));
381
              }
382
              else {
383
              $req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
384
              " GROUP BY ce_image".
385
              " HAVING GROUP_CONCAT(tag_normalise) REGEXP %s)",
386
              $db->proteger(implode('|', $p['masque.tag_pictoflora']['OR'])));
387
              }
388
			*/
389
 
390
			// ==== XXX: puisque on est bassiné par cette "DEPENDENT SUBQUERY", nous la faisons donc indépendemment ====
1430 raphael 391
			if(isset($p['masque.tag_pictoflora']['AND'])) {
1422 raphael 392
				// optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
393
				// donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
1430 raphael 394
				sort($p['masque.tag_pictoflora']['AND']);
1486 raphael 395
 
396
				// plutôt que db->connexion->query->fetchColumn(), une API pourrie nous oblige à ...
397
				$ids = $db->recupererTous(sprintf(
398
					"SELECT ce_image FROM del_image_tag WHERE actif = 1".
399
					" GROUP BY ce_image".
400
					" HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s",
401
					$db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND']))));
402
				// puis:
403
				$ids = array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
404
				if($ids) $req['where'][] = sprintf("vdi.id_image IN (%s)", implode(',', $ids));
405
 
1422 raphael 406
			}
1430 raphael 407
			else {
1486 raphael 408
				$ids = $db->recupererTous(sprintf(
409
					"SELECT ce_image FROM del_image_tag WHERE actif = 1".
410
					" GROUP BY ce_image".
411
					" HAVING GROUP_CONCAT(tag_normalise) REGEXP %s",
412
					$db->proteger(implode('|', $p['masque.tag_pictoflora']['OR']))));
413
				$ids = array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
414
				if($ids) $req['where'][] = sprintf("vdi.id_image IN (%s)", implode(',', $ids));
1422 raphael 415
			}
1486 raphael 416
 
1422 raphael 417
		}
418
	}
419
 
1430 raphael 420
 
1422 raphael 421
	static function getIdImages($p, $req, $db) {
422
		return $db->recupererTous(sprintf(
423
			'SELECT SQL_CALC_FOUND_ROWS id_image' .
424
			' FROM v_del_image vdi'.
425
			' %s' . // LEFT JOIN if any
426
			' WHERE %s'. // where-clause ou TRUE
427
			' %s'. // group-by
428
			' ORDER BY %s'.
429
			' LIMIT %d, %d -- %s',
430
 
431
			$req['join'] ? implode(' ', array_unique($req['join'])) : '',
432
			$req['where'] ? implode(' AND ', $req['where']) : 'TRUE',
433
 
434
			$req['groupby'] ? ('GROUP BY ' . implode(', ', array_unique($req['groupby']))) : '',
435
 
436
			$req['orderby'],
437
 
438
			$p['navigation.depart'], $p['navigation.limite'], __FILE__ . ':' . __LINE__));
439
 
440
	}
441
 
442
	static function chargerImages($db, $idImg) {
443
		$obs_fields = Observation::sqlFieldsToAlias(self::$mappings['observations'], NULL);
444
		$image_fields = Observation::sqlFieldsToAlias(self::$mappings['images'], NULL);
445
 
446
		return $db->recupererTous(sprintf('SELECT '.
1486 raphael 447
        ' CONCAT(id_image, "-", id_observation) AS jsonindex,'.
448
        ' %1$s, %2$s FROM v_del_image '.
449
        ' WHERE %3$s'.
450
        ' -- %4$s',
451
        $obs_fields, $image_fields,
452
        sprintf('id_image IN (%s)', implode(',', $idImg)),
453
        __FILE__ . ':' . __LINE__));
1422 raphael 454
 
455
	}
456
 
457
	/* "masque" ne fait jamais que faire une requête sur la plupart des champs, (presque) tous traités
458
	   de manière identique à la seule différence que:
459
	   1) ils sont combinés par des "OU" logiques plutôt que des "ET".
460
	   2) les tags sont traités différemment pour conserver la compatibilité avec l'utilisation historique:
1486 raphael 461
       Tous les mots-clefs doivent matcher et sont séparés par des espaces
462
       (dit autrement, str_replace(" ", ".*", $mots-clefs-requête) =~ $mots-clefs-mysql)
1451 raphael 463
	   Pour plus d'information: ListeObservations::sqlAddMasqueConstraint() */
1422 raphael 464
	static function sqlAddMasqueConstraint($p, $db, &$req, Conteneur $c = NULL) {
465
		if(!empty($p['masque'])) {
466
			$or_params = array('masque.auteur' => $p['masque'],
1486 raphael 467
            'masque.departement' => $p['masque'],
468
            'masque.commune' => $p['masque'], // TODO/XXX ?
469
            'masque.id_zone_geo' => $p['masque'],
1422 raphael 470
 
1486 raphael 471
            /* tous-deux remplacent masque.tag
472
               mais sont traité séparément des requestFilterParams() */
473
            // 'masque.tag_cel' => $p['masque'],
474
            // 'masque.tag_pictoflora' => $p['masque'],
1422 raphael 475
 
1486 raphael 476
            'masque.ns' => $p['masque'],
477
            'masque.famille' => $p['masque'],
478
            'masque.date' => $p['masque'],
479
            'masque.genre' => $p['masque'],
480
            'masque.milieu' => $p['masque'],
1422 raphael 481
 
1486 raphael 482
            // tri est aussi nécessaire car affecte les contraintes de JOIN
483
            'tri' => $p['tri'],
484
            'ordre' => $p['ordre']);
1422 raphael 485
 
486
			$or_masque = array_merge(
1451 raphael 487
				ListeObservations::requestFilterParams($or_params, NULL, $c /* pour masque.departement */),
1430 raphael 488
				self::requestFilterParams($or_params));
1422 raphael 489
 
1430 raphael 490
			/* Lorsqu'on utilise le masque général pour chercher des tags, ils sont
491
			   postulés comme séparés par des espaces, et doivent être tous matchés. */
492
			$or_masque['masque.tag_cel'] = self::buildTagsAST($p['masque'], 'AND', ' ');
493
			$or_masque['masque.tag_pictoflora'] = self::buildTagsAST($p['masque'], 'AND', ' ');
494
 
495
 
496
			// pas de select, groupby & co ici: uniquement 'join' et 'where'
1422 raphael 497
			$or_req = array('join' => array(), 'where' => array());
1451 raphael 498
			ListeObservations::sqlAddConstraint($or_masque, $db, $or_req);
1422 raphael 499
			self::sqlAddConstraint($or_masque, $db, $or_req);
500
 
501
			if($or_req['where']) {
502
				$req['where'][] = '(' . implode(' OR ', $or_req['where']) . ')';
503
				// utile au cas ou des jointures seraient rajoutées
504
				$req['join'] = array_unique(array_merge($req['join'], $or_req['join']));
505
			}
506
		}
507
	}
508
 
1451 raphael 509
	// complete & override ListeObservations::requestFilterParams() (même usage)
1422 raphael 510
	static function requestFilterParams(Array $params, $parametres_autorises = NULL) {
511
		if($parametres_autorises) { // filtrage de toute clef inconnue
512
			$params = array_intersect_key($params, array_flip($parametres_autorises));
513
		}
514
 
515
		$p = array();
1451 raphael 516
		$p['tri'] = ListeObservations::unsetIfInvalid($params, 'tri', self::$tri_possible);
517
		$p['format'] = ListeObservations::unsetIfInvalid($params, 'format', self::$format_image_possible);
1422 raphael 518
 
1430 raphael 519
		// "milieu" inutile pour IdentiPlantes ?
1422 raphael 520
		if(isset($params['masque.milieu'])) $p['masque.milieu'] = trim($params['masque.milieu']);
521
 
522
		// compatibilité
523
		if(isset($params['masque.tag'])) {
524
			$params['masque.tag_cel'] = $params['masque.tag_pictoflora'] = $params['masque.tag'];
525
		}
526
 
527
		if($p['tri'] == 'votes' || $p['tri'] == 'tags') {
528
			// ces critère de tri des image à privilégier ne s'applique qu'à un protocole donné
529
			if(!isset($params['protocole']) || !is_numeric($params['protocole']))
530
				$p['protocole'] = self::$default_proto;
531
			else
532
				$p['protocole'] = intval($params['protocole']);
533
		}
534
 
535
		return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
1390 raphael 536
	}
1422 raphael 537
 
538
 
1430 raphael 539
	/* Construit un (vulgaire) abstract syntax tree:
540
	   "AND" => [ "tag1", "tag2" ]
541
	   Idéalement (avec un parser simple comme proposé par http://hoa-project.net/Literature/Hack/Compiler.html#Langage_PP)
542
	   nous aurions:
543
	   "AND" => [ "tag1", "tag2", "OR" => [ "tag3", "tag4" ] ]
1422 raphael 544
 
1430 raphael 545
	   Ici nous devons traiter les cas suivants:
546
	   tags séparés par des "ET/AND OU/OR", séparés par des espaces ou des virgules.
547
	   Mais la chaîne peut aussi avoir été issue du "masque général" (la barre de recherche générique).
548
	   ce qui implique des comportement par défaut différents afin de préserver la compatibilité.
549
 
550
	   Théorie:
551
	   1) tags passés par "champ tag":
552
	   - support du ET/OU, et explode par virgule.
553
	   - si pas d'opérande détectée: "OU"
554
 
555
	   2) tags passés par "recherche générale":
556
	   - support du ET/OU, et explode par whitespace.
557
	   - si pas d'opérande détectée: "ET"
558
 
559
	   La présence de $additional_sep s'explique car ET/OU sous-entendent une séparation par des espaces.
560
	   Mais ce n'est pas toujours pertinent car: 1) la compatibilité suggère de considérer parfois
561
	   la virgule comme séparateur et 2) les tags *peuvent* contenir des espaces. Par conséquent:
562
	   * a,b,c => "a" $default_op "b" $default_op "c"
563
	   * a,b AND c => "a" AND "b" AND "c"
564
	   * a OR b AND c,d => "a" AND "b" AND "c" AND "d"
565
	   C'est à dire par ordre décroissant de priorité:
566
	   1) opérande contenu dans la chaîne
567
	   2) opérande par défaut
568
	   3) les séparateurs présents sont substitués par l'opérande déterminée par 1) ou 2)
569
 
570
	   // TODO: support des parenthèses, imbrications & co: "(", ")"
571
	   // http://codehackit.blogspot.fr/2011/08/expression-parser-in-php.html
572
	   // http://blog.angeloff.name/post/2012/08/05/php-recursive-patterns/
573
 
574
	   @param $str: la chaîne à "parser"
575
	   @param $default_op: "AND" ou "OR"
576
	   @param $additional_sep: séparateur de mots:
577
	*/
578
	static function buildTagsAST($str = NULL, $default_op, $additional_sep = ',') {
579
		if(!$str) return;
580
		$words = preg_split('/ (OR|AND|ET|OU) /', $str, -1, PREG_SPLIT_NO_EMPTY);
581
 
582
		if(preg_match('/\b(ET|AND)\b/', $str)) $op = 'AND';
583
		elseif(preg_match('/\b(OU|OR)\b/', $str)) $op = 'OR';
584
		else $op = $default_op;
585
 
586
		if($additional_sep) {
587
			array_walk($words,
1486 raphael 588
            create_function('&$v, $k, $sep', '$v = preg_split("/".$sep."/", $v, -1, PREG_SPLIT_NO_EMPTY);'),
589
            $additional_sep);
1430 raphael 590
		}
591
		$words = self::array_flatten($words);
592
		$words = array_map('trim', $words);
593
		return array($op => array_filter($words));
594
	}
595
 
596
 
1422 raphael 597
	// met à jour *toutes* les stats de nombre de tags et de moyenne des votes
598
	static function _update_statistics($db) {
599
		$db->requeter("TRUNCATE TABLE del_image_stat");
600
		$db->requeter(<<<EOF
601
INSERT INTO `del_image_stat` (
1430 raphael 602
	SELECT id_image, divo.ce_protocole, divo.moyenne, divo.nb_votes, dit.ctags
603
	FROM `tb_cel`.`cel_images` ci
604
	LEFT JOIN
605
	( SELECT ce_image, ce_protocole, AVG(valeur) AS moyenne, COUNT(valeur) AS nb_votes FROM del_image_vote
606
	  GROUP BY ce_image, ce_protocole ) AS divo
607
	ON ci.id_image = divo.ce_image
608
	LEFT JOIN
609
	( SELECT ce_image, COUNT(id_tag) as ctags FROM del_image_tag
610
	  GROUP BY ce_image ) AS dit
611
	ON ci.id_image = dit.ce_image )
1422 raphael 612
EOF
613
		);
614
	}
1430 raphael 615
 
616
	static function revOrderBy($orderby) {
617
		return $orderby == 'asc' ? 'desc' : 'asc';
618
	}
619
 
620
	static function array_flatten($arr) {
621
		$arr = array_values($arr);
622
		while (list($k,$v)=each($arr)) {
623
			if (is_array($v)) {
624
				array_splice($arr,$k,1,$v);
625
				next($arr);
626
			}
627
		}
628
		return $arr;
629
	}
1486 raphael 630
}