Subversion Repositories eFlore/Applications.del

Rev

Rev 1584 | Rev 1655 | 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
1495 raphael 8
 * @see http://www.tela-botanica.org/wikini/identiplante/wakka.php?wiki=IdentiPlante_PictoFlora_MoteurRecherche
1422 raphael 9
 *
10
 * Backend pour PictoFlora (del.html#page_recherche_images)
11
 *
12
 *
13
 * == Notes ==
14
 *
15
 * tri=votes et tri=tags: affectent le choix des images affichées (donc getIdImages())
16
 * Cependant ce total ne nous intéresse même pas (MoyenneVotePresenteur.java s'en occupe).
17
 * Seul tri=date_transmission nous évite l'AVG() + GROUP BY
18
 *
19
 * protocole: il affecte l'affichage des information, mais le JSON contient déjà
20
 * l'intégralité (chercher les données de vote pour 1 ou plusieurs protocoles) est quasi-identique.
21
 * Par contre, le tri par moyenne des votes, sous-entend "pour un protocole donné".
22
 * Dès lors le choix d'un protocole doit avoir été fait afin de régler le JOIN et ainsi l'ORDER BY.
23
 * (cf requestFilterParams())
24
 *
25
 * Histoire: auparavant (pré-r142x) un AVG + GROUP BY étaient utilisés pour générer on-the-fly les valeurs
1490 raphael 26
 * utilisées ensuite pour l'ORDER BY. La situation à base de del_image_stat
27
 * est déjà bien meilleure sans être pour autant optimale. cf commentaire de sqlAddConstraint()
1422 raphael 28
 *
29
 *
30
 * Tags:
31
 * Le comportement habituel dans le masque *général*: les mots sont séparés par des espaces,
32
 * implod()ed par des AND (tous les mots doivent matcher).
33
 * Et le test effectué doit matcher sur:
34
 * %(les tags d'observations)% *OU* %(les tags d'images)% *OU* %(les tags publics)%
35
 *
36
 * Le comportement habituel dans le masque *tag*: les mots ne sont *pas* splittés (1 seule expression),
37
 * Et le test effectué doit matcher sur:
38
 * ^(expression)% *OU* %(expression)% [cf getConditionsImages()]
39
 *
40
 * Par défaut les tags sont comma-separated (OU logique).
41
 * Cependant pour conserver le comportement du masque général qui sous-entend un ET logique sur
1604 jpm 42
 * des tags séparés par des espaces recherche
1422 raphael 43
 *
44
 * TODO:
45
 * -affiner la gestion de passage de mots-clefs dans le masque général.
46
 * - subqueries dans le FROM pour les critère WHERE portant directement sur v_del_image
47
 * plutôt que dans WHERE (qui nécessite dès lors un FULL-JOIN)
48
 * (http://www.mysqlperformanceblog.com/2007/04/06/using-delayed-join-to-optimize-count-and-limit-queries/)
49
 * - éviter de dépendre d'une jointure systématique sur `cel_obs`, uniquement pour `(date_)transmission
50
 * (cf VIEW del_image)
1492 raphael 51
 * - poursuivre la réorganisation des méthodes statiques parmis Observation, ListeObservations et ListeImages2
1422 raphael 52
 * - *peut-être*: passer requestFilterParams() en méthode de classe
53
 *
1438 raphael 54
 *
55
 * MySQL sux:
56
 * 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 57
 *	MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery
1438 raphael 58
 * EXPLAIN SELECT * FROM del_image WHERE id_image IN (SELECT 3);
1486 raphael 59
 *	PRIMARY
1438 raphael 60
 * EXPLAIN SELECT * FROM del_image WHERE id_image IN (SELECT MIN(3));
1486 raphael 61
 *	DEPENDENT SUBQUERY ... ... ... mwarf !
1438 raphael 62
 * 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 63
 *	5.5: MATERIALIZED		del_image_tag	ALL				ce_image NULL NULL NULL 38276 Using where
64
 *	5.1: DEPENDENT SUBQUERY	del_image_tag	index_subquery	ce_image ce_image 8 func 1 Using where
1438 raphael 65
 * FORCE INDEX/IGNORE INDEX semble incapable de résoudre le problème de l'optimiseur MySQL
66
 *
1390 raphael 67
 */
68
 
1490 raphael 69
require_once(dirname(__FILE__) . '/../DelTk.php');
1390 raphael 70
require_once(dirname(__FILE__) . '/../observations/Observation.php');
71
restore_error_handler();
72
restore_exception_handler();
73
error_reporting(E_ALL);
74
 
1422 raphael 75
// del/services/0.1/images?navigation.depart=0&navigation.limite=12&tri=votes&ordre=desc
76
// del/services/0.1/images?navigation.depart=0&navigation.limite=12&tri=votes&ordre=desc&masque=plop
77
// del/services/0.1/images?navigation.depart=0&navigation.limite=12&tri=votes&ordre=desc&protocole=3
78
// del/services/0.1/images?navigation.depart=0&navigation.limite=12&tri=votes&ordre=desc&protocole=3&masque=plop
79
 
1514 aurelien 80
class ListeImages {
1390 raphael 81
 
1604 jpm 82
	// TODO: PHP-x.y, ces variables devrait être des "const"
83
	static $format_image_possible = array('O','CRX2S','CRS','CXS','CS','XS','S','M','L','XL','X2L','X3L');
1390 raphael 84
 
1604 jpm 85
	static $tri_possible = array('date_transmission', 'date_observation', 'votes', 'tags', 'points');
1390 raphael 86
 
1604 jpm 87
	// en plus de ceux dans DelTk
88
	static $parametres_autorises = array('protocole', 'masque.tag_cel', 'masque.tag_pictoflora', 'masque.milieu');
1422 raphael 89
 
1604 jpm 90
	static $default_params = array('navigation.depart' => 0, 'navigation.limite' => 10,
91
		'tri' => 'date_transmission', 'ordre' => 'desc',
92
		// spécifiques à PictoFlora:
93
		'format' => 'XL');
1422 raphael 94
 
1604 jpm 95
	static $default_proto = 3; // proto par défaut: capitalisation d'img (utilisé uniquement pour tri=(tags|votes|points))
1422 raphael 96
 
1604 jpm 97
	static $mappings = array(
98
		'observations' => array( // v_del_image
99
			"id_observation" => 1,
100
			"date_observation" => 1,
101
			"date_transmission" => 1,
102
			"famille" => "determination.famille",
103
			"nom_sel" => "determination.ns",
104
			"nom_sel_nn" => "determination.nn",
105
			"nom_referentiel" => "determination.referentiel",
106
			"nt" => "determination.nt",
107
			"ce_zone_geo" => "id_zone_geo",
108
			"zone_geo" => 1,
109
			"lieudit" => 1,
110
			"station" => 1,
111
			"milieu" => 1,
112
			"mots_cles_texte" => "mots_cles_texte",
113
			"commentaire" => 1,
114
			"ce_utilisateur" => "auteur.id",
115
			"nom_utilisateur" => "auteur.nom",
116
			"prenom_utilisateur" => "auteur.prenom",),
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
	);
1422 raphael 123
 
1604 jpm 124
	public function __construct(Conteneur $conteneur = null) {
1564 mathias 125
		$this->conteneur = $conteneur == null ? new Conteneur() : $conteneur;
126
		$this->conteneur->chargerConfiguration('config_images.ini');
1604 jpm 127
		$this->bdd = $conteneur->getGestionBdd()->getBdd();
128
	}
1390 raphael 129
 
1604 jpm 130
	public function consulter($ressources, $parametres) {
131
		/* Certes nous sélectionnons ici (nom|prenom|courriel)_utilisateur de cel_obs, mais il ne nous intéressent pas
132
		Par contre, ci-dessous nous prenons i_(nom|prenom|courriel)_utilisateur.
133
		Notons cependant qu'aucun moyen ne devrait permettre que i_*_utilisateur != *_utilisateur
134
		Le propriétaire d'une obs et de l'image associée est *toujours* le même. */
135
		array_walk(self::$mappings['observations'], create_function('&$val, $k', 'if($val==1) $val = $k;'));
136
		array_walk(self::$mappings['images'], create_function('&$val, $k', 'if($val==1) $val = $k;'));
137
		// pour les votes, les mappings de "Observation" nous suffisent
138
		array_walk(Observation::$mappings['votes'], create_function('&$val, $k', 'if($val==1) $val = $k;'));
1390 raphael 139
 
1604 jpm 140
		// la nécessité du 'groupby' dépend des 'join's utilisés (LEFT ou INNER) ainsi que de la cardinalité
141
		// de `ce_image` dans ces tables jointes.
142
		// Contrairement à IdentiPlantes, nous n'avons de HAVING pour PictoFlora, mais par contre un ORDER BY
143
		$req = array('select' => array(), 'join' => array(), 'where' => array(), 'groupby' => array(), 'orderby' => array());
1422 raphael 144
 
1604 jpm 145
		$db = $this->bdd;
1422 raphael 146
 
1604 jpm 147
		// filtrage de l'INPUT général, on réutilise 90% de identiplante en terme de paramètres autorisés
148
		// ($parametres_autorises) sauf... masque.type qui fait des modif' de WHERE sur les mots-clefs.
149
		// Évitons ce genre de chose pour PictoFlora et les risques de conflits avec masque.tag
150
		// même si ceux-ci sont improbables (pas d'<input> pour cela).
151
		$params_ip = DelTk::requestFilterParams($parametres,
152
			array_diff(DelTk::$parametres_autorises, array('masque.type')),
153
			$this->conteneur);
1390 raphael 154
 
1604 jpm 155
		// notre propre filtrage sur l'INPUT
156
		$params_pf = self::requestFilterParams($parametres,
157
			array_merge(DelTk::$parametres_autorises, self::$parametres_autorises));
1422 raphael 158
 
1604 jpm 159
		/* filtrage des tags + sémantique des valeurs multiples:
160
		Lorsqu'on utilise masque.tag* pour chercher des tags, ils sont
161
		postulés comme séparés par des virgule, et l'un au moins des tags doit matcher. */
162
		$params_pf['masque.tag_cel'] = DelTk::buildTagsAST(@$parametres['masque.tag_cel'], 'OR', ',');
1390 raphael 163
 
1604 jpm 164
		if(!isset($parametres['masque.tag_pictoflora']) && isset($parametres['masque.tag'])) {
165
			$parametres['masque.tag_pictoflora'] = $parametres['masque.tag'];
166
		}
167
		$params_pf['masque.tag_pictoflora'] = DelTk::buildTagsAST(@$parametres['masque.tag_pictoflora'], 'OR', ',');
1390 raphael 168
 
1604 jpm 169
		$params = array_merge(
170
			DelTk::$default_params, // paramètre par défaut Identiplante
171
			self::$default_params, // paramètres par défaut PictoFlora
172
			$params_ip, // les paramètres passés, traités par Identiplante
173
			$params_pf); // les paramètres passés, traités par PictoFlora
1390 raphael 174
 
1604 jpm 175
		if (isset($parametres['format'])) {
176
			$params['format'] = $parametres['format'];
177
		}
1422 raphael 178
 
1604 jpm 179
		// création des contraintes (génériques de DelTk)
180
		DelTk::sqlAddConstraint($params, $db, $req);
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
 
1604 jpm 188
		// 1) grunt-work: *la* requête de récupération des id valides (+ SQL_CALC_FOUND_ROWS)
189
		// $idobs_tab = ListeObservations::getIdObs($params, $req, $db);
190
		$idobs_tab = self::getIdImages($params, $req, $db);
1390 raphael 191
 
1604 jpm 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) {
194
			$resultat = new ResultatService();
195
			$resultat->corps = array('entete' => DelTk::makeJSONHeader(0, $params, Config::get('url_service')),
196
				'resultats' => array());
197
			return $resultat;
198
		}
1390 raphael 199
 
1604 jpm 200
		// idobs est une liste (toujours ordonnée) des id d'observations recherchées
201
		$idobs = array_values(array_map(create_function('$a', 'return $a["id_image"];'), $idobs_tab));
202
		$total = $db->recuperer('SELECT FOUND_ROWS() AS c'); $total = intval($total['c']);
1390 raphael 203
 
1604 jpm 204
		$liaisons = self::chargerImages($db, $idobs);
1390 raphael 205
 
1604 jpm 206
		list($images, $images_keyed_by_id_image) = self::reformateImagesDoubleIndex(
207
			$liaisons,
208
			$this->conteneur->getParametre('images.url_images'),
209
			$params['format']);
1422 raphael 210
 
1604 jpm 211
		// on charge les votes pour ces images et pour *tous* les protocoles
212
		$votes = Observation::chargerVotesImage($db, $liaisons, NULL);
1422 raphael 213
 
1604 jpm 214
		// subtilité, nous passons ici le tableau d'images indexé par id_image qui est bien plus pratique pour
215
		// associer les vote à un tableau, puisque nous ne connaissons pas les id d'observation.
216
		// Mais magiquement (par référence), cela va remplir notre tableau indexé par couple d'id (id_image, id_observation)
217
		// cf reformateImagesDoubleIndex() à qui revient la tâche de créer ces deux versions simultanément lorsque
218
		// c'est encore possible.
219
		if ($votes) {
220
			Observation::mapVotesToImages($votes, $images_keyed_by_id_image);
221
		}
1422 raphael 222
 
1604 jpm 223
		// les deux masques de tags sont transformés en AST dans le processus de construction de la requête.
224
		// Reprenous les paramètres originaux non-nettoyés (ils sont valables car le nettoyage est déterministe)
225
		$params_header = array_merge($params, array_filter(array('masque.tag_cel' => @$parametres['masque.tag_cel'],
226
			'masque.tag_pictoflora' => @$parametres['masque.tag_pictoflora'])));
227
		$resultat = new ResultatService();
228
		$resultat->corps = array(
229
			'entete' => DelTk::makeJSONHeader($total, $params_header, Config::get('url_service')),
230
			'resultats' => $images);
231
		return $resultat;
232
	}
1495 raphael 233
 
1604 jpm 234
	public function supprimer($ressources) {
235
		$idImage = $ressources[0];
236
		$controlAcces = $this->conteneur->getControleAcces();
237
 
238
		$retour = false;
239
		if ($controlAcces->controlerIpAutorisees()) {
240
			if ($controlAcces->etreUtilisateurAvecDroitAdmin()) {
241
				$urlServiceBase = $this->conteneur->getParametre('url_service_suppression_image');
242
				$url = $urlServiceBase.$idImage;
243
 
244
				$clientHttp = $this->conteneur->getRestClient();
245
				echo $url;
246
				$retour = $clientHttp->supprimer($url);
247
				echo $retour;
248
			} else {
249
				$message = "Vous ne pouvez pas accéder à ce service car vous n'avez pas les droits d'administrateur !\n";
250
				$code = RestServeur::HTTP_CODE_ACCES_NON_AUTORISE;
251
				throw new Exception($message, $code);
252
			}
253
		}
254
		return $retour;
255
	}
256
 
257
	/**
258
	 * TODO: partie spécifique liées à la complexité de PictoFlora:
259
	 * génération de la clause ORDER BY (génère la valeur de la clef orderby' de $req)
260
	 * nécessaire ? tableau sprintf(key (tri) => value (ordre), key => value ...).
261
	 * Cependant il est impensable de joindre sur un AVG() des valeurs des votes pour
262
	 * *chaque* couple (id_image, protocole) de la base afin de trouver les images
263
	 * les "mieux notées", ou bien les images ayant le "plus de tags" (COUNT())
264
	 */
265
	static function sqlOrderBy($p, $db, &$req) {
1564 mathias 266
		// parmi self::$tri_possible
1604 jpm 267
		if ($p['tri'] == 'votes') { // LEFT JOIN sur "dis" ci-dessous
268
			$req['orderby'] = 'dis.moyenne ' . $p['ordre'] . ', dis.nb_votes ' . $p['ordre'];
269
			return;
1564 mathias 270
		}
1604 jpm 271
 
272
		if ($p['tri'] == 'points') { // LEFT JOIN sur "dis" ci-dessous
273
			$req['orderby'] = 'dis.nb_points ' . $p['ordre'] . ', dis.moyenne ' . $p['ordre'] . ', dis.nb_votes ' . $p['ordre'];
274
			return;
1564 mathias 275
		}
1604 jpm 276
 
277
		if ($p['tri'] == 'tags') { // LEFT JOIN sur "dis" ci-dessous
278
			$req['orderby'] = 'dis.nb_tags ' . $p['ordre'];
279
			return;
1564 mathias 280
		}
1604 jpm 281
 
282
		if ($p['tri'] == 'date_observation') {
283
			$req['orderby'] = 'date_observation ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
284
			return;
1564 mathias 285
		}
1604 jpm 286
 
1564 mathias 287
		// tri == 'date_transmission'
288
		// avant cel:r1860, date_transmission pouvait être NULL
289
		// or nous voulons de la cohérence (notamment pour phpunit)
290
		$req['orderby'] = 'date_transmission ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
1604 jpm 291
	}
1495 raphael 292
 
1604 jpm 293
	/*
294
	 * in $p: un tableau de paramètres, dont:
295
	 * - 'masque.tag_cel': *tableau* de mots-clefs à chercher parmi cel_image.mots_clefs_texte
296
	 * - 'masque.tag_pictoflora': *tableau* de mots-clefs à chercher parmi del_image_tag.tag_normalise
297
	 * - 'tag_explode_semantic': défini si les éléments sont tous recherchés ou NON
298
	 *
299
	 * in/ou: $req: un tableau de structure de requête MySQL
300
	 *
301
	 * Attention, le fait que nous cherchions masque.tag_cel OU/ET masque.tag_cel
302
	 * ne dépend pas de nous, mais du niveau supérieur de construction de la requête:
303
	 * Soit directement $this->consulter() si des masque.tag* sont passés
304
	 * (split sur ",", "AND" entre chaque condition, "OR" pour chaque valeur de tag)
305
	 * Soit via sqlAddMasqueConstraint():
306
	 * (pas de split, "OR" entre chaque condition) [ comportement historique ]
307
	 * équivalent à:
308
	 * (split sur " ", "OR" entre chaque condition, "AND" pour chaque valeur de tag)
309
	 *
310
	 */
311
	static function sqlAddConstraint($p, $db, &$req, Conteneur $c = NULL) {
1584 mathias 312
		// TODO implement dans DelTk ?
1604 jpm 313
		if (!empty($p['masque.milieu'])) {
314
			$req['where'][] = 'vdi.milieu LIKE '.$db->proteger('%' . $p['masque.milieu'].'%');
1584 mathias 315
		}
1604 jpm 316
 
1584 mathias 317
		/* Pour le tri par AVG() des votes nous avons toujours un protocole donné,
1604 jpm 318
			celui-ci indique sur quels votes porte l'AVG.
319
			(c'est un *vote* qui porte sur un protocole et non l'image elle-même) */
1584 mathias 320
		/* TODO: perf problème:
1604 jpm 321
			1) SQL_CALC_FOUND_ROWS: fixable en:
322
				- dissociant le comptage de la récup d'id + javascript async
323
				- ou ne rafraîchir le total *que* pour les requête impliquant un changement de pagination
324
				(paramètre booléen "with-total" par exemple)
325
			2) jointure forcées: en utilisant `del_imagè`, nous forçons les 2 premiers
326
				JOIN sur cel_obs_images et cel_obs pour filtrer sur "transmission".
327
				Dénormaliser cette valeur et l'intégrer à `cel_images` ferait économiser cette couteuse
328
				jointure, ... lorsqu'aucun masque portant sur `cel_obs` n'est utilisé
329
			3) non-problème: l'ordre des joins est forcé par l'usage de la vue:
330
				(cel_images/cel_obs_images/cel_obs/del_image_stat)
331
				Cependant c'est à l'optimiseur de définir son ordre préféré. */
332
		if ($p['tri'] == 'votes' || $p['tri'] == 'points') {
333
			// $p['protocole'] *est* défini (cf requestFilterParams())
334
			// petite optimisation: INNER JOIN si ordre DESC car les 0 à la fin
335
			if($p['ordre'] == 'desc') {
1584 mathias 336
				// pas de group by nécessaire pour cette jointure
337
				// PRIMARY KEY (`ce_image`, `ce_protocole`)
338
				$req['join']['dis'] = sprintf('INNER JOIN del_image_stat dis'.
1604 jpm 339
					 ' ON vdi.id_image = dis.ce_image'.
340
					 ' AND dis.ce_protocole = %d',
341
					 $p['protocole']);
342
			} else {
1584 mathias 343
				$req['join']['dis'] = sprintf('LEFT JOIN del_image_stat dis'.
1604 jpm 344
					 ' ON vdi.id_image = dis.ce_image'.
345
					 ' AND dis.ce_protocole = %d',
346
					 $p['protocole']);
1584 mathias 347
				// nécessaire (dup ce_image dans del_image_stat)
348
				$req['groupby'][] = 'vdi.id_observation';
1604 jpm 349
			}
1584 mathias 350
		}
1604 jpm 351
 
352
		if ($p['tri'] == 'tags') {
353
			$req['join'][] = sprintf('%s JOIN del_image_stat dis ON vdi.id_image = dis.ce_image',
354
				($p['ordre'] == 'desc') ? 'INNER' : 'LEFT');
355
			// nécessaire (dup ce_image dans del_image_stat)
356
			$req['groupby'][] = 'vdi.id_observation';
1584 mathias 357
		}
1604 jpm 358
 
1584 mathias 359
		// car il ne sont pas traités par la générique requestFilterParams() les clefs "masque.tag_*"
360
		// sont toujours présentes; bien que parfois NULL.
1604 jpm 361
		if ($p['masque.tag_cel']) {
362
			if (isset($p['masque.tag_cel']['AND'])) {
363
				// TODO: utiliser les tables de mots clefs normaliées dans tb_cel ?
364
				// et auquel cas laisser au client le choix du couteux "%" ?
365
				$tags = $p['masque.tag_cel']['AND'];
366
				array_walk($tags, create_function('&$val, $k, $db',
367
					'$val = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) LIKE %s",
368
					$db->proteger("%".$val."%"));'),
369
					$db);
370
				$req['where'][] = '(' . implode(' AND ', $tags) . ')';
371
			} else {
372
				$req['where'][] = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) REGEXP %s",
373
					$db->proteger(implode('|', $p['masque.tag_cel']['OR'])));
374
			}
1584 mathias 375
		}
1604 jpm 376
 
377
		if ($p['masque.tag_pictoflora']) {
378
			// inutilisable pour l'instant
379
			// self::sqlAddPictoFloraTagConstraint1($p, $db, $req);
380
 
381
			// intéressante, mais problème d'optimiseur MySQL 5.5 (dependant subquery)
382
			// self::sqlAddPictoFloraTagConstraint2($p, $db, $req);
383
 
384
			// approche fiable mais sous-optimale
385
			self::sqlAddPictoFloraTagConstraint3($p, $db, $req);
1584 mathias 386
		}
1604 jpm 387
	}
1486 raphael 388
 
1604 jpm 389
	/* approche intéressante si les deux problèmes suivants peuvent être résolu:
390
		- LEFT JOIN => dup => *gestion de multiples GROUP BY* (car in-fine un LIMIT est utilisé)
391
		- dans le cas d'un ET logique, comment chercher les observations correspondantes ? */
392
	static function sqlAddPictoFloraTagConstraint1($p, $db, &$req) {
393
		// XXX: utiliser tag plutôt que tag_normalise ?
394
		$req['join'][] = 'LEFT JOIN del_image_tag dit ON dit.ce_image = vdi.id_image';
395
		$req['where'][] = 'dit.actif = 1';
396
		$req['groupby'][] = 'vdi.id_image'; // TODO: nécessaire (car dup') mais risque de conflict en cas de tri (multiple GROUP BY)
397
		// XXX: en cas de ET, possibilité du GROUP_CONCAT(), mais probablement sans grand intérêt, d'où une boucle
398
		if (isset($p['masque.tag_pictoflora']['AND'])) {
399
			// TODO/XXX : comment matcher les observations ayant tous les mots-clef passés ?
400
			// ... le LEFT-JOIN n'y semble pas adapté
401
		} else {
402
			$protected_tags = array();
403
			foreach ($p['masque.tag_pictoflora']['OR'] as $tag) {
404
				$protected_tags[] = $db->proteger(strtolower($tag));
405
			}
406
			$req['where'][] = sprintf('tag_normalise IN (%s)', implode(',', $protected_tags));
407
		}
1495 raphael 408
	}
1486 raphael 409
 
1604 jpm 410
	// inutilisé pour l'instant pour cause de soucis d'optimiseur MySQL (cf commentaire en intro)
411
	static function sqlAddPictoFloraTagConstraint2($p, $db, &$req) {
412
		// Note à propos des 4 "@ instruction" ci-dessous (notamment sur recupererTous())
413
		// REGEXP permet un puissant mécanisme de sélection des obs/image à qui sait
414
		// l'utiliser, mais peut sortir une erreur en cas de REGEXP invalide
415
		// ex: REGEX "^(".
416
		// Pour l'heure nous ignorons ce type d'erreur car aucun de nos champ de recherche
417
		// ne peuvent (ou ne devrait) comporter des meta-caractères
418
		// ([])?*+\\
419
		if (isset($p['masque.tag_pictoflora']['AND'])) {
420
			// optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
421
			// donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
422
			sort($p['masque.tag_pictoflora']['AND']);
423
			$req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
424
				" GROUP BY ce_image".
425
				" HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s)",
426
				$db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND'])));
427
		} else {
428
			$req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
429
				" GROUP BY ce_image".
430
				" HAVING GROUP_CONCAT(tag_normalise) REGEXP %s)",
431
				$db->proteger(implode('|', $p['masque.tag_pictoflora']['OR'])));
432
		}
1495 raphael 433
	}
1491 raphael 434
 
1604 jpm 435
	// si l'on est bassiné par les "DEPENDENT SUBQUERY", nous la faisons donc indépendemment via cette fonction
436
	static function sqlAddPictoFloraTagConstraint3($p, $db, &$req) {
437
		if (isset($p['masque.tag_pictoflora']['AND'])) {
438
			// optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
439
			// donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
440
			sort($p['masque.tag_pictoflora']['AND']);
1486 raphael 441
 
1604 jpm 442
			// plutôt que db->connexion->query->fetchColumn(), une API pourrie nous oblige à ...
443
			$ids = @$db->recupererTous(sprintf(
444
			"SELECT ce_image FROM del_image_tag WHERE actif = 1".
445
			" GROUP BY ce_image".
446
			" HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s",
447
			$db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND']))));
1486 raphael 448
 
1604 jpm 449
			// puis:
450
			$ids = @array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
451
			$ids = !empty($ids) ? implode(',', $ids) : 'SELECT ce_image FROM del_image_tag  WHERE false';
452
			$req['where'][] = sprintf("vdi.id_image IN (%s)", $ids);
453
		} else {
454
			$ids = @$db->recupererTous(sprintf(
455
				"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_pictoflora']['OR']))));
1491 raphael 459
 
1604 jpm 460
			$ids = @array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
461
			$ids = !empty($ids) ? implode(',', $ids) : 'SELECT ce_image FROM del_image_tag  WHERE false';
462
			$req['where'][] = sprintf("vdi.id_image IN (%s)", $ids);
463
		}
1495 raphael 464
	}
1486 raphael 465
 
1604 jpm 466
	static function getIdImages($p, $req, $db) {
467
		return $db->recupererTous(sprintf(
468
			'SELECT SQL_CALC_FOUND_ROWS id_image' .
469
			//', dis.moyenne, dis.nb_points, dis.nb_votes' . // debug
470
			' FROM v_del_image vdi'.
471
			' %s' . // LEFT JOIN if any
472
			' WHERE %s'. // where-clause ou TRUE
473
			' %s'. // group-by
474
			' ORDER BY %s'.
475
			' LIMIT %d, %d -- %s',
476
 
477
			$req['join'] ? implode(' ', array_unique($req['join'])) : '',
478
			$req['where'] ? implode(' AND ', $req['where']) : 'TRUE',
479
 
480
			$req['groupby'] ? ('GROUP BY ' . implode(', ', array_unique($req['groupby']))) : '',
481
 
482
			$req['orderby'],
483
 
484
			$p['navigation.depart'], $p['navigation.limite'], __FILE__ . ':' . __LINE__));
1422 raphael 485
	}
486
 
1604 jpm 487
	static function chargerImages($db, $idImg) {
1564 mathias 488
		$obs_fields = DelTk::sqlFieldsToAlias(self::$mappings['observations'], NULL);
489
		$image_fields = DelTk::sqlFieldsToAlias(self::$mappings['images'], NULL);
1604 jpm 490
 
1564 mathias 491
		return $db->recupererTous(sprintf('SELECT '.
1604 jpm 492
			' CONCAT(id_image, "-", id_observation) AS jsonindex,'.
493
			' %1$s, %2$s FROM v_del_image '.
494
			' WHERE %3$s'.
495
			' ORDER BY %4$s'. // important car MySQL ne conserve par l'ordre du IN()
496
			' -- %5$s',
497
			$obs_fields, $image_fields,
498
			sprintf('id_image IN (%s)', implode(',', $idImg)),
499
			sprintf('FIELD(id_image, %s)', implode(',', $idImg)),
500
			__FILE__ . ':' . __LINE__));
501
	}
1422 raphael 502
 
1604 jpm 503
	/* "masque" ne fait jamais que faire une requête sur la plupart des champs, (presque) tous traités
504
		de manière identique à la seule différence que:
505
		1) ils sont combinés par des "OU" logiques plutôt que des "ET".
506
		2) les tags sont traités différemment pour conserver la compatibilité avec l'utilisation historique:
507
		Tous les mots-clefs doivent matcher et sont séparés par des espaces
508
		(dit autrement, str_replace(" ", ".*", $mots-clefs-requête) =~ $mots-clefs-mysql)
509
		Pour plus d'information: (ListeObservations|DelTk)::sqlAddMasqueConstraint() */
510
	static function sqlAddMasqueConstraint($p, $db, &$req, Conteneur $c = NULL) {
511
		if (!empty($p['masque'])) {
512
			$or_params = array('masque.auteur' => $p['masque'],
513
				'masque.departement' => $p['masque'],
514
				'masque.commune' => $p['masque'], // TODO/XXX ?
515
				'masque.id_zone_geo' => $p['masque'],
1422 raphael 516
 
1604 jpm 517
				/* tous-deux remplacent masque.tag
518
				mais sont traité séparément des requestFilterParams() */
519
				// 'masque.tag_cel' => $p['masque'],
520
				// 'masque.tag_pictoflora' => $p['masque'],
521
 
522
				'masque.ns' => $p['masque'],
523
				'masque.famille' => $p['masque'],
524
				'masque.date' => $p['masque'],
525
				'masque.genre' => $p['masque'],
526
				'masque.milieu' => $p['masque'],
527
				'masque.tag_cel' => $p['masque'],
528
				'masque.tag_pictoflora' => $p['masque'],
529
 
530
				// tri est aussi nécessaire car affecte les contraintes de JOIN
531
				'tri' => $p['tri'],
532
				'ordre' => $p['ordre']);
533
			if (array_key_exists('protocole', $p)) {
534
				$or_params['protocole'] = $p['protocole'];
535
			}
536
 
537
			/* Cependant les champs spécifiques ont priorité sur le masque général.
538
				Pour cette raison nous supprimons la génération de SQL du masque général sur les
539
				champ spécifiques qui feront l'objet d'un traitement avec une valeur propre. */
540
			if(isset($p['masque.auteur'])) unset($or_params['masque.auteur']);
541
			if(isset($p['masque.departement'])) unset($or_params['masque.departement']);
542
			if(isset($p['masque.commune'])) unset($or_params['masque.commune']);
543
			if(isset($p['masque.id_zone_geo'])) unset($or_params['masque.id_zone_geo']);
544
			if(isset($p['masque.ns'])) unset($or_params['masque.ns']);
545
			if(isset($p['masque.famille'])) unset($or_params['masque.famille']);
546
			if(isset($p['masque.date'])) unset($or_params['masque.date']);
547
			if(isset($p['masque.genre'])) unset($or_params['masque.genre']);
548
			if(isset($p['masque.milieu'])) unset($or_params['masque.milieu']);
549
			if(isset($p['masque.tag_cel'])) unset($or_params['masque.tag_cel']);
550
			if(isset($p['masque.tag_pictoflora'])) unset($or_params['masque.tag_pictoflora']);
551
 
552
			$or_masque = array_merge(
1584 mathias 553
				DelTk::requestFilterParams($or_params, NULL, $c /* pour masque.departement */),
554
				self::requestFilterParams($or_params)
1604 jpm 555
			);
556
 
557
			/* Lorsqu'on utilise le masque général pour chercher des tags, ils sont
558
				postulés comme séparés par des espaces, et doivent être tous matchés. */
559
			if (isset($or_params['masque.tag_cel'])) {
560
				$or_masque['masque.tag_cel'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
561
			}
562
			if (isset($or_params['masque.tag_pictoflora'])) {
563
				$or_masque['masque.tag_pictoflora'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
564
			}
565
 
566
			// pas de select, groupby & co ici: uniquement 'join' et 'where'
567
			$or_req = array('join' => array(), 'where' => array());
568
			DelTk::sqlAddConstraint($or_masque, $db, $or_req);
569
 
570
			self::sqlAddConstraint($or_masque, $db, $or_req);
571
 
572
			if ($or_req['where']) {
573
				$req['where'][] = '(' . implode(' OR ', $or_req['where']) . ')';
574
				// utile au cas ou des jointures seraient rajoutées
575
				$req['join'] = array_unique(array_merge($req['join'], $or_req['join']));
576
			}
1584 mathias 577
		}
1604 jpm 578
	}
1422 raphael 579
 
1490 raphael 580
 
1604 jpm 581
	// cf Observation::reformateObservationSimpleIndex() et ListeObservations::reformateObservation()
582
	// (trop de variétés de formatage, à unifier côté client pour unifier côté backend ...)
583
	static function reformateImagesDoubleIndex($obs, $url_pattern = '', $image_format = 'XL') {
1564 mathias 584
		// XXX: cf Observation.php::consulter(), nous pourriouns ici
585
		// conserver les valeurs vides (pour les phptests notamment, ou non)
586
		// $obs = array_map('array_filter', $obs);
587
		$obs_merged = $obs_keyed_by_id_image = array();
1604 jpm 588
		foreach ($obs as $o) {
589
			// ceci nous complique la tâche pour le reste du processing...
590
			$id = $o['jsonindex'];
591
			// ainsi nous utilisons deux tableaux: le final, indexé par couple d'id(image-obs)
592
			// et celui indexé par simple id_image qui est fort utile pour mapVotesToImages()
593
			// mais tout deux partage leur référence à "protocole"
594
			$image = array(
595
				'id_image' => $o['id_image'],
1564 mathias 596
				'binaire.href' => sprintf($url_pattern, $o['id_image'], $image_format),
597
				'mots_cles_texte' => @$o['i_mots_cles_texte'], // @, peut avoir été filtré par array_map() ci-dessus
1604 jpm 598
			);
599
 
600
			unset($o['id_image'], $o['i_mots_cles_texte'], $o['jsonindex']);
601
			if (!isset($obs_merged[$id])) {
602
				$obs_merged[$id] = $image;
603
			}
604
			$obs_merged[$id]['observation'] = $o;
605
			$obs_merged[$id]['protocoles_votes'] = array();
606
 
607
			$obs_keyed_by_id_image[$image['id_image']]['protocoles_votes'] =& $obs_merged[$id]['protocoles_votes'];
1564 mathias 608
		}
1604 jpm 609
 
1564 mathias 610
		return array($obs_merged,$obs_keyed_by_id_image);
1604 jpm 611
	}
1490 raphael 612
 
1604 jpm 613
	// complete & override DelTk::requestFilterParams() (même usage)
614
	static function requestFilterParams(Array $params, $parametres_autorises = NULL) {
615
		if ($parametres_autorises) { // filtrage de toute clef inconnue
616
			$params = array_intersect_key($params, array_flip($parametres_autorises));
617
		}
1490 raphael 618
 
1584 mathias 619
		$p = array();
620
		$p['tri'] = DelTk::unsetIfInvalid($params, 'tri', self::$tri_possible);
621
		$p['format'] = DelTk::unsetIfInvalid($params, 'format', self::$format_image_possible);
1604 jpm 622
 
1584 mathias 623
		// "milieu" inutile pour IdentiPlantes ?
1604 jpm 624
		if (isset($params['masque.milieu'])) {
625
			$p['masque.milieu'] = trim($params['masque.milieu']);
626
		}
627
 
1584 mathias 628
		// compatibilité
1604 jpm 629
		if (isset($params['masque.tag'])) {
630
			$params['masque.tag_cel'] = $params['masque.tag_pictoflora'] = $params['masque.tag'];
1584 mathias 631
		}
1604 jpm 632
 
633
		if ($p['tri'] == 'votes' || $p['tri'] == 'tags' || $p['tri'] == 'points') {
634
			// ces critère de tri des image à privilégier ne s'applique qu'à un protocole donné
635
			if(!isset($params['protocole']) || !is_numeric($params['protocole'])) {
1584 mathias 636
				$p['protocole'] = self::$default_proto;
1604 jpm 637
			} else {
1584 mathias 638
				$p['protocole'] = intval($params['protocole']);
1604 jpm 639
			}
1584 mathias 640
		}
1604 jpm 641
 
1584 mathias 642
		return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
1604 jpm 643
	}
1422 raphael 644
 
1486 raphael 645
}
1604 jpm 646
?>