Subversion Repositories eFlore/Applications.del

Rev

Rev 1705 | Rev 1827 | 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",
1666 jpm 116
			"prenom_utilisateur" => "auteur.prenom",
117
			"courriel_utilisateur" => "auteur.courriel",),
1604 jpm 118
		'images' => array( // v_del_image
119
			'id_image' => 1,
120
			// l'alias suivant est particulier: in-fine il doit s'appeler mots_cles_texte
121
			// mais nous afin d'éviter un conflit d'alias nous le renommons plus tard (reformateImagesDoubleIndex)
122
			'i_mots_cles_texte' => 1)
123
	);
1422 raphael 124
 
1604 jpm 125
	public function __construct(Conteneur $conteneur = null) {
1564 mathias 126
		$this->conteneur = $conteneur == null ? new Conteneur() : $conteneur;
127
		$this->conteneur->chargerConfiguration('config_images.ini');
1604 jpm 128
		$this->bdd = $conteneur->getGestionBdd()->getBdd();
129
	}
1390 raphael 130
 
1604 jpm 131
	public function consulter($ressources, $parametres) {
132
		/* Certes nous sélectionnons ici (nom|prenom|courriel)_utilisateur de cel_obs, mais il ne nous intéressent pas
133
		Par contre, ci-dessous nous prenons i_(nom|prenom|courriel)_utilisateur.
134
		Notons cependant qu'aucun moyen ne devrait permettre que i_*_utilisateur != *_utilisateur
135
		Le propriétaire d'une obs et de l'image associée est *toujours* le même. */
136
		array_walk(self::$mappings['observations'], create_function('&$val, $k', 'if($val==1) $val = $k;'));
137
		array_walk(self::$mappings['images'], create_function('&$val, $k', 'if($val==1) $val = $k;'));
138
		// pour les votes, les mappings de "Observation" nous suffisent
139
		array_walk(Observation::$mappings['votes'], create_function('&$val, $k', 'if($val==1) $val = $k;'));
1390 raphael 140
 
1604 jpm 141
		// la nécessité du 'groupby' dépend des 'join's utilisés (LEFT ou INNER) ainsi que de la cardinalité
142
		// de `ce_image` dans ces tables jointes.
143
		// Contrairement à IdentiPlantes, nous n'avons de HAVING pour PictoFlora, mais par contre un ORDER BY
144
		$req = array('select' => array(), 'join' => array(), 'where' => array(), 'groupby' => array(), 'orderby' => array());
1422 raphael 145
 
1604 jpm 146
		$db = $this->bdd;
1422 raphael 147
 
1604 jpm 148
		// filtrage de l'INPUT général, on réutilise 90% de identiplante en terme de paramètres autorisés
149
		// ($parametres_autorises) sauf... masque.type qui fait des modif' de WHERE sur les mots-clefs.
150
		// Évitons ce genre de chose pour PictoFlora et les risques de conflits avec masque.tag
151
		// même si ceux-ci sont improbables (pas d'<input> pour cela).
152
		$params_ip = DelTk::requestFilterParams($parametres,
153
			array_diff(DelTk::$parametres_autorises, array('masque.type')),
154
			$this->conteneur);
1390 raphael 155
 
1604 jpm 156
		// notre propre filtrage sur l'INPUT
157
		$params_pf = self::requestFilterParams($parametres,
158
			array_merge(DelTk::$parametres_autorises, self::$parametres_autorises));
1422 raphael 159
 
1604 jpm 160
		/* filtrage des tags + sémantique des valeurs multiples:
161
		Lorsqu'on utilise masque.tag* pour chercher des tags, ils sont
162
		postulés comme séparés par des virgule, et l'un au moins des tags doit matcher. */
163
		$params_pf['masque.tag_cel'] = DelTk::buildTagsAST(@$parametres['masque.tag_cel'], 'OR', ',');
1390 raphael 164
 
1604 jpm 165
		if(!isset($parametres['masque.tag_pictoflora']) && isset($parametres['masque.tag'])) {
166
			$parametres['masque.tag_pictoflora'] = $parametres['masque.tag'];
167
		}
168
		$params_pf['masque.tag_pictoflora'] = DelTk::buildTagsAST(@$parametres['masque.tag_pictoflora'], 'OR', ',');
1390 raphael 169
 
1604 jpm 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
 
1604 jpm 176
		if (isset($parametres['format'])) {
177
			$params['format'] = $parametres['format'];
178
		}
1422 raphael 179
 
1604 jpm 180
		// création des contraintes (génériques de DelTk)
181
		DelTk::sqlAddConstraint($params, $db, $req);
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
 
1604 jpm 189
		// 1) grunt-work: *la* requête de récupération des id valides (+ SQL_CALC_FOUND_ROWS)
190
		// $idobs_tab = ListeObservations::getIdObs($params, $req, $db);
191
		$idobs_tab = self::getIdImages($params, $req, $db);
1390 raphael 192
 
1604 jpm 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) {
195
			$resultat = new ResultatService();
196
			$resultat->corps = array('entete' => DelTk::makeJSONHeader(0, $params, Config::get('url_service')),
197
				'resultats' => array());
198
			return $resultat;
199
		}
1390 raphael 200
 
1604 jpm 201
		// idobs est une liste (toujours ordonnée) des id d'observations recherchées
202
		$idobs = array_values(array_map(create_function('$a', 'return $a["id_image"];'), $idobs_tab));
203
		$total = $db->recuperer('SELECT FOUND_ROWS() AS c'); $total = intval($total['c']);
1390 raphael 204
 
1604 jpm 205
		$liaisons = self::chargerImages($db, $idobs);
1390 raphael 206
 
1604 jpm 207
		list($images, $images_keyed_by_id_image) = self::reformateImagesDoubleIndex(
208
			$liaisons,
209
			$this->conteneur->getParametre('images.url_images'),
210
			$params['format']);
1422 raphael 211
 
1604 jpm 212
		// on charge les votes pour ces images et pour *tous* les protocoles
213
		$votes = Observation::chargerVotesImage($db, $liaisons, NULL);
1422 raphael 214
 
1604 jpm 215
		// subtilité, nous passons ici le tableau d'images indexé par id_image qui est bien plus pratique pour
216
		// associer les vote à un tableau, puisque nous ne connaissons pas les id d'observation.
217
		// Mais magiquement (par référence), cela va remplir notre tableau indexé par couple d'id (id_image, id_observation)
218
		// cf reformateImagesDoubleIndex() à qui revient la tâche de créer ces deux versions simultanément lorsque
219
		// c'est encore possible.
220
		if ($votes) {
221
			Observation::mapVotesToImages($votes, $images_keyed_by_id_image);
222
		}
1422 raphael 223
 
1604 jpm 224
		// les deux masques de tags sont transformés en AST dans le processus de construction de la requête.
225
		// Reprenous les paramètres originaux non-nettoyés (ils sont valables car le nettoyage est déterministe)
226
		$params_header = array_merge($params, array_filter(array('masque.tag_cel' => @$parametres['masque.tag_cel'],
227
			'masque.tag_pictoflora' => @$parametres['masque.tag_pictoflora'])));
228
		$resultat = new ResultatService();
229
		$resultat->corps = array(
230
			'entete' => DelTk::makeJSONHeader($total, $params_header, Config::get('url_service')),
231
			'resultats' => $images);
232
		return $resultat;
233
	}
1495 raphael 234
 
1690 jpm 235
 
236
	/**
237
	 * Supprime une image directement dans le CEL en faisant un appel à un web service du CEL.
238
	 * Utilisé uniquement par les admins.
239
	 *
240
	 * @param array		$ressources tableau des informations contenues dans l'url après le nom du service
241
	 * @param array		$parametres contenu du post
242
	 * @return mixed	Chaine "OK" (en majuscule) en cas de succès, booléen "false" en cas d'échec
243
	 */
1604 jpm 244
	public function supprimer($ressources) {
245
		$controlAcces = $this->conteneur->getControleAcces();
1690 jpm 246
		$controlAcces->etreUtilisateurAvecDroitAdmin();
1604 jpm 247
 
1690 jpm 248
		$urlServiceBase = $this->conteneur->getParametre('urlServiceCelImage');
249
		$idImage = $ressources[0];
250
		$url = $urlServiceBase.$idImage;
1604 jpm 251
 
1690 jpm 252
		$clientHttp = $this->conteneur->getRestClient();
253
		$retourCel = $clientHttp->supprimer($url);
254
		$retour = preg_match('/^OK$/i', $retourCel) ? 'OK' : false;
1604 jpm 255
		return $retour;
256
	}
257
 
258
	/**
259
	 * TODO: partie spécifique liées à la complexité de PictoFlora:
260
	 * génération de la clause ORDER BY (génère la valeur de la clef orderby' de $req)
261
	 * nécessaire ? tableau sprintf(key (tri) => value (ordre), key => value ...).
262
	 * Cependant il est impensable de joindre sur un AVG() des valeurs des votes pour
263
	 * *chaque* couple (id_image, protocole) de la base afin de trouver les images
264
	 * les "mieux notées", ou bien les images ayant le "plus de tags" (COUNT())
265
	 */
266
	static function sqlOrderBy($p, $db, &$req) {
1564 mathias 267
		// parmi self::$tri_possible
1604 jpm 268
		if ($p['tri'] == 'votes') { // LEFT JOIN sur "dis" ci-dessous
269
			$req['orderby'] = 'dis.moyenne ' . $p['ordre'] . ', dis.nb_votes ' . $p['ordre'];
270
			return;
1564 mathias 271
		}
1604 jpm 272
 
273
		if ($p['tri'] == 'points') { // LEFT JOIN sur "dis" ci-dessous
274
			$req['orderby'] = 'dis.nb_points ' . $p['ordre'] . ', dis.moyenne ' . $p['ordre'] . ', dis.nb_votes ' . $p['ordre'];
275
			return;
1564 mathias 276
		}
1604 jpm 277
 
278
		if ($p['tri'] == 'tags') { // LEFT JOIN sur "dis" ci-dessous
279
			$req['orderby'] = 'dis.nb_tags ' . $p['ordre'];
280
			return;
1564 mathias 281
		}
1604 jpm 282
 
283
		if ($p['tri'] == 'date_observation') {
284
			$req['orderby'] = 'date_observation ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
285
			return;
1564 mathias 286
		}
1604 jpm 287
 
1564 mathias 288
		// tri == 'date_transmission'
289
		// avant cel:r1860, date_transmission pouvait être NULL
290
		// or nous voulons de la cohérence (notamment pour phpunit)
291
		$req['orderby'] = 'date_transmission ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
1604 jpm 292
	}
1495 raphael 293
 
1604 jpm 294
	/*
295
	 * in $p: un tableau de paramètres, dont:
296
	 * - 'masque.tag_cel': *tableau* de mots-clefs à chercher parmi cel_image.mots_clefs_texte
297
	 * - 'masque.tag_pictoflora': *tableau* de mots-clefs à chercher parmi del_image_tag.tag_normalise
298
	 * - 'tag_explode_semantic': défini si les éléments sont tous recherchés ou NON
299
	 *
300
	 * in/ou: $req: un tableau de structure de requête MySQL
301
	 *
302
	 * Attention, le fait que nous cherchions masque.tag_cel OU/ET masque.tag_cel
303
	 * ne dépend pas de nous, mais du niveau supérieur de construction de la requête:
304
	 * Soit directement $this->consulter() si des masque.tag* sont passés
305
	 * (split sur ",", "AND" entre chaque condition, "OR" pour chaque valeur de tag)
306
	 * Soit via sqlAddMasqueConstraint():
307
	 * (pas de split, "OR" entre chaque condition) [ comportement historique ]
308
	 * équivalent à:
309
	 * (split sur " ", "OR" entre chaque condition, "AND" pour chaque valeur de tag)
310
	 *
311
	 */
312
	static function sqlAddConstraint($p, $db, &$req, Conteneur $c = NULL) {
1584 mathias 313
		// TODO implement dans DelTk ?
1604 jpm 314
		if (!empty($p['masque.milieu'])) {
315
			$req['where'][] = 'vdi.milieu LIKE '.$db->proteger('%' . $p['masque.milieu'].'%');
1584 mathias 316
		}
1604 jpm 317
 
1584 mathias 318
		/* Pour le tri par AVG() des votes nous avons toujours un protocole donné,
1604 jpm 319
			celui-ci indique sur quels votes porte l'AVG.
320
			(c'est un *vote* qui porte sur un protocole et non l'image elle-même) */
1584 mathias 321
		/* TODO: perf problème:
1604 jpm 322
			1) SQL_CALC_FOUND_ROWS: fixable en:
323
				- dissociant le comptage de la récup d'id + javascript async
324
				- ou ne rafraîchir le total *que* pour les requête impliquant un changement de pagination
325
				(paramètre booléen "with-total" par exemple)
326
			2) jointure forcées: en utilisant `del_imagè`, nous forçons les 2 premiers
327
				JOIN sur cel_obs_images et cel_obs pour filtrer sur "transmission".
328
				Dénormaliser cette valeur et l'intégrer à `cel_images` ferait économiser cette couteuse
329
				jointure, ... lorsqu'aucun masque portant sur `cel_obs` n'est utilisé
330
			3) non-problème: l'ordre des joins est forcé par l'usage de la vue:
331
				(cel_images/cel_obs_images/cel_obs/del_image_stat)
332
				Cependant c'est à l'optimiseur de définir son ordre préféré. */
333
		if ($p['tri'] == 'votes' || $p['tri'] == 'points') {
334
			// $p['protocole'] *est* défini (cf requestFilterParams())
1748 mathias 335
			$req['join']['dis'] = sprintf('LEFT JOIN del_image_stat dis'.
336
				 ' ON vdi.id_image = dis.ce_image'.
337
				 ' AND dis.ce_protocole = %d',
338
				 $p['protocole']);
1584 mathias 339
		}
1604 jpm 340
 
341
		if ($p['tri'] == 'tags') {
342
			$req['join'][] = sprintf('%s JOIN del_image_stat dis ON vdi.id_image = dis.ce_image',
343
				($p['ordre'] == 'desc') ? 'INNER' : 'LEFT');
344
			// nécessaire (dup ce_image dans del_image_stat)
345
			$req['groupby'][] = 'vdi.id_observation';
1584 mathias 346
		}
1604 jpm 347
 
1584 mathias 348
		// car il ne sont pas traités par la générique requestFilterParams() les clefs "masque.tag_*"
349
		// sont toujours présentes; bien que parfois NULL.
1604 jpm 350
		if ($p['masque.tag_cel']) {
351
			if (isset($p['masque.tag_cel']['AND'])) {
352
				// TODO: utiliser les tables de mots clefs normaliées dans tb_cel ?
353
				// et auquel cas laisser au client le choix du couteux "%" ?
354
				$tags = $p['masque.tag_cel']['AND'];
355
				array_walk($tags, create_function('&$val, $k, $db',
1655 mathias 356
					'$val = sprintf("CONCAT(IFNULL(vdi.mots_cles_texte,\'\'),IFNULL(vdi.i_mots_cles_texte,\'\')) LIKE %s",
1604 jpm 357
					$db->proteger("%".$val."%"));'),
358
					$db);
359
				$req['where'][] = '(' . implode(' AND ', $tags) . ')';
360
			} else {
1669 mathias 361
				$req['where'][] = sprintf("CONCAT(IFNULL(vdi.mots_cles_texte,''),IFNULL(vdi.i_mots_cles_texte,'')) REGEXP %s",
1604 jpm 362
					$db->proteger(implode('|', $p['masque.tag_cel']['OR'])));
363
			}
1584 mathias 364
		}
1604 jpm 365
 
366
		if ($p['masque.tag_pictoflora']) {
367
			// inutilisable pour l'instant
368
			// self::sqlAddPictoFloraTagConstraint1($p, $db, $req);
369
 
370
			// intéressante, mais problème d'optimiseur MySQL 5.5 (dependant subquery)
371
			// self::sqlAddPictoFloraTagConstraint2($p, $db, $req);
372
 
373
			// approche fiable mais sous-optimale
374
			self::sqlAddPictoFloraTagConstraint3($p, $db, $req);
1584 mathias 375
		}
1604 jpm 376
	}
1486 raphael 377
 
1604 jpm 378
	/* approche intéressante si les deux problèmes suivants peuvent être résolu:
379
		- LEFT JOIN => dup => *gestion de multiples GROUP BY* (car in-fine un LIMIT est utilisé)
380
		- dans le cas d'un ET logique, comment chercher les observations correspondantes ? */
381
	static function sqlAddPictoFloraTagConstraint1($p, $db, &$req) {
382
		// XXX: utiliser tag plutôt que tag_normalise ?
383
		$req['join'][] = 'LEFT JOIN del_image_tag dit ON dit.ce_image = vdi.id_image';
384
		$req['where'][] = 'dit.actif = 1';
385
		$req['groupby'][] = 'vdi.id_image'; // TODO: nécessaire (car dup') mais risque de conflict en cas de tri (multiple GROUP BY)
386
		// XXX: en cas de ET, possibilité du GROUP_CONCAT(), mais probablement sans grand intérêt, d'où une boucle
387
		if (isset($p['masque.tag_pictoflora']['AND'])) {
388
			// TODO/XXX : comment matcher les observations ayant tous les mots-clef passés ?
389
			// ... le LEFT-JOIN n'y semble pas adapté
390
		} else {
391
			$protected_tags = array();
392
			foreach ($p['masque.tag_pictoflora']['OR'] as $tag) {
393
				$protected_tags[] = $db->proteger(strtolower($tag));
394
			}
395
			$req['where'][] = sprintf('tag_normalise IN (%s)', implode(',', $protected_tags));
396
		}
1495 raphael 397
	}
1486 raphael 398
 
1604 jpm 399
	// inutilisé pour l'instant pour cause de soucis d'optimiseur MySQL (cf commentaire en intro)
400
	static function sqlAddPictoFloraTagConstraint2($p, $db, &$req) {
401
		// Note à propos des 4 "@ instruction" ci-dessous (notamment sur recupererTous())
402
		// REGEXP permet un puissant mécanisme de sélection des obs/image à qui sait
403
		// l'utiliser, mais peut sortir une erreur en cas de REGEXP invalide
404
		// ex: REGEX "^(".
405
		// Pour l'heure nous ignorons ce type d'erreur car aucun de nos champ de recherche
406
		// ne peuvent (ou ne devrait) comporter des meta-caractères
407
		// ([])?*+\\
408
		if (isset($p['masque.tag_pictoflora']['AND'])) {
409
			// optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
410
			// donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
411
			sort($p['masque.tag_pictoflora']['AND']);
412
			$req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
413
				" GROUP BY ce_image".
414
				" HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s)",
415
				$db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND'])));
416
		} else {
417
			$req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
418
				" GROUP BY ce_image".
419
				" HAVING GROUP_CONCAT(tag_normalise) REGEXP %s)",
420
				$db->proteger(implode('|', $p['masque.tag_pictoflora']['OR'])));
421
		}
1495 raphael 422
	}
1491 raphael 423
 
1604 jpm 424
	// si l'on est bassiné par les "DEPENDENT SUBQUERY", nous la faisons donc indépendemment via cette fonction
425
	static function sqlAddPictoFloraTagConstraint3($p, $db, &$req) {
426
		if (isset($p['masque.tag_pictoflora']['AND'])) {
427
			// optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
428
			// donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
429
			sort($p['masque.tag_pictoflora']['AND']);
1486 raphael 430
 
1604 jpm 431
			// plutôt que db->connexion->query->fetchColumn(), une API pourrie nous oblige à ...
432
			$ids = @$db->recupererTous(sprintf(
433
			"SELECT ce_image FROM del_image_tag WHERE actif = 1".
434
			" GROUP BY ce_image".
435
			" HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s",
436
			$db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND']))));
1486 raphael 437
 
1604 jpm 438
			// puis:
439
			$ids = @array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
440
			$ids = !empty($ids) ? implode(',', $ids) : 'SELECT ce_image FROM del_image_tag  WHERE false';
441
			$req['where'][] = sprintf("vdi.id_image IN (%s)", $ids);
442
		} else {
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) REGEXP %s",
447
				$db->proteger(implode('|', $p['masque.tag_pictoflora']['OR']))));
1491 raphael 448
 
1604 jpm 449
			$ids = @array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
450
			$ids = !empty($ids) ? implode(',', $ids) : 'SELECT ce_image FROM del_image_tag  WHERE false';
451
			$req['where'][] = sprintf("vdi.id_image IN (%s)", $ids);
452
		}
1495 raphael 453
	}
1486 raphael 454
 
1604 jpm 455
	static function getIdImages($p, $req, $db) {
1669 mathias 456
		$req = sprintf(
1604 jpm 457
			'SELECT SQL_CALC_FOUND_ROWS id_image' .
458
			//', dis.moyenne, dis.nb_points, dis.nb_votes' . // debug
459
			' FROM v_del_image vdi'.
460
			' %s' . // LEFT JOIN if any
461
			' WHERE %s'. // where-clause ou TRUE
462
			' %s'. // group-by
463
			' ORDER BY %s'.
464
			' LIMIT %d, %d -- %s',
465
 
466
			$req['join'] ? implode(' ', array_unique($req['join'])) : '',
467
			$req['where'] ? implode(' AND ', $req['where']) : 'TRUE',
468
 
469
			$req['groupby'] ? ('GROUP BY ' . implode(', ', array_unique($req['groupby']))) : '',
470
 
471
			$req['orderby'],
472
 
1669 mathias 473
			$p['navigation.depart'], $p['navigation.limite'], __FILE__ . ':' . __LINE__);
474
		return $db->recupererTous($req);
1422 raphael 475
	}
476
 
1604 jpm 477
	static function chargerImages($db, $idImg) {
1564 mathias 478
		$obs_fields = DelTk::sqlFieldsToAlias(self::$mappings['observations'], NULL);
479
		$image_fields = DelTk::sqlFieldsToAlias(self::$mappings['images'], NULL);
1604 jpm 480
 
1564 mathias 481
		return $db->recupererTous(sprintf('SELECT '.
1604 jpm 482
			' CONCAT(id_image, "-", id_observation) AS jsonindex,'.
483
			' %1$s, %2$s FROM v_del_image '.
484
			' WHERE %3$s'.
485
			' ORDER BY %4$s'. // important car MySQL ne conserve par l'ordre du IN()
486
			' -- %5$s',
487
			$obs_fields, $image_fields,
488
			sprintf('id_image IN (%s)', implode(',', $idImg)),
489
			sprintf('FIELD(id_image, %s)', implode(',', $idImg)),
490
			__FILE__ . ':' . __LINE__));
491
	}
1422 raphael 492
 
1604 jpm 493
	/* "masque" ne fait jamais que faire une requête sur la plupart des champs, (presque) tous traités
494
		de manière identique à la seule différence que:
495
		1) ils sont combinés par des "OU" logiques plutôt que des "ET".
496
		2) les tags sont traités différemment pour conserver la compatibilité avec l'utilisation historique:
497
		Tous les mots-clefs doivent matcher et sont séparés par des espaces
498
		(dit autrement, str_replace(" ", ".*", $mots-clefs-requête) =~ $mots-clefs-mysql)
499
		Pour plus d'information: (ListeObservations|DelTk)::sqlAddMasqueConstraint() */
500
	static function sqlAddMasqueConstraint($p, $db, &$req, Conteneur $c = NULL) {
501
		if (!empty($p['masque'])) {
502
			$or_params = array('masque.auteur' => $p['masque'],
503
				'masque.departement' => $p['masque'],
504
				'masque.commune' => $p['masque'], // TODO/XXX ?
505
				'masque.id_zone_geo' => $p['masque'],
1422 raphael 506
 
1604 jpm 507
				/* tous-deux remplacent masque.tag
508
				mais sont traité séparément des requestFilterParams() */
509
				// 'masque.tag_cel' => $p['masque'],
510
				// 'masque.tag_pictoflora' => $p['masque'],
511
 
512
				'masque.ns' => $p['masque'],
513
				'masque.famille' => $p['masque'],
514
				'masque.date' => $p['masque'],
515
				'masque.genre' => $p['masque'],
516
				'masque.milieu' => $p['masque'],
517
				'masque.tag_cel' => $p['masque'],
518
				'masque.tag_pictoflora' => $p['masque'],
519
 
520
				// tri est aussi nécessaire car affecte les contraintes de JOIN
521
				'tri' => $p['tri'],
522
				'ordre' => $p['ordre']);
523
			if (array_key_exists('protocole', $p)) {
524
				$or_params['protocole'] = $p['protocole'];
525
			}
526
 
527
			/* Cependant les champs spécifiques ont priorité sur le masque général.
528
				Pour cette raison nous supprimons la génération de SQL du masque général sur les
529
				champ spécifiques qui feront l'objet d'un traitement avec une valeur propre. */
530
			if(isset($p['masque.auteur'])) unset($or_params['masque.auteur']);
531
			if(isset($p['masque.departement'])) unset($or_params['masque.departement']);
532
			if(isset($p['masque.commune'])) unset($or_params['masque.commune']);
533
			if(isset($p['masque.id_zone_geo'])) unset($or_params['masque.id_zone_geo']);
534
			if(isset($p['masque.ns'])) unset($or_params['masque.ns']);
535
			if(isset($p['masque.famille'])) unset($or_params['masque.famille']);
536
			if(isset($p['masque.date'])) unset($or_params['masque.date']);
537
			if(isset($p['masque.genre'])) unset($or_params['masque.genre']);
538
			if(isset($p['masque.milieu'])) unset($or_params['masque.milieu']);
539
			if(isset($p['masque.tag_cel'])) unset($or_params['masque.tag_cel']);
540
			if(isset($p['masque.tag_pictoflora'])) unset($or_params['masque.tag_pictoflora']);
541
 
542
			$or_masque = array_merge(
1584 mathias 543
				DelTk::requestFilterParams($or_params, NULL, $c /* pour masque.departement */),
544
				self::requestFilterParams($or_params)
1604 jpm 545
			);
546
 
547
			/* Lorsqu'on utilise le masque général pour chercher des tags, ils sont
548
				postulés comme séparés par des espaces, et doivent être tous matchés. */
549
			if (isset($or_params['masque.tag_cel'])) {
550
				$or_masque['masque.tag_cel'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
551
			}
552
			if (isset($or_params['masque.tag_pictoflora'])) {
553
				$or_masque['masque.tag_pictoflora'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
554
			}
555
 
556
			// pas de select, groupby & co ici: uniquement 'join' et 'where'
557
			$or_req = array('join' => array(), 'where' => array());
558
			DelTk::sqlAddConstraint($or_masque, $db, $or_req);
559
 
560
			self::sqlAddConstraint($or_masque, $db, $or_req);
561
 
562
			if ($or_req['where']) {
563
				$req['where'][] = '(' . implode(' OR ', $or_req['where']) . ')';
564
				// utile au cas ou des jointures seraient rajoutées
565
				$req['join'] = array_unique(array_merge($req['join'], $or_req['join']));
566
			}
1584 mathias 567
		}
1604 jpm 568
	}
1422 raphael 569
 
1490 raphael 570
 
1604 jpm 571
	// cf Observation::reformateObservationSimpleIndex() et ListeObservations::reformateObservation()
572
	// (trop de variétés de formatage, à unifier côté client pour unifier côté backend ...)
573
	static function reformateImagesDoubleIndex($obs, $url_pattern = '', $image_format = 'XL') {
1564 mathias 574
		// XXX: cf Observation.php::consulter(), nous pourriouns ici
575
		// conserver les valeurs vides (pour les phptests notamment, ou non)
576
		// $obs = array_map('array_filter', $obs);
577
		$obs_merged = $obs_keyed_by_id_image = array();
1604 jpm 578
		foreach ($obs as $o) {
579
			// ceci nous complique la tâche pour le reste du processing...
580
			$id = $o['jsonindex'];
581
			// ainsi nous utilisons deux tableaux: le final, indexé par couple d'id(image-obs)
582
			// et celui indexé par simple id_image qui est fort utile pour mapVotesToImages()
583
			// mais tout deux partage leur référence à "protocole"
584
			$image = array(
585
				'id_image' => $o['id_image'],
1564 mathias 586
				'binaire.href' => sprintf($url_pattern, $o['id_image'], $image_format),
587
				'mots_cles_texte' => @$o['i_mots_cles_texte'], // @, peut avoir été filtré par array_map() ci-dessus
1604 jpm 588
			);
589
 
590
			unset($o['id_image'], $o['i_mots_cles_texte'], $o['jsonindex']);
591
			if (!isset($obs_merged[$id])) {
592
				$obs_merged[$id] = $image;
593
			}
594
			$obs_merged[$id]['observation'] = $o;
595
			$obs_merged[$id]['protocoles_votes'] = array();
596
 
597
			$obs_keyed_by_id_image[$image['id_image']]['protocoles_votes'] =& $obs_merged[$id]['protocoles_votes'];
1564 mathias 598
		}
1604 jpm 599
 
1564 mathias 600
		return array($obs_merged,$obs_keyed_by_id_image);
1604 jpm 601
	}
1490 raphael 602
 
1604 jpm 603
	// complete & override DelTk::requestFilterParams() (même usage)
604
	static function requestFilterParams(Array $params, $parametres_autorises = NULL) {
605
		if ($parametres_autorises) { // filtrage de toute clef inconnue
606
			$params = array_intersect_key($params, array_flip($parametres_autorises));
607
		}
1490 raphael 608
 
1584 mathias 609
		$p = array();
610
		$p['tri'] = DelTk::unsetIfInvalid($params, 'tri', self::$tri_possible);
611
		$p['format'] = DelTk::unsetIfInvalid($params, 'format', self::$format_image_possible);
1604 jpm 612
 
1584 mathias 613
		// "milieu" inutile pour IdentiPlantes ?
1604 jpm 614
		if (isset($params['masque.milieu'])) {
615
			$p['masque.milieu'] = trim($params['masque.milieu']);
616
		}
617
 
1584 mathias 618
		// compatibilité
1604 jpm 619
		if (isset($params['masque.tag'])) {
620
			$params['masque.tag_cel'] = $params['masque.tag_pictoflora'] = $params['masque.tag'];
1584 mathias 621
		}
1604 jpm 622
 
623
		if ($p['tri'] == 'votes' || $p['tri'] == 'tags' || $p['tri'] == 'points') {
624
			// ces critère de tri des image à privilégier ne s'applique qu'à un protocole donné
625
			if(!isset($params['protocole']) || !is_numeric($params['protocole'])) {
1584 mathias 626
				$p['protocole'] = self::$default_proto;
1604 jpm 627
			} else {
1584 mathias 628
				$p['protocole'] = intval($params['protocole']);
1604 jpm 629
			}
1584 mathias 630
		}
1604 jpm 631
 
1584 mathias 632
		return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
1604 jpm 633
	}
1422 raphael 634
 
1486 raphael 635
}
1604 jpm 636
?>