Subversion Repositories eFlore/Applications.del

Rev

Rev 1451 | Rev 1490 | Go to most recent revision | Only display areas with differences | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

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