Subversion Repositories eFlore/Applications.del

Rev

Rev 1491 | Rev 1495 | 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
1490 raphael 25
 * utilisées ensuite pour l'ORDER BY. La situation à base de del_image_stat
26
 * est déjà bien meilleure sans être pour autant optimale. cf commentaire de sqlAddConstraint()
1422 raphael 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)
1492 raphael 50
 * - poursuivre la réorganisation des méthodes statiques parmis Observation, ListeObservations et ListeImages2
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
 
1490 raphael 68
require_once(dirname(__FILE__) . '/../DelTk.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
 
1490 raphael 86
	// en plus de ceux dans DelTk
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).
1490 raphael 154
		$params_ip = DelTk::requestFilterParams($parametres,
155
        array_diff(DelTk::$parametres_autorises,
1486 raphael 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,
1490 raphael 161
        array_merge(DelTk::$parametres_autorises,
1486 raphael 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. */
1490 raphael 167
		$params_pf['masque.tag_cel'] = DelTk::buildTagsAST(@$parametres['masque.tag_cel'], 'OR', ',');
168
		$params_pf['masque.tag_pictoflora'] = DelTk::buildTagsAST(@$parametres['masque.tag_pictoflora'], 'OR', ',');
1390 raphael 169
 
1490 raphael 170
		$params = array_merge(
171
            DelTk::$default_params, // paramètre par défaut Identiplante
172
            self::$default_params, // paramètres par défaut PictoFlora
173
            $params_ip, // les paramètres passés, traités par Identiplante
174
            $params_pf); // les paramètres passés, traités par PictoFlora
1390 raphael 175
 
1422 raphael 176
		// XXX: temp tweak
177
		/* $this->conteneur->setParametre('url_images', sprintf($this->conteneur->getParametre('url_images'),
178
		   "%09d", $params['format']));*/
179
 
1490 raphael 180
		// création des contraintes (génériques de DelTk)
181
		DelTk::sqlAddConstraint($params, $db, $req);
1422 raphael 182
		// création des contraintes spécifiques (sur les tags essentiellement)
183
		self::sqlAddConstraint($params, $db, $req, $this->conteneur);
184
		// création des contraintes spécifiques impliquées par le masque général
185
		self::sqlAddMasqueConstraint($params, $db, $req, $this->conteneur);
186
		// l'ORDER BY s'avére complexe
187
		self::sqlOrderBy($params, $db, $req);
1390 raphael 188
 
189
		// 1) grunt-work: *la* requête de récupération des id valides (+ SQL_CALC_FOUND_ROWS)
1451 raphael 190
		// $idobs_tab = ListeObservations::getIdObs($params, $req, $db);
1422 raphael 191
		$idobs_tab = self::getIdImages($params, $req, $db);
1390 raphael 192
 
1422 raphael 193
		// Ce n'est pas la peine de continuer s'il n'y a pas eu de résultats dans la table del_obs_images
194
		if(!$idobs_tab) {
1433 raphael 195
			$resultat = new ResultatService();
1490 raphael 196
			$resultat->corps = array('entete' => DelTk::makeJSONHeader(0, $params, Config::get('url_service')),
1486 raphael 197
            'resultats' => array());
1433 raphael 198
			return $resultat;
199
			/*
1486 raphael 200
              header('HTTP/1.0 404 Not Found');
201
              // don't die (phpunit)
202
              throw(new Exception()); */
1390 raphael 203
		}
204
 
205
 
1422 raphael 206
		// idobs est une liste (toujours ordonnée) des id d'observations recherchées
207
		$idobs = array_values(array_map(create_function('$a', 'return $a["id_image"];'), $idobs_tab));
208
		$total = $db->recuperer('SELECT FOUND_ROWS() AS c'); $total = intval($total['c']);
1390 raphael 209
 
1422 raphael 210
		$liaisons = self::chargerImages($db, $idobs);
211
 
212
		/*
1486 raphael 213
        // Q&D
214
        $images = array();
215
        $o = new Observation($this->conteneur);
216
        foreach($idobs as $i) {
217
        $images[$i] = $o->consulter(array($i), array('justthrow' => 1));
218
        }
1422 raphael 219
		*/
1490 raphael 220
		list($images, $images_keyed_by_id_image) = self::reformateImagesDoubleIndex(
1422 raphael 221
			$liaisons,
222
			$this->conteneur->getParametre('url_images'),
223
			$params['format']);
224
 
225
 
226
		// on charge les votes pour ces images et pour *tous* les protocoles
227
		$votes = Observation::chargerVotesImage($db, $liaisons, NULL);
228
 
229
		// subtilité, nous passons ici le tableau d'images indexé par id_image qui est bien plus pratique pour
230
		// associer les vote à un tableau, puisque nous ne connaissons pas les id d'observation.
231
		// Mais magiquement (par référence), cela va remplir notre tableau indexé par couple d'id (id_image, id_observation)
232
		// cf reformateImagesDoubleIndex() à qui revient la tâche de créer ces deux versions simultanément lorsque
233
		// c'est encore possible.
234
		if($votes) Observation::mapVotesToImages($votes, $images_keyed_by_id_image);
235
 
1431 raphael 236
		// les deux masques de tags sont transformés en AST dans le processus de construction de la requête.
237
		// Reprenous les paramètres originaux non-nettoyés (ils sont valables car le nettoyage est déterministe)
238
		$params_header = array_merge($params, array_filter(array('masque.tag_cel' => @$parametres['masque.tag_cel'],
1486 raphael 239
        'masque.tag_pictoflora' => @$parametres['masque.tag_pictoflora'])));
1390 raphael 240
		$resultat = new ResultatService();
1490 raphael 241
		$resultat->corps = array('entete' => DelTk::makeJSONHeader($total, $params_header, Config::get('url_service')),
1486 raphael 242
        'resultats' => $images);
1390 raphael 243
		return $resultat;
1422 raphael 244
	}
1390 raphael 245
 
1422 raphael 246
	/**
247
	 * TODO: partie spécifique liées à la complexité de PictoFlora:
248
	 * génération de la clause ORDER BY (génère la valeur de la clef orderby' de $req)
249
	 * nécessaire ? tableau sprintf(key (tri) => value (ordre), key => value ...).
250
	 * Cependant il est impensable de joindre sur un AVG() des valeurs des votes pour
251
	 * *chaque* couple (id_image, protocole) de la base afin de trouver les images
252
	 * les "mieux notées", ou bien les images ayant le "plus de tags" (COUNT())
253
	 */
254
	static function sqlOrderBy($p, $db, &$req) {
255
		// parmi self::$tri_possible
256
		if($p['tri'] == 'votes') { // LEFT JOIN sur "dis" ci-dessous
1430 raphael 257
			$req['orderby'] = 'dis.moyenne ' . $p['ordre'] . ', dis.nb_votes ' . $p['ordre'];
1422 raphael 258
			return;
259
		}
260
 
261
		if($p['tri'] == 'tags') { // LEFT JOIN sur "dis" ci-dessous
262
			$req['orderby'] = 'dis.nb_tags ' . $p['ordre'];
263
			return;
264
		}
265
 
1430 raphael 266
		if($p['tri'] == 'date_observation') {
267
			$req['orderby'] = 'date_observation ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
268
			return;
269
		}
270
 
1422 raphael 271
		// tri == 'date_transmission'
272
		// avant cel:r1860, date_transmission pouvait être NULL
273
		// or nous voulons de la consistence (notamment pour phpunit)
1430 raphael 274
		$req['orderby'] = 'date_transmission ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
1390 raphael 275
	}
276
 
1422 raphael 277
	/*
278
	 * in $p: un tableau de paramètres, dont:
279
	 * - 'masque.tag_cel': *tableau* de mots-clefs à chercher parmi cel_image.mots_clefs_texte
280
	 * - 'masque.tag_pictoflora': *tableau* de mots-clefs à chercher parmi del_image_tag.tag_normalise
281
	 * - 'tag_explode_semantic': défini si les éléments sont tous recherchés ou NON
282
	 *
283
	 * in/ou: $req: un tableau de structure de requête MySQL
284
	 *
285
	 * Attention, le fait que nous cherchions masque.tag_cel OU/ET masque.tag_cel
286
	 * ne dépend pas de nous, mais du niveau supérieur de construction de la requête:
287
	 * Soit directement $this->consulter() si des masque.tag* sont passés
288
	 * (split sur ",", "AND" entre chaque condition, "OR" pour chaque valeur de tag)
289
	 * Soit via sqlAddMasqueConstraint():
290
	 * (pas de split, "OR" entre chaque condition) [ comportement historique ]
291
	 * équivalent à:
292
	 * (split sur " ", "OR" entre chaque condition, "AND" pour chaque valeur de tag)
293
	 *
294
	 */
295
	static function sqlAddConstraint($p, $db, &$req, Conteneur $c = NULL) {
1490 raphael 296
		// TODO implement dans DelTk ?
1422 raphael 297
		if(!empty($p['masque.milieu'])) {
298
			$req['where'][] = 'vdi.milieu LIKE '.$db->proteger('%' . $p['masque.milieu'].'%');
299
		}
300
 
301
 
302
		/* Pour le tri par AVG() des votes nous avons toujours un protocole donné,
303
		   celui-ci indique sur quels votes porte l'AVG.
304
		   (c'est un *vote* qui porte sur un protocole et non l'image elle-même) */
305
		/* TODO: perf problème:
306
		   1) SQL_CALC_FOUND_ROWS: fixable en:
1486 raphael 307
           - dissociant le comptage de la récup d'id + javascript async
308
           - ou ne rafraîchir le total *que* pour les requête impliquant un changement de pagination
309
           (paramètre booléen "with-total" par exemple)
1422 raphael 310
		   2) jointure forcées: en utilisant `del_imagè`, nous forçons les 2 premiers
1486 raphael 311
           JOIN sur cel_obs_images et cel_obs pour filtrer sur "transmission".
312
           Dénormaliser cette valeur et l'intégrer à `cel_images` ferait économiser cette couteuse
313
           jointure, ... lorsqu'aucun masque portant sur `cel_obs` n'est utilisé
1422 raphael 314
		   3) non-problème: l'ordre des joins est forcé par l'usage de la vue:
1486 raphael 315
           (cel_images/cel_obs_images/cel_obs/del_image_stat)
316
           Cependant c'est à l'optimiseur de définir son ordre préféré. */
1422 raphael 317
		if($p['tri'] == 'votes') {
318
			// $p['protocole'] *est* défini (cf requestFilterParams())
319
			// petite optimisation: INNER JOIN si ordre DESC car les 0 à la fin
320
			if($p['ordre'] == 'desc') {
321
				// pas de group by nécessaire pour cette jointure
322
				// PRIMARY KEY (`ce_image`, `ce_protocole`)
323
				$req['join'][] = sprintf('INNER JOIN del_image_stat dis'.
1486 raphael 324
                ' ON vdi.id_image = dis.ce_image'.
325
                ' AND dis.ce_protocole = %d',
326
                $p['protocole']);
1422 raphael 327
			} else {
328
				$req['join'][] = sprintf('LEFT JOIN del_image_stat dis'.
1486 raphael 329
                ' ON vdi.id_image = dis.ce_image'.
330
                ' AND dis.ce_protocole = %d',
331
                $p['protocole']);
1422 raphael 332
				// nécessaire (dup ce_image dans del_image_stat)
333
				$req['groupby'][] = 'vdi.id_observation';
334
			}
335
		}
336
 
337
		if($p['tri'] == 'tags') {
338
			$req['join'][] = sprintf('%s JOIN del_image_stat dis ON vdi.id_image = dis.ce_image',
1486 raphael 339
            ($p['ordre'] == 'desc') ? 'INNER' : 'LEFT');
1422 raphael 340
			// nécessaire (dup ce_image dans del_image_stat)
341
			$req['groupby'][] = 'vdi.id_observation';
342
		}
343
 
1430 raphael 344
		// car il ne sont pas traités par la générique requestFilterParams() les clefs "masque.tag_*"
345
		// sont toujours présentes; bien que parfois NULL.
346
		if($p['masque.tag_cel']) {
347
			if(isset($p['masque.tag_cel']['AND'])) {
1422 raphael 348
				// TODO: utiliser les tables de mots clefs normaliées dans tb_cel ?
349
				// et auquel cas laisser au client le choix du couteux "%" ?
1430 raphael 350
				$tags = $p['masque.tag_cel']['AND'];
1422 raphael 351
				array_walk($tags, create_function('&$val, $k, $db',
1486 raphael 352
                '$val = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) LIKE %s",
353
																  $db->proteger("%".$val."%"));'),
354
                $db);
1430 raphael 355
				$req['where'][] = '(' . implode(' AND ', $tags) . ')';
1422 raphael 356
			}
1430 raphael 357
			else {
358
				$req['where'][] = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) REGEXP %s",
1486 raphael 359
                $db->proteger(implode('|', $p['masque.tag_cel']['OR'])));
1422 raphael 360
			}
361
		}
362
 
363
 
364
		// XXX: utiliser tag plutôt que tag_normalise ?
1430 raphael 365
		if($p['masque.tag_pictoflora']) {
1422 raphael 366
			// pas de LEFT JOIN ? ou bien peut-être en cas de tri, mais nous parlons bien ici d'un masque
367
			/* $req['join'][] = 'LEFT JOIN del_image_tag dit ON dit.ce_image = vdi.id_image';
368
			   $req['where'][] = 'dit.actif = 1'; */
369
 
1486 raphael 370
 
1491 raphael 371
            // Note à propos des 4 "@ instruction" ci-dessous (notamment sur recupererTous())
372
            // REGEXP permet un puissant mécanisme de sélection des obs/image à qui sait
373
            // l'utiliser, mais peut sortir une erreur en cas de REGEXP invalide
374
            // ex: REGEX "^(".
375
            // Pour l'heure nous ignorons ce type d'erreur car aucun de nos champ de recherche
376
            // ne peuvent (ou ne devrait) comporter des meta-caractères
377
            // ([])?*+\\
1486 raphael 378
 
1491 raphael 379
 
1486 raphael 380
			// ==== commenté pour l'instant pour cause de soucis d'optimiseur MySQL (cf commentaire en intro) ====
381
			/*
382
              if(isset($p['masque.tag_pictoflora']['AND'])) {
383
              // optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
384
              // donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
385
              sort($p['masque.tag_pictoflora']['AND']);
386
              $req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
387
              " GROUP BY ce_image".
388
              " HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s)",
389
              $db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND'])));
390
              }
391
              else {
392
              $req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
393
              " GROUP BY ce_image".
394
              " HAVING GROUP_CONCAT(tag_normalise) REGEXP %s)",
395
              $db->proteger(implode('|', $p['masque.tag_pictoflora']['OR'])));
396
              }
397
			*/
398
 
399
			// ==== XXX: puisque on est bassiné par cette "DEPENDENT SUBQUERY", nous la faisons donc indépendemment ====
1430 raphael 400
			if(isset($p['masque.tag_pictoflora']['AND'])) {
1422 raphael 401
				// optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
402
				// donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
1430 raphael 403
				sort($p['masque.tag_pictoflora']['AND']);
1486 raphael 404
 
405
				// plutôt que db->connexion->query->fetchColumn(), une API pourrie nous oblige à ...
1491 raphael 406
				$ids = @$db->recupererTous(sprintf(
1486 raphael 407
					"SELECT ce_image FROM del_image_tag WHERE actif = 1".
408
					" GROUP BY ce_image".
409
					" HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s",
410
					$db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND']))));
1491 raphael 411
 
1486 raphael 412
				// puis:
1491 raphael 413
				$ids = @array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
1486 raphael 414
				if($ids) $req['where'][] = sprintf("vdi.id_image IN (%s)", implode(',', $ids));
415
 
1422 raphael 416
			}
1430 raphael 417
			else {
1491 raphael 418
				$ids = @$db->recupererTous(sprintf(
1486 raphael 419
					"SELECT ce_image FROM del_image_tag WHERE actif = 1".
420
					" GROUP BY ce_image".
421
					" HAVING GROUP_CONCAT(tag_normalise) REGEXP %s",
422
					$db->proteger(implode('|', $p['masque.tag_pictoflora']['OR']))));
1491 raphael 423
 
424
				$ids = @array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
1486 raphael 425
				if($ids) $req['where'][] = sprintf("vdi.id_image IN (%s)", implode(',', $ids));
1422 raphael 426
			}
1486 raphael 427
 
1422 raphael 428
		}
429
	}
430
 
1430 raphael 431
 
1422 raphael 432
	static function getIdImages($p, $req, $db) {
433
		return $db->recupererTous(sprintf(
434
			'SELECT SQL_CALC_FOUND_ROWS id_image' .
435
			' FROM v_del_image vdi'.
436
			' %s' . // LEFT JOIN if any
437
			' WHERE %s'. // where-clause ou TRUE
438
			' %s'. // group-by
439
			' ORDER BY %s'.
440
			' LIMIT %d, %d -- %s',
441
 
442
			$req['join'] ? implode(' ', array_unique($req['join'])) : '',
443
			$req['where'] ? implode(' AND ', $req['where']) : 'TRUE',
444
 
445
			$req['groupby'] ? ('GROUP BY ' . implode(', ', array_unique($req['groupby']))) : '',
446
 
447
			$req['orderby'],
448
 
449
			$p['navigation.depart'], $p['navigation.limite'], __FILE__ . ':' . __LINE__));
450
 
451
	}
452
 
453
	static function chargerImages($db, $idImg) {
1490 raphael 454
		$obs_fields = DelTk::sqlFieldsToAlias(self::$mappings['observations'], NULL);
455
		$image_fields = DelTk::sqlFieldsToAlias(self::$mappings['images'], NULL);
1422 raphael 456
 
457
		return $db->recupererTous(sprintf('SELECT '.
1486 raphael 458
        ' CONCAT(id_image, "-", id_observation) AS jsonindex,'.
459
        ' %1$s, %2$s FROM v_del_image '.
460
        ' WHERE %3$s'.
461
        ' -- %4$s',
462
        $obs_fields, $image_fields,
463
        sprintf('id_image IN (%s)', implode(',', $idImg)),
464
        __FILE__ . ':' . __LINE__));
1422 raphael 465
 
466
	}
467
 
468
	/* "masque" ne fait jamais que faire une requête sur la plupart des champs, (presque) tous traités
469
	   de manière identique à la seule différence que:
470
	   1) ils sont combinés par des "OU" logiques plutôt que des "ET".
471
	   2) les tags sont traités différemment pour conserver la compatibilité avec l'utilisation historique:
1486 raphael 472
       Tous les mots-clefs doivent matcher et sont séparés par des espaces
473
       (dit autrement, str_replace(" ", ".*", $mots-clefs-requête) =~ $mots-clefs-mysql)
1492 raphael 474
	   Pour plus d'information: (ListeObservations|DelTk)::sqlAddMasqueConstraint() */
1422 raphael 475
	static function sqlAddMasqueConstraint($p, $db, &$req, Conteneur $c = NULL) {
476
		if(!empty($p['masque'])) {
477
			$or_params = array('masque.auteur' => $p['masque'],
1486 raphael 478
            'masque.departement' => $p['masque'],
479
            'masque.commune' => $p['masque'], // TODO/XXX ?
480
            'masque.id_zone_geo' => $p['masque'],
1422 raphael 481
 
1486 raphael 482
            /* tous-deux remplacent masque.tag
483
               mais sont traité séparément des requestFilterParams() */
484
            // 'masque.tag_cel' => $p['masque'],
485
            // 'masque.tag_pictoflora' => $p['masque'],
1422 raphael 486
 
1486 raphael 487
            'masque.ns' => $p['masque'],
488
            'masque.famille' => $p['masque'],
489
            'masque.date' => $p['masque'],
490
            'masque.genre' => $p['masque'],
491
            'masque.milieu' => $p['masque'],
1422 raphael 492
 
1486 raphael 493
            // tri est aussi nécessaire car affecte les contraintes de JOIN
494
            'tri' => $p['tri'],
495
            'ordre' => $p['ordre']);
1422 raphael 496
 
497
			$or_masque = array_merge(
1490 raphael 498
				DelTk::requestFilterParams($or_params, NULL, $c /* pour masque.departement */),
1430 raphael 499
				self::requestFilterParams($or_params));
1422 raphael 500
 
1430 raphael 501
			/* Lorsqu'on utilise le masque général pour chercher des tags, ils sont
502
			   postulés comme séparés par des espaces, et doivent être tous matchés. */
1490 raphael 503
			$or_masque['masque.tag_cel'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
504
			$or_masque['masque.tag_pictoflora'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
1430 raphael 505
 
506
 
507
			// pas de select, groupby & co ici: uniquement 'join' et 'where'
1422 raphael 508
			$or_req = array('join' => array(), 'where' => array());
1490 raphael 509
			DelTk::sqlAddConstraint($or_masque, $db, $or_req);
1422 raphael 510
			self::sqlAddConstraint($or_masque, $db, $or_req);
511
 
512
			if($or_req['where']) {
513
				$req['where'][] = '(' . implode(' OR ', $or_req['where']) . ')';
514
				// utile au cas ou des jointures seraient rajoutées
515
				$req['join'] = array_unique(array_merge($req['join'], $or_req['join']));
516
			}
517
		}
518
	}
519
 
1490 raphael 520
 
521
	// cf Observation::reformateObservationSimpleIndex() et ListeObservations::reformateObservation()
522
    // (trop de variétés de formatage, à unifier côté client pour unifier côté backend ...)
523
	static function reformateImagesDoubleIndex($obs, $url_pattern = '', $image_format = 'XL') {
524
		// XXX: cf Observation.php::consulter(), nous pourriouns ici
525
		// conserver les valeurs vides (pour les phptests notamment, ou non)
526
		// $obs = array_map('array_filter', $obs);
527
		$obs_merged = $obs_keyed_by_id_image = array();
528
		foreach($obs as $o) {
529
			// ceci nous complique la tâche pour le reste du processing...
530
			$id = $o['jsonindex'];
531
			// ainsi nous utilisons deux tableaux: le final, indexé par couple d'id(image-obs)
532
			// et celui indexé par simple id_image qui est fort utile pour mapVotesToImages()
533
			// mais tout deux partage leur référence à "protocole"
534
			$image = array(
535
				'id_image' => $o['id_image'],
536
				'binaire.href' => sprintf($url_pattern, $o['id_image'], $image_format),
537
				'mots_cles_texte' => @$o['i_mots_cles_texte'], // @, peut avoir été filtré par array_map() ci-dessus
538
			);
539
			unset($o['id_image'], $o['i_mots_cles_texte'], $o['jsonindex']);
540
			if(!isset($obs_merged[$id])) $obs_merged[$id] = $image;
541
			$obs_merged[$id]['observation'] = $o;
542
			$obs_merged[$id]['protocoles_votes'] = array();
543
 
544
			$obs_keyed_by_id_image[$image['id_image']]['protocoles_votes'] = &$obs_merged[$id]['protocoles_votes'];
545
		}
546
 
547
		return array($obs_merged,$obs_keyed_by_id_image);
548
	}
549
 
550
 
551
 
552
	// complete & override DelTk::requestFilterParams() (même usage)
1422 raphael 553
	static function requestFilterParams(Array $params, $parametres_autorises = NULL) {
554
		if($parametres_autorises) { // filtrage de toute clef inconnue
555
			$params = array_intersect_key($params, array_flip($parametres_autorises));
556
		}
557
 
558
		$p = array();
1490 raphael 559
		$p['tri'] = DelTk::unsetIfInvalid($params, 'tri', self::$tri_possible);
560
		$p['format'] = DelTk::unsetIfInvalid($params, 'format', self::$format_image_possible);
1422 raphael 561
 
1430 raphael 562
		// "milieu" inutile pour IdentiPlantes ?
1422 raphael 563
		if(isset($params['masque.milieu'])) $p['masque.milieu'] = trim($params['masque.milieu']);
564
 
565
		// compatibilité
566
		if(isset($params['masque.tag'])) {
567
			$params['masque.tag_cel'] = $params['masque.tag_pictoflora'] = $params['masque.tag'];
568
		}
569
 
570
		if($p['tri'] == 'votes' || $p['tri'] == 'tags') {
571
			// ces critère de tri des image à privilégier ne s'applique qu'à un protocole donné
572
			if(!isset($params['protocole']) || !is_numeric($params['protocole']))
573
				$p['protocole'] = self::$default_proto;
574
			else
575
				$p['protocole'] = intval($params['protocole']);
576
		}
577
 
578
		return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
1390 raphael 579
	}
1422 raphael 580
 
581
 
582
 
583
	// met à jour *toutes* les stats de nombre de tags et de moyenne des votes
584
	static function _update_statistics($db) {
585
		$db->requeter("TRUNCATE TABLE del_image_stat");
586
		$db->requeter(<<<EOF
587
INSERT INTO `del_image_stat` (
1430 raphael 588
	SELECT id_image, divo.ce_protocole, divo.moyenne, divo.nb_votes, dit.ctags
589
	FROM `tb_cel`.`cel_images` ci
590
	LEFT JOIN
591
	( SELECT ce_image, ce_protocole, AVG(valeur) AS moyenne, COUNT(valeur) AS nb_votes FROM del_image_vote
592
	  GROUP BY ce_image, ce_protocole ) AS divo
593
	ON ci.id_image = divo.ce_image
594
	LEFT JOIN
595
	( SELECT ce_image, COUNT(id_tag) as ctags FROM del_image_tag
596
	  GROUP BY ce_image ) AS dit
597
	ON ci.id_image = dit.ce_image )
1422 raphael 598
EOF
599
		);
600
	}
1430 raphael 601
 
602
	static function revOrderBy($orderby) {
603
		return $orderby == 'asc' ? 'desc' : 'asc';
604
	}
1486 raphael 605
}