Subversion Repositories eFlore/Applications.del

Rev

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

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