Subversion Repositories eFlore/Applications.del

Rev

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

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