Subversion Repositories eFlore/Applications.del

Rev

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

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