Subversion Repositories eFlore/Applications.del

Rev

Rev 1845 | Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1840 jpm 1
<?php
2
// declare(encoding='UTF-8');
3
/**
4
 * Classe contenant des méthodes permettant de construire les requêtes SQL complexe concernant les images et obs.
5
 *
6
 * @category  DEL
7
 * @package   Services
8
 * @package   Bibliotheque
9
 * @version   0.1
10
 * @author    Mathias CHOUET <mathias@tela-botanica.org>
11
 * @author    Jean-Pascal MILCENT <jpm@tela-botanica.org>
12
 * @author    Aurelien PERONNET <aurelien@tela-botanica.org>
13
 * @license   GPL v3 <http://www.gnu.org/licenses/gpl.txt>
14
 * @license   CECILL v2 <http://www.cecill.info/licences/Licence_CeCILL_V2-en.txt>
15
 * @copyright 1999-2014 Tela Botanica <accueil@tela-botanica.org>
16
 */
17
class Sql {
18
 
19
	private $conteneur;
20
	private $bdd;
21
	private $parametres = array();
22
	private $requete = array(
23
		'select' => array(),
24
		'join' => array(),
25
		'where' => array(),
26
		'groupby' => array(),
27
		'orderby' => array());
28
 
29
	private $champsPrenom = array('du.prenom', 'vdi.prenom_utilisateur');
30
	private $champsNom = array('du.nom',  'vdi.nom_utilisateur');
31
 
32
 
33
	public function __construct(Conteneur $conteneur) {
34
		$this->conteneur = $conteneur;
35
		$this->bdd = $this->conteneur->getBdd();
36
	}
37
 
38
	public function setParametres(Array $parametres) {
39
		$this->parametres = $parametres;
40
	}
41
 
42
	public function getRequeteSql() {
43
		return $this->requete;
44
	}
45
 
46
	private function addJoin($join) {
47
		$this->requete['join'][] = $join;
48
	}
49
 
50
	public function getJoin() {
51
		return ($this->requete['join'] ? implode(' ', array_unique($this->requete['join'])).' ' : '');
52
	}
53
 
54
	private function addJoinDis($join) {
55
		$this->requete['join']['dis'] = $join;
56
	}
57
 
58
	private function addWhere($idParam, $where) {
59
		if (isset($this->parametres['_parametres_condition_or_'])
60
				&& in_array($idParam, $this->parametres['_parametres_condition_or_'])) {
61
			$this->requete['where']['OR'][] = $where;
62
		} else {
63
			$this->requete['where']['AND'][] = $where;
64
		}
65
	}
66
	public function getWhere() {
67
		if (isset($this->requete['where']['OR']) && count($this->requete['where']['OR']) > 0) {
68
			$this->requete['where']['AND'][] = '('.implode(' OR ', $this->requete['where']['OR']).')';
69
		}
70
 
71
		$where = ' TRUE ';
72
		if (isset($this->requete['where']['AND']) && count($this->requete['where']['AND']) > 0) {
73
			$where = implode(' AND ', $this->requete['where']['AND']).' ';
74
		}
75
		return $where;
76
	}
77
 
78
	private function addGroupBy($groupBy) {
79
		$this->requete['groupby'][] = $groupBy;
80
	}
81
 
82
	public function getGroupBy() {
83
		$groupby = '';
84
		if (isset($this->requete['groupby']) && count($this->requete['groupby']) > 0) {
85
			$groupby = 'GROUP BY '.implode(', ', array_unique($this->requete['groupby'])).' ';
86
		}
87
		return $groupby;
88
	}
89
 
90
	private function addOrderBy($orderby) {
91
		$this->requete['orderby'][] = $orderby;
92
	}
93
 
94
	public function getOrderBy() {
95
		$orderby = '';
96
		if (isset($this->requete['orderby']) && count($this->requete['orderby']) > 0) {
97
			$orderby = 'ORDER BY '.implode(', ', array_unique($this->requete['orderby'])).' ';
98
		}
99
		return $orderby;
100
	}
101
 
102
	public function getLimit() {
103
		return 'LIMIT '.$this->parametres['navigation.depart'].','.$this->parametres['navigation.limite'].' ';
104
	}
105
 
106
	/**
107
	 * - Rempli le tableau des contraintes "where" et "join" nécessaire
108
	 * à la *recherche* des observations demandées ($req) utilisées par self::getIdObs()
109
	 *
110
	 * Attention, cela signifie que toutes les tables ne sont pas *forcément*
111
	 * join'ées, par exemple si aucune contrainte ne le nécessite.
112
	 * $req tel qu'il est rempli ici est utile pour récupéré la seule liste des
113
	 * id d'observation qui match.
114
	 * Pour la récupération effective de "toutes" les données correspondante, il faut
115
	 * réinitialiser $req["join"] afin d'y ajouter toutes les autres tables.
116
	 *
117
	 * Note: toujours rajouter les préfixes de table (vdi,du,doi ou di), en fonction de ce que défini
118
	 * les JOIN qui sont utilisés.
119
	 * le préfix de v_del_image est "vdi" (cf: "FROM" de self::getIdObs())
120
	 * le préfix de del_utilisateur sur id_utilisateur = vdi.ce_utilisateur est "du"
121
	 *
122
	 * @param $p les paramètres (notamment de masque) passés par l'URL et déjà traités/filtrés (sauf quotes)
123
	 * @param $req le tableau, passé par référence représentant les composants de la requête à bâtir
124
	 */
125
	public function ajouterContraintes() {
126
		$this->ajouterContrainteAuteur();
127
		$this->ajouterContrainteDate();
128
		$this->ajouterContrainteDepartement();
129
		$this->ajouterContrainteIdZoneGeo();
130
		$this->ajouterContrainteGenre();
131
		$this->ajouterContrainteFamille();
132
		$this->ajouterContrainteNs();
133
		$this->ajouterContrainteNn();
134
		$this->ajouterContrainteReferentiel();
135
		$this->ajouterContrainteCommune();
136
	}
137
 
138
	private function ajouterContrainteAuteur() {
139
		if (isset($this->parametres['masque.auteur'])) {
140
			$auteur = $this->parametres['masque.auteur'];
141
			// id du poster de l'obs
142
			$this->addJoin('LEFT JOIN del_utilisateur AS du ON (du.id_utilisateur = vdi.ce_utilisateur) ');
143
			// id du poster de l'image... NON, c'est le même que le posteur de l'obs
144
			// Cette jointure de table est ignoré ci-dessous pour les recherches d'auteurs
145
			// $req['join'][] = 'LEFT JOIN del_utilisateur AS dui ON dui.id_utilisateur = vdi.i_ce_utilisateur';
146
 
147
			if (is_numeric($auteur)) {
148
				$this->ajouterContrainteAuteurId();
149
			} elseif(preg_match('/^.{5,}@[a-z0-9-.]{5,}$/i', $auteur)) {
150
				$this->ajouterContrainteAuteurEmail();
151
			} else {
152
				$this->ajouterContrainteAuteurIntitule();
153
			}
154
		}
155
	}
156
 
157
	private function ajouterContrainteAuteurId() {
158
		$id = $this->parametres['masque.auteur'];
159
		$sqlTpl = '(du.id_utilisateur = %1$d OR vdi.ce_utilisateur = %1$d)';
160
		$whereAuteur = sprintf($sqlTpl, $id);
161
		$this->addWhere('masque.auteur', $whereAuteur);
162
	}
163
 
164
	private function ajouterContrainteAuteurEmail() {
165
		$email = $this->parametres['masque.auteur'];
166
		$sqlTpl = '(du.courriel LIKE %1$s OR vdi.courriel LIKE %1$s )';
167
		$emailP = $this->bdd->proteger("$email%");
168
		$whereAuteur = sprintf($sqlTpl, $emailP);
169
		$this->addWhere('masque.auteur', $whereAuteur);
170
	}
171
 
172
	/**
173
	 * Retourne une clause where du style:
174
	 * CONCAT(IF(du.prenom IS NULL, "", du.prenom), [...] vdi.i_nomutilisateur) REGEXP 'xxx'
175
	 * Note; i_(nom|prenom_utilisateur), alias pour cel_images.(nom|prenom), n'est pas traité
176
	 * car cette information est redondante dans cel_image et devrait être supprimée.
177
	 */
178
	private function ajouterContrainteAuteurIntitule() {
179
		$auteurExplode = explode(' ', $this->parametres['masque.auteur']);
180
		$nbreMots = count($auteurExplode);
181
 
182
		if ($nbreMots == 1) {
183
			$this->ajouterContrainteAuteurPrenomOuNom();
184
		} else if ($nbreMots == 2) {
185
			$this->ajouterContrainteAuteurPrenomEtNom();
186
		}
187
	}
188
 
189
	private function ajouterContrainteAuteurPrenomOuNom() {
190
		$prenomOuNom = $this->parametres['masque.auteur'];
191
 
192
		$sqlTpl = 'CONCAT(%s,%s) LIKE %s';
193
		$champsPrenomSql = self::ajouterIfNullPourConcat($this->champsPrenom);
194
		$champsNomSql = self::ajouterIfNullPourConcat($this->champsNom);
195
		$auteurMotif = $this->bdd->proteger("%$prenomOuNom%");
196
 
197
		$auteurWhere = sprintf($sqlTpl, $champsPrenomSql, $champsNomSql, $auteurMotif);
198
		$this->addWhere('masque.auteur', $auteurWhere);
199
	}
200
 
201
	private function ajouterContrainteAuteurPrenomEtNom() {
202
		list($prenom, $nom) = explode(' ', $this->parametres['masque.auteur']);
203
 
204
		$sqlTpl = '(CONCAT(%1$s,%2$s) LIKE %3$s AND CONCAT(%1$s,%2$s) LIKE %4$s)';
205
		$champsPrenomSql = self::ajouterIfNullPourConcat($this->champsPrenom);
206
		$champsNomSql = self::ajouterIfNullPourConcat($this->champsNom);
207
		$prenomMotif = $this->bdd->proteger("%$prenom%");
208
		$nomMotif = $this->bdd->proteger("%$nom%");
209
 
210
		$auteurWhere = sprintf($sqlTpl, $champsPrenomSql, $champsNomSql, $prenomMotif, $nomMotif);
211
		$this->addWhere('masque.auteur', $auteurWhere);
212
	}
213
 
214
		/**
215
	 * Lorsque l'on concatène des champs, un seul NULL prend le dessus,
216
	 * Il faut donc utiliser la syntaxe IFNULL(%s, "").
217
	 * (Cette fonction effectue aussi l'implode() "final"
218
	 */
219
	private static function ajouterIfNullPourConcat($champs) {
220
		$champsProteges = array();
221
		foreach ($champs as $champ) {
222
			$champsProteges[] = "IFNULL($champ, '')";
223
		}
224
		return implode(',', $champsProteges);
225
	}
226
 
227
	private function ajouterContrainteDate() {
228
		if (isset($this->parametres['masque.date'])) {
229
			$date = $this->parametres['masque.date'];
230
			if (is_integer($date) && $date < 2030 && $date > 1600) {
231
				$sqlTpl = "YEAR(vdi.date_observation) = %d";
232
				$dateWhere = sprintf($sqlTpl, $date);
233
				$this->addWhere('masque.date', $dateWhere);
234
			} else {
235
				$sqlTpl = "DATE_FORMAT(vdi.date_observation, '%%Y-%%m-%%d') = %s";
236
				$dateP = $this->bdd->proteger(strftime('%Y-%m-%d', $date));
237
				$dateWhere = sprintf($sqlTpl, $dateP);
238
				$this->addWhere('masque.date', $dateWhere);
239
			}
240
		}
241
	}
242
 
243
	private function ajouterContrainteDepartement() {
244
		if (isset($this->parametres['masque.departement'])) {
245
			$dept = $this->parametres['masque.departement'];
246
			$deptMotif = $this->bdd->proteger("INSEE-C:$dept");
247
			$this->addWhere('masque.departement', "vdi.ce_zone_geo LIKE $deptMotif");
248
		}
249
	}
250
 
251
	private function ajouterContrainteIdZoneGeo() {
252
		if (isset($this->parametres['masque.id_zone_geo'])) {
253
			$idZgMotif = $this->bdd->proteger($this->parametres['masque.id_zone_geo']);
254
			$this->addWhere('masque.id_zone_geo', "vdi.ce_zone_geo = $idZgMotif");
255
		}
256
	}
257
 
258
	private function ajouterContrainteGenre() {
259
		if (isset($this->parametres['masque.genre'])) {
260
			$genre = $this->parametres['masque.genre'];
261
			$genreMotif = $this->bdd->proteger("%$genre% %");
262
			$this->addWhere('masque.genre', "vdi.nom_sel LIKE $genreMotif");
263
		}
264
	}
265
 
266
	private function ajouterContrainteFamille() {
267
		if (isset($this->parametres['masque.famille'])) {
268
			$familleMotif = $this->bdd->proteger($this->parametres['masque.famille']);
269
			$this->addWhere('masque.famille', "vdi.famille = $familleMotif");
270
		}
271
	}
272
 
273
	private function ajouterContrainteNs() {
274
		if (isset($this->parametres['masque.ns'])) {
275
			$ns = $this->parametres['masque.ns'];
276
			$nsMotif = $this->bdd->proteger("$ns%");
277
			$this->addWhere('masque.ns', "vdi.nom_sel LIKE $nsMotif");
278
		}
279
	}
280
 
281
	private function ajouterContrainteNn() {
282
		if (isset($this->parametres['masque.nn'])) {
283
			$sqlTpl = '(vdi.nom_sel_nn = %1$d OR vdi.nom_ret_nn = %1$d)';
284
			$nnWhere = sprintf($sqlTpl, $this->parametres['masque.nn']);
285
			$this->addWhere('masque.nn', $nnWhere);
286
		}
287
	}
288
 
289
	private function ajouterContrainteReferentiel() {
290
		if (isset($this->parametres['masque.referentiel'])) {
291
			$ref = $this->parametres['masque.referentiel'];
292
			$refMotif = $this->bdd->proteger("$ref%");
293
			$this->addWhere('masque.referentiel', "vdi.nom_referentiel LIKE $refMotif");
294
		}
295
	}
296
 
297
	private function ajouterContrainteCommune() {
298
		if (isset($this->parametres['masque.commune'])) {
299
			$commune = $this->parametres['masque.commune'];
300
			$communeMotif = $this->bdd->proteger("$commune%");
301
			$this->addWhere('masque.commune', "vdi.zone_geo LIKE $communeMotif");
302
		}
303
	}
304
 
305
	/**
306
	 * in $p: un tableau de paramètres, dont:
307
	 * - 'masque.tag_cel': *tableau* de mots-clefs à chercher parmi cel_image.mots_clefs_texte
308
	 * - 'masque.tag_del': *tableau* de mots-clefs à chercher parmi del_image_tag.tag_normalise
309
	 * - 'tag_explode_semantic': défini si les éléments sont tous recherchés ou NON
310
	 *
311
	 * in/ou: $req: un tableau de structure de requête MySQL
312
	 *
313
	 * Attention, le fait que nous cherchions masque.tag_cel OU/ET masque.tag_cel
314
	 * ne dépend pas de nous, mais du niveau supérieur de construction de la requête:
315
	 * Soit directement $this->consulter() si des masque.tag* sont passés
316
	 * (split sur ",", "AND" entre chaque condition, "OR" pour chaque valeur de tag)
317
	 * Soit via sqlAddMasqueConstraint():
318
	 * (pas de split, "OR" entre chaque condition) [ comportement historique ]
319
	 * équivalent à:
320
	 * (split sur " ", "OR" entre chaque condition, "AND" pour chaque valeur de tag)
321
	 *
322
	 */
323
	public function ajouterConstrainteAppliImg() {
324
		$this->ajouterContrainteMilieu();
325
		$this->ajouterContrainteTri();
326
		$this->ajouterContrainteTagCel();
327
		$this->ajouterContrainteTagDel();
328
	}
329
 
330
	private function ajouterContrainteMilieu() {
331
		if (isset($this->parametres['masque.milieu'])) {
332
			$milieu = $this->parametres['masque.milieu'];
333
			$milieuMotif = $this->bdd->proteger("%$milieu%");
334
			$this->addWhere('masque.milieu', "vdi.milieu LIKE $milieuMotif");
335
		}
336
	}
337
 
338
	/** Pour le tri par AVG() des votes nous avons toujours un protocole donné,
339
	 * celui-ci indique sur quels votes porte l'AVG.
340
	 * (c'est un *vote* qui porte sur un protocole et non l'image elle-même)
341
	 * TODO: perf problème:
342
	 * 1) SQL_CALC_FOUND_ROWS: fixable en:
343
	 * - dissociant le comptage de la récup d'id + javascript async
344
	 * - ou ne rafraîchir le total *que* pour les requête impliquant un changement de pagination
345
	 * (paramètre booléen "with-total" par exemple)
346
	 * 2) jointure forcées: en utilisant `del_imagè`, nous forçons les 2 premiers
347
	 * JOIN sur cel_obs_images et cel_obs pour filtrer sur "transmission".
348
	 * Dénormaliser cette valeur et l'intégrer à `cel_images` ferait économiser cette couteuse
349
	 * jointure, ... lorsqu'aucun masque portant sur `cel_obs` n'est utilisé
350
	 * 3) non-problème: l'ordre des joins est forcé par l'usage de la vue:
351
	 * (cel_images/cel_obs_images/cel_obs/del_image_stat)
352
	 * Cependant c'est à l'optimiseur de définir son ordre préféré.
353
	 */
354
	private function ajouterContrainteTri() {
355
		if (isset($this->parametres['tri'])) {
356
			$tri = $this->parametres['tri'];
357
 
358
			if (isset($this->parametres['protocole'])  && ($tri == 'votes' || $tri == 'points')) {
359
				// $this->parametres['protocole'] *est* défini (cf Outils::filtrerUrlsParams...())
360
				$sqlTpl = 'LEFT JOIN del_image_stat dis ON vdi.id_image = dis.ce_image AND dis.ce_protocole = %d';
361
				$triSql = sprintf($sqlTpl, $this->parametres['protocole']);
362
				$this->addJoinDis($triSql);
363
			}
364
 
365
			if (isset($this->parametres['ordre']) && $tri == 'tags') {
366
				$typeJointure = ($this->parametres['ordre'] == 'desc') ? 'INNER' : 'LEFT';
367
				$this->addJoin("$typeJointure JOIN del_image_stat dis ON vdi.id_image = dis.ce_image");
368
				// nécessaire (dup ce_image dans del_image_stat)
369
				$this->addGroupBy('vdi.id_observation');
370
			}
371
		}
372
	}
373
 
374
	/**
375
	 * Car il ne sont pas traités par la générique requestFilterParams() les clefs "masque.tag_*"
376
	 * sont toujours présentes; bien que parfois NULL.
377
	 */
378
	// TODO: utiliser les tables de mots clefs normaliées dans tb_cel ? et auquel cas laisser au client le choix du couteux "%" ?
379
	private function ajouterContrainteTagCel() {
380
		if ($this->parametres['masque.tag_cel']) {
381
			if (isset($this->parametres['masque.tag_cel']['AND'])) {
382
				$tags = $this->parametres['masque.tag_cel']['AND'];
383
				$clausesWhere = array();
384
				foreach ($tags as $tag) {
385
					$tagMotif = $this->bdd->proteger("%$tag%");
386
					$sqlTpl = "CONCAT(IFNULL(vdi.mots_cles_texte,''),IFNULL(vdi.i_mots_cles_texte,'')) LIKE %s";
387
					$clausesWhere[] = sprintf($sqlTpl, $tagMotif);
388
				}
389
				$whereTags = implode(' AND ', $clausesWhere);
390
				$this->addWhere('masque.tag_cel', "($whereTags)");
391
			} else if (isset($this->parametres['masque.tag_cel']['OR'])) {
392
				$tags = $this->parametres['masque.tag_cel']['OR'];
393
				$sqlTpl = "CONCAT(IFNULL(vdi.mots_cles_texte,''),IFNULL(vdi.i_mots_cles_texte,'')) REGEXP %s";
394
				$tagMotif = $this->bdd->proteger(implode('|', $tags));
395
				$tagSql = sprintf($sqlTpl, $tagMotif);
396
				$this->addWhere('masque.tag_cel', $tagSql);
397
			}
398
		}
399
	}
400
 
401
	/**
402
	 * Plusieurs solutions disponibles pour la gestion des contraintes des tags DEL :
403
	 * - inutilisable pour l'instant : ajouterContrainteTagDelSolution1();
404
	 * - intéressante, mais problème d'optimiseur MySQL 5.5 (dependant subquery) : ajouterContrainteTagDelSolution2();
405
	 * - approche fiable mais sous-optimale : ajouterContrainteTagDelSolution3();
406
	 */
407
	private function ajouterContrainteTagDel() {
408
		if (isset($this->parametres['masque.tag_del'])) {
409
			$this->ajouterContrainteTagDelSolution3();
410
		}
411
	}
412
 
413
	/** Approche intéressante si les deux problèmes suivants peuvent être résolu:
414
	 * - LEFT JOIN => dup => *gestion de multiples GROUP BY* (car in-fine un LIMIT est utilisé)
415
	 * - dans le cas d'un ET logique, comment chercher les observations correspondantes ?
416
	 */
417
	private function ajouterContrainteTagDelSolution1() {
418
		// XXX: utiliser tag plutôt que tag_normalise ?
419
		$req['join'][] = 'LEFT JOIN del_image_tag dit ON dit.ce_image = vdi.id_image';
420
		$req['where'][] = 'dit.actif = 1';
421
		$req['groupby'][] = 'vdi.id_image'; // TODO: nécessaire (car dup') mais risque de conflict en cas de tri (multiple GROUP BY)
422
		// XXX: en cas de ET, possibilité du GROUP_CONCAT(), mais probablement sans grand intérêt, d'où une boucle
423
		if (isset($p['masque.tag_del']['AND'])) {
424
			// TODO/XXX : comment matcher les observations ayant tous les mots-clef passés ?
425
			// ... le LEFT-JOIN n'y semble pas adapté
426
		} else {
427
			$protected_tags = array();
428
			foreach ($p['masque.tag_del']['OR'] as $tag) {
429
				$protected_tags[] = $db->proteger(strtolower($tag));
430
			}
431
			$req['where'][] = sprintf('tag_normalise IN (%s)', implode(',', $protected_tags));
432
		}
433
	}
434
 
435
	/**
436
	 * Inutilisé pour l'instant pour cause de soucis d'optimiseur MySQL (cf commentaire en intro)
437
	 */
438
	private function ajouterContrainteTagDelSolution2() {
439
		// Note à propos des 4 "@ instruction" ci-dessous (notamment sur recupererTous())
440
		// REGEXP permet un puissant mécanisme de sélection des obs/image à qui sait
441
		// l'utiliser, mais peut sortir une erreur en cas de REGEXP invalide
442
		// ex: REGEX "^(".
443
		// Pour l'heure nous ignorons ce type d'erreur car aucun de nos champ de recherche
444
		// ne peuvent (ou ne devrait) comporter des meta-caractères
445
		// ([])?*+\\
446
		if (isset($p['masque.tag_del']['AND'])) {
447
			// optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
448
			// donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
449
			sort($p['masque.tag_del']['AND']);
450
			$req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
451
				" GROUP BY ce_image".
452
				" HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s)",
453
				$db->proteger(implode('.*', $p['masque.tag_del']['AND'])));
454
		} else {
455
			$req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
456
				" GROUP BY ce_image".
457
				" HAVING GROUP_CONCAT(tag_normalise) REGEXP %s)",
458
				$db->proteger(implode('|', $p['masque.tag_del']['OR'])));
459
		}
460
	}
461
 
462
	/**
463
	 * Si l'on est bassiné par les "DEPENDENT SUBQUERY", nous la faisons donc indépendemment via cette fonction
464
	 */
465
	private function ajouterContrainteTagDelSolution3() {
466
		if (isset($this->parametres['masque.tag_del']['AND'])) {
467
			$tags = $this->parametres['masque.tag_del']['AND'];
468
			// optimisation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
469
			// donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
470
			sort($tags);
471
			$tagsMotif = $this->bdd->proteger(implode('.*', $tags));
472
			$requete = 'SELECT ce_image '.
473
				'FROM del_image_tag '.
474
				'WHERE actif = 1 '.
475
				'GROUP BY ce_image '.
476
				"HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP $tagsMotif ".
477
				' -- '.__FILE__.' : '.__LINE__;
478
			$sql = $this->recupererSqlContrainteTag($requete);
479
			$this->addWhere('masque.tag_del', $sql);
480
 
481
		} else if (isset($this->parametres['masque.tag_del']['OR'])) {
482
			$tags = $this->parametres['masque.tag_del']['OR'];
483
			$tagsMotif = $this->bdd->proteger(implode('|', $tags));
484
			$requete = 'SELECT ce_image '.
485
				'FROM del_image_tag '.
486
				'WHERE actif = 1 '.
487
				'GROUP BY ce_image '.
488
				"HAVING GROUP_CONCAT(tag_normalise) REGEXP $tagsMotif ".
489
				' -- '.__FILE__.' : '.__LINE__;
490
			$sql = $this->recupererSqlContrainteTag($requete);
491
			$this->addWhere('masque.tag_del', $sql);
492
		}
493
	}
494
 
495
	private function recupererSqlContrainteTag($requete) {
496
		$resultats = $this->bdd->recupererTous($requete);
497
		$ids = array();
498
		foreach ($resultats as $resultat) {
499
			$ids[] = $resultat['ce_image'];
500
		}
501
 
502
		if (!empty($ids)) {
503
			$clauseIn = implode(',', $ids);
504
		} else {
505
			$clauseIn = 'SELECT ce_image FROM del_image_tag WHERE false';
506
		}
507
		return "vdi.id_image IN ($clauseIn)";
508
	}
509
 
510
	/**
511
	 * Partie spécifique à PictoFlora:
512
	 * génération de la clause ORDER BY (génère la valeur de la clef orderby' de $req)
513
	 * nécessaire ? tableau sprintf(key (tri) => value (ordre), key => value ...).
514
	 * Cependant il est impensable de joindre sur un AVG() des valeurs des votes pour
515
	 * *chaque* couple (id_image, protocole) de la base afin de trouver les images
516
	 * les "mieux notées", ou bien les images ayant le "plus de tags" (COUNT())
517
	 */
518
	public function definirOrdreSqlAppliImg() {
519
		$ordre = $this->parametres['ordre'];
520
 
521
		// parmi self::$tri_possible
522
		switch ($this->parametres['tri']) {
523
			case 'votes' :
524
				$this->addOrderBy("dis.moyenne $ordre, dis.nb_votes $ordre");
525
				break;
526
			case 'points' :
527
				$this->addOrderBy("dis.nb_points $ordre, dis.moyenne $ordre, dis.nb_votes $ordre");
528
				break;
529
			case 'tags' :
530
				$this->addOrderBy("dis.nb_tags $ordre");
531
				break;
532
			case 'date_observation' :
533
				$this->addOrderBy("date_observation $ordre, id_observation $ordre");
534
				break;
535
			default:
536
				$this->addOrderBy("date_transmission $ordre, id_observation $ordre");
537
		}
538
	}
539
 
540
	public function getAliasDesChamps($champsEtAlias, $select = null, $prefix = null) {
541
		$arr = ($select) ? array_intersect_key($champsEtAlias, array_flip($select)) :  $champsEtAlias;
542
		$keys = array_keys($arr);
543
 
544
		if ($prefix) {
545
			array_walk($keys, create_function('&$val, $k, $prefix', '$val = sprintf("%s.`%s`", $prefix, $val);'), $prefix);
546
		} else {
547
			array_walk($keys, create_function('&$val, $k', '$val = sprintf("`%s`", $val);'));
548
		}
549
 
550
		return implode(', ', array_map(create_function('$v, $k', 'return sprintf("%s AS `%s`", $k, $v);'), $arr, $keys));
551
	}
552
 
553
		// Charger les images et leurs votes associés
554
	public function getVotesDesImages($idsImages, $protocole = null) {
555
		if (!$idsImages) return;
556
 
557
		$mappingVotes = $this->conteneur->getParametreTableau('votes.mapping');
558
		$mappingProtocoles = $this->conteneur->getParametreTableau('protocoles.mapping');
559
 		$selectVotes = array('id_vote', 'ce_image', 'ce_protocole', 'ce_utilisateur', 'valeur', 'date');
560
		$selectProtocole = array('id_protocole', 'intitule', 'descriptif', 'tag');
561
		$voteChamps = $this->getAliasDesChamps($mappingVotes, $selectVotes, 'v'); // "v": cf alias dans la requête
562
		$protoChamps = $this->getAliasDesChamps($mappingProtocoles, $selectProtocole, 'p');
563
		$idImgsConcat = implode(',', $idsImages);
564
 
565
		$requete = "SELECT $voteChamps, $protoChamps ".
566
			'FROM del_image_vote AS v '.
567
			'	INNER JOIN del_image_protocole AS p ON (v.ce_protocole = p.id_protocole) '.
568
			"WHERE v.ce_image IN ($idImgsConcat) ".
569
			($protocole ? "	AND v.ce_protocole = $protocole " : '').
570
			"ORDER BY FIELD(v.ce_image, $idImgsConcat) ".
571
			'-- '.__FILE__.':'.__LINE__;
572
 
573
		return $this->bdd->recupererTous($requete);
574
	}
575
 
576
	/**
577
	 * Ajoute les informations sur le protocole et les votes aux images.
578
	 *
579
	 * ATTENTION : Subtilité, nous passons ici le tableau d'images indexé par id_image qui est bien
580
	 * plus pratique pour associer les vote à un tableau, puisque nous ne connaissons pas les id d'observation.
581
	 * Mais magiquement (par référence), cela va remplir notre tableau indexé par couple d'id (id_image, id_observation)
582
	 * cf ListeImages::reformateImagesDoubleIndex() à qui revient la tâche de créer ces deux versions
583
	 * simultanément lorsque c'est encore possible.
584
	 */
585
	public function ajouterInfosVotesProtocoles($votes, &$images) {
586
		if (!$votes) return;
587
 
588
		$mappingVotes = $this->conteneur->getParametreTableau('votes.mapping');
589
		$mappingProtocoles = $this->conteneur->getParametreTableau('protocoles.mapping');
590
 
591
		// pour chaque vote
592
		foreach ($votes as $vote) {
593
			$imgId = $vote['image.id'];
594
			$protoId = $vote['protocole.id'];
595
 
596
			if (!array_key_exists('protocoles_votes', $images[$imgId]) ||
597
			   !array_key_exists($protoId, $images[$imgId]['protocoles_votes'])) {
598
				// extrait les champs spécifique au protocole (le LEFT JOIN de chargerVotesImage les ramène en doublons
599
				$protocole = array_intersect_key($vote, array_flip($mappingProtocoles));
600
				$images[$imgId]['protocoles_votes'][$protoId] = $protocole;
601
			}
602
 
603
			$chpsVotes = array('id_vote', 'ce_image', 'ce_utilisateur', 'valeur', 'date');
604
			$voteSelection = array_intersect_key($mappingVotes, array_flip($chpsVotes));
605
			$vote = array_intersect_key($vote, array_flip($voteSelection));
606
			$images[$imgId]['protocoles_votes'][$protoId]['votes'][$vote['vote.id']] = $vote;
607
		}
608
	}
609
}