Subversion Repositories eFlore/Applications.del

Rev

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

Rev 1498 Rev 1514
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 ListeImages {
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
	
-
 
170
	
-
 
171
	if(!isset($parametres['masque.tag_pictoflora']) && isset($parametres['masque.tag'])) {
-
 
172
		$parametres['masque.tag_pictoflora'] = $parametres['masque.tag'];
-
 
173
	}
169
	$params_pf['masque.tag_pictoflora'] = DelTk::buildTagsAST(@$parametres['masque.tag_pictoflora'], 'OR', ',');
174
	$params_pf['masque.tag_pictoflora'] = DelTk::buildTagsAST(@$parametres['masque.tag_pictoflora'], 'OR', ',');
170
 
175
 
171
	$params = array_merge(
176
	$params = array_merge(
172
            DelTk::$default_params, // paramètre par défaut Identiplante
177
            DelTk::$default_params, // paramètre par défaut Identiplante
173
            self::$default_params, // paramètres par défaut PictoFlora
178
            self::$default_params, // paramètres par défaut PictoFlora
174
            $params_ip, // les paramètres passés, traités par Identiplante
179
            $params_ip, // les paramètres passés, traités par Identiplante
175
            $params_pf); // les paramètres passés, traités par PictoFlora
180
            $params_pf); // les paramètres passés, traités par PictoFlora
176
 
181
 
177
	// XXX: temp tweak
-
 
178
	/* $this->conteneur->setParametre('url_images', sprintf($this->conteneur->getParametre('images.url_images'),
182
	if(isset($parametres['format'])) {
-
 
183
		$params['format'] = $parametres['format'];
179
	   "%09d", $params['format']));*/
184
	}
180
 
185
 
181
	// création des contraintes (génériques de DelTk)
186
	// création des contraintes (génériques de DelTk)
182
	DelTk::sqlAddConstraint($params, $db, $req);
187
	DelTk::sqlAddConstraint($params, $db, $req);
183
	// création des contraintes spécifiques (sur les tags essentiellement)
188
	// création des contraintes spécifiques (sur les tags essentiellement)
184
	self::sqlAddConstraint($params, $db, $req, $this->conteneur);
189
	self::sqlAddConstraint($params, $db, $req, $this->conteneur);
185
	// création des contraintes spécifiques impliquées par le masque général
190
	// création des contraintes spécifiques impliquées par le masque général
186
	self::sqlAddMasqueConstraint($params, $db, $req, $this->conteneur);
191
	self::sqlAddMasqueConstraint($params, $db, $req, $this->conteneur);
187
	// l'ORDER BY s'avére complexe
192
	// l'ORDER BY s'avére complexe
188
	self::sqlOrderBy($params, $db, $req);
193
	self::sqlOrderBy($params, $db, $req);
189
 
194
 
190
	// 1) grunt-work: *la* requête de récupération des id valides (+ SQL_CALC_FOUND_ROWS)
195
	// 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);
196
	// $idobs_tab = ListeObservations::getIdObs($params, $req, $db);
192
	$idobs_tab = self::getIdImages($params, $req, $db);
197
	$idobs_tab = self::getIdImages($params, $req, $db);
193
 
198
 
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
199
	// 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) {
200
	if(!$idobs_tab) {
196
	    $resultat = new ResultatService();
201
	    $resultat = new ResultatService();
197
	    $resultat->corps = array('entete' => DelTk::makeJSONHeader(0, $params, Config::get('url_service')),
202
	    $resultat->corps = array('entete' => DelTk::makeJSONHeader(0, $params, Config::get('url_service')),
198
				     'resultats' => array());
203
				     'resultats' => array());
199
	    return $resultat;
204
	    return $resultat;
200
	    /*
205
	    /*
201
              header('HTTP/1.0 404 Not Found');
206
              header('HTTP/1.0 404 Not Found');
202
              // don't die (phpunit)
207
              // don't die (phpunit)
203
              throw(new Exception()); */
208
              throw(new Exception()); */
204
	}
209
	}
205
 
210
 
206
 
211
 
207
	// idobs est une liste (toujours ordonnée) des id d'observations recherchées
212
	// 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));
213
	$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']);
214
	$total = $db->recuperer('SELECT FOUND_ROWS() AS c'); $total = intval($total['c']);
210
 
215
 
211
	$liaisons = self::chargerImages($db, $idobs);
216
	$liaisons = self::chargerImages($db, $idobs);
212
 
-
 
213
	/* 
217
	/* 
214
        // Q&D
218
        // Q&D
215
        $images = array();
219
        $images = array();
216
        $o = new Observation($this->conteneur);
220
        $o = new Observation($this->conteneur);
217
        foreach($idobs as $i) {
221
        foreach($idobs as $i) {
218
        $images[$i] = $o->consulter(array($i), array('justthrow' => 1));
222
        $images[$i] = $o->consulter(array($i), array('justthrow' => 1));
219
        }
223
        }
220
	*/
224
	*/
221
	list($images, $images_keyed_by_id_image) = self::reformateImagesDoubleIndex(
225
	list($images, $images_keyed_by_id_image) = self::reformateImagesDoubleIndex(
222
	    $liaisons,
226
	    $liaisons,
223
	    $this->conteneur->getParametre('images.url_images'),
227
	    $this->conteneur->getParametre('images.url_images'),
224
	    $params['format']);
228
	    $params['format']);
225
 
-
 
226
 
229
 
227
	// on charge les votes pour ces images et pour *tous* les protocoles
230
	// on charge les votes pour ces images et pour *tous* les protocoles
228
	$votes = Observation::chargerVotesImage($db, $liaisons, NULL);
231
	$votes = Observation::chargerVotesImage($db, $liaisons, NULL);
229
 
232
 
230
	// subtilité, nous passons ici le tableau d'images indexé par id_image qui est bien plus pratique pour
233
	// 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.
234
	// 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)
235
	// 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
236
	// cf reformateImagesDoubleIndex() à qui revient la tâche de créer ces deux versions simultanément lorsque
234
	// c'est encore possible.
237
	// c'est encore possible.
235
	if($votes) Observation::mapVotesToImages($votes, $images_keyed_by_id_image);
238
	if($votes) Observation::mapVotesToImages($votes, $images_keyed_by_id_image);
236
 
239
 
237
	// les deux masques de tags sont transformés en AST dans le processus de construction de la requête.
240
	// 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)
241
	// 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'],
242
	$params_header = array_merge($params, array_filter(array('masque.tag_cel' => @$parametres['masque.tag_cel'],
240
								 'masque.tag_pictoflora' => @$parametres['masque.tag_pictoflora'])));
243
								 'masque.tag_pictoflora' => @$parametres['masque.tag_pictoflora'])));
241
	$resultat = new ResultatService();
244
	$resultat = new ResultatService();
242
	$resultat->corps = array('entete' => DelTk::makeJSONHeader($total, $params_header, Config::get('url_service')),
245
	$resultat->corps = array('entete' => DelTk::makeJSONHeader($total, $params_header, Config::get('url_service')),
243
				 'resultats' => $images);
246
				 'resultats' => $images);
244
	return $resultat;
247
	return $resultat;
245
    }
248
    }
246
 
249
 
247
    /**
250
    /**
248
     * TODO: partie spécifique liées à la complexité de PictoFlora:
251
     * 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)
252
     * 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 ...).
253
     * 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
254
     * 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
255
     * *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())
256
     * les "mieux notées", ou bien les images ayant le "plus de tags" (COUNT())
254
     */
257
     */
255
    static function sqlOrderBy($p, $db, &$req) {
258
    static function sqlOrderBy($p, $db, &$req) {
256
	// parmi self::$tri_possible
259
	// parmi self::$tri_possible
257
	if($p['tri'] == 'votes') { // LEFT JOIN sur "dis" ci-dessous
260
	if($p['tri'] == 'votes') { // LEFT JOIN sur "dis" ci-dessous
258
	    $req['orderby'] = 'dis.moyenne ' . $p['ordre'] . ', dis.nb_votes ' . $p['ordre'];
261
	    $req['orderby'] = 'dis.moyenne ' . $p['ordre'] . ', dis.nb_votes ' . $p['ordre'];
259
	    return;
262
	    return;
260
	}
263
	}
261
		
264
		
262
	if($p['tri'] == 'tags') { // LEFT JOIN sur "dis" ci-dessous
265
	if($p['tri'] == 'tags') { // LEFT JOIN sur "dis" ci-dessous
263
	    $req['orderby'] = 'dis.nb_tags ' . $p['ordre'];
266
	    $req['orderby'] = 'dis.nb_tags ' . $p['ordre'];
264
	    return;
267
	    return;
265
	}
268
	}
266
 
269
 
267
	if($p['tri'] == 'date_observation') {
270
	if($p['tri'] == 'date_observation') {
268
	    $req['orderby'] = 'date_observation ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
271
	    $req['orderby'] = 'date_observation ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
269
	    return;
272
	    return;
270
	}
273
	}
271
 
274
 
272
	// tri == 'date_transmission'
275
	// tri == 'date_transmission'
273
	// avant cel:r1860, date_transmission pouvait être NULL
276
	// avant cel:r1860, date_transmission pouvait être NULL
274
	// or nous voulons de la consistence (notamment pour phpunit)
277
	// or nous voulons de la consistence (notamment pour phpunit)
275
	$req['orderby'] = 'date_transmission ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
278
	$req['orderby'] = 'date_transmission ' . $p['ordre'] . ', id_observation ' . $p['ordre'];
276
    }
279
    }
277
 
280
 
278
    /*
281
    /*
279
     * in $p: un tableau de paramètres, dont:
282
     * in $p: un tableau de paramètres, dont:
280
     * - 'masque.tag_cel': *tableau* de mots-clefs à chercher parmi cel_image.mots_clefs_texte
283
     * - '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
284
     * - '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
285
     * - 'tag_explode_semantic': défini si les éléments sont tous recherchés ou NON
283
     *
286
     *
284
     * in/ou: $req: un tableau de structure de requête MySQL
287
     * in/ou: $req: un tableau de structure de requête MySQL
285
     *
288
     *
286
     * Attention, le fait que nous cherchions masque.tag_cel OU/ET masque.tag_cel
289
     * 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:
290
     * 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
291
     * Soit directement $this->consulter() si des masque.tag* sont passés
289
     * (split sur ",", "AND" entre chaque condition, "OR" pour chaque valeur de tag)
292
     * (split sur ",", "AND" entre chaque condition, "OR" pour chaque valeur de tag)
290
     * Soit via sqlAddMasqueConstraint():
293
     * Soit via sqlAddMasqueConstraint():
291
     * (pas de split, "OR" entre chaque condition) [ comportement historique ]
294
     * (pas de split, "OR" entre chaque condition) [ comportement historique ]
292
     * équivalent à:
295
     * équivalent à:
293
     * (split sur " ", "OR" entre chaque condition, "AND" pour chaque valeur de tag)
296
     * (split sur " ", "OR" entre chaque condition, "AND" pour chaque valeur de tag)
294
     *
297
     *
295
     */
298
     */
296
    static function sqlAddConstraint($p, $db, &$req, Conteneur $c = NULL) {
299
    static function sqlAddConstraint($p, $db, &$req, Conteneur $c = NULL) {
297
	// TODO implement dans DelTk ?
300
	// TODO implement dans DelTk ?
298
	if(!empty($p['masque.milieu'])) {
301
	if(!empty($p['masque.milieu'])) {
299
	    $req['where'][] = 'vdi.milieu LIKE '.$db->proteger('%' . $p['masque.milieu'].'%');
302
	    $req['where'][] = 'vdi.milieu LIKE '.$db->proteger('%' . $p['masque.milieu'].'%');
300
	}
303
	}
301
 
304
 
302
 
305
 
303
	/* Pour le tri par AVG() des votes nous avons toujours un protocole donné,
306
	/* Pour le tri par AVG() des votes nous avons toujours un protocole donné,
304
	   celui-ci indique sur quels votes porte l'AVG.
307
	   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) */
308
	   (c'est un *vote* qui porte sur un protocole et non l'image elle-même) */
306
	/* TODO: perf problème:
309
	/* TODO: perf problème:
307
	   1) SQL_CALC_FOUND_ROWS: fixable en:
310
	   1) SQL_CALC_FOUND_ROWS: fixable en:
308
           - dissociant le comptage de la récup d'id + javascript async
311
           - 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
312
           - 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)
313
           (paramètre booléen "with-total" par exemple)
311
	   2) jointure forcées: en utilisant `del_imagè`, nous forçons les 2 premiers
314
	   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".
315
           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
316
           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é
317
           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:
318
	   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)
319
           (cel_images/cel_obs_images/cel_obs/del_image_stat)
317
           Cependant c'est à l'optimiseur de définir son ordre préféré. */
320
           Cependant c'est à l'optimiseur de définir son ordre préféré. */
318
	if($p['tri'] == 'votes') {
321
	if($p['tri'] == 'votes') {
319
	    // $p['protocole'] *est* défini (cf requestFilterParams())
322
	    // $p['protocole'] *est* défini (cf requestFilterParams())
320
	    // petite optimisation: INNER JOIN si ordre DESC car les 0 à la fin
323
	    // petite optimisation: INNER JOIN si ordre DESC car les 0 à la fin
321
	    if($p['ordre'] == 'desc') {
324
	    if($p['ordre'] == 'desc') {
322
		// pas de group by nécessaire pour cette jointure
325
		// pas de group by nécessaire pour cette jointure
323
		// PRIMARY KEY (`ce_image`, `ce_protocole`)
326
		// PRIMARY KEY (`ce_image`, `ce_protocole`)
324
		$req['join'][] = sprintf('INNER JOIN del_image_stat dis'.
327
		$req['join'][] = sprintf('INNER JOIN del_image_stat dis'.
325
					 ' ON vdi.id_image = dis.ce_image'.
328
					 ' ON vdi.id_image = dis.ce_image'.
326
					 ' AND dis.ce_protocole = %d',
329
					 ' AND dis.ce_protocole = %d',
327
					 $p['protocole']);
330
					 $p['protocole']);
328
	    } else {
331
	    } else {
329
		$req['join'][] = sprintf('LEFT JOIN del_image_stat dis'.
332
		$req['join'][] = sprintf('LEFT JOIN del_image_stat dis'.
330
					 ' ON vdi.id_image = dis.ce_image'.
333
					 ' ON vdi.id_image = dis.ce_image'.
331
					 ' AND dis.ce_protocole = %d',
334
					 ' AND dis.ce_protocole = %d',
332
					 $p['protocole']);
335
					 $p['protocole']);
333
		// nécessaire (dup ce_image dans del_image_stat)
336
		// nécessaire (dup ce_image dans del_image_stat)
334
		$req['groupby'][] = 'vdi.id_observation';
337
		$req['groupby'][] = 'vdi.id_observation';
335
	    }
338
	    }
336
	}
339
	}
337
 
340
 
338
	if($p['tri'] == 'tags') {
341
	if($p['tri'] == 'tags') {
339
	    $req['join'][] = sprintf('%s JOIN del_image_stat dis ON vdi.id_image = dis.ce_image',
342
	    $req['join'][] = sprintf('%s JOIN del_image_stat dis ON vdi.id_image = dis.ce_image',
340
				     ($p['ordre'] == 'desc') ? 'INNER' : 'LEFT');
343
				     ($p['ordre'] == 'desc') ? 'INNER' : 'LEFT');
341
	    // nécessaire (dup ce_image dans del_image_stat)
344
	    // nécessaire (dup ce_image dans del_image_stat)
342
	    $req['groupby'][] = 'vdi.id_observation';
345
	    $req['groupby'][] = 'vdi.id_observation';
343
	}
346
	}
344
 
347
 
345
	// car il ne sont pas traités par la générique requestFilterParams() les clefs "masque.tag_*"
348
	// 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.
349
	// sont toujours présentes; bien que parfois NULL.
347
	if($p['masque.tag_cel']) {
350
	if($p['masque.tag_cel']) {
348
	    if(isset($p['masque.tag_cel']['AND'])) {
351
	    if(isset($p['masque.tag_cel']['AND'])) {
349
		// TODO: utiliser les tables de mots clefs normaliées dans tb_cel ?
352
		// TODO: utiliser les tables de mots clefs normaliées dans tb_cel ?
350
		// et auquel cas laisser au client le choix du couteux "%" ?
353
		// et auquel cas laisser au client le choix du couteux "%" ?
351
		$tags = $p['masque.tag_cel']['AND'];
354
		$tags = $p['masque.tag_cel']['AND'];
352
		array_walk($tags, create_function('&$val, $k, $db',
355
		array_walk($tags, create_function('&$val, $k, $db',
353
						  '$val = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) LIKE %s",
356
						  '$val = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) LIKE %s",
354
																  $db->proteger("%".$val."%"));'),
357
																  $db->proteger("%".$val."%"));'),
355
			   $db);
358
			   $db);
356
		$req['where'][] = '(' . implode(' AND ', $tags) . ')';
359
		$req['where'][] = '(' . implode(' AND ', $tags) . ')';
357
	    }
360
	    }
358
	    else {
361
	    else {
359
		$req['where'][] = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) REGEXP %s",
362
		$req['where'][] = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) REGEXP %s",
360
					  $db->proteger(implode('|', $p['masque.tag_cel']['OR'])));
363
					  $db->proteger(implode('|', $p['masque.tag_cel']['OR'])));
361
	    }
364
	    }
362
	}
365
	}
363
 
366
 
364
	if($p['masque.tag_pictoflora']) {
367
	if($p['masque.tag_pictoflora']) {
365
	    // inutilisable pour l'instant
368
	    // inutilisable pour l'instant
366
	    // self::sqlAddPictoFloraTagConstraint1($p, $db, $req);
369
	    // self::sqlAddPictoFloraTagConstraint1($p, $db, $req);
367
 
370
 
368
	    // intéressante, mais problème d'optimiseur MySQL 5.5 (dependant subquery)
371
	    // intéressante, mais problème d'optimiseur MySQL 5.5 (dependant subquery)
369
	    // self::sqlAddPictoFloraTagConstraint2($p, $db, $req);
372
	    // self::sqlAddPictoFloraTagConstraint2($p, $db, $req);
370
 
373
 
371
	    // approche fiable mais sous-optimale
374
	    // approche fiable mais sous-optimale
372
	    self::sqlAddPictoFloraTagConstraint3($p, $db, $req);
375
	    self::sqlAddPictoFloraTagConstraint3($p, $db, $req);
373
	}
376
	}
374
    }
377
    }
375
 
378
 
376
    /* approche intéressante si les deux problèmes suivants peuvent être résolu:
379
    /* 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é)
380
       - 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 ? */
381
       - dans le cas d'un ET logique, comment chercher les observations correspondantes ? */
379
    static function sqlAddPictoFloraTagConstraint1($p, $db, &$req) {
382
    static function sqlAddPictoFloraTagConstraint1($p, $db, &$req) {
380
	// XXX: utiliser tag plutôt que tag_normalise ?
383
	// XXX: utiliser tag plutôt que tag_normalise ?
381
	$req['join'][] = 'LEFT JOIN del_image_tag dit ON dit.ce_image = vdi.id_image';
384
	$req['join'][] = 'LEFT JOIN del_image_tag dit ON dit.ce_image = vdi.id_image';
382
	$req['where'][] = 'dit.actif = 1';
385
	$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)
386
	$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
387
	// 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'])) {
388
	if(isset($p['masque.tag_pictoflora']['AND'])) {
386
	    // TODO/XXX : comment matcher les observations ayant tous les mots-clef passés ?
389
	    // TODO/XXX : comment matcher les observations ayant tous les mots-clef passés ?
387
	    // ... le LEFT-JOIN n'y semble pas adapté
390
	    // ... le LEFT-JOIN n'y semble pas adapté
388
	}
391
	}
389
	else {
392
	else {
390
	    $protected_tags = array();
393
	    $protected_tags = array();
391
	    foreach($p['masque.tag_pictoflora']['OR'] as $tag) $protected_tags[] = $db->proteger(strtolower($tag));
394
	    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));
395
	    $req['where'][] = sprintf('tag_normalise IN (%s)', implode(',', $protected_tags));
393
	}
396
	}
394
    }
397
    }
395
 
398
 
396
    // inutilisé pour l'instant pour cause de soucis d'optimiseur MySQL (cf commentaire en intro)
399
    // inutilisé pour l'instant pour cause de soucis d'optimiseur MySQL (cf commentaire en intro)
397
    static function sqlAddPictoFloraTagConstraint2($p, $db, &$req) {
400
    static function sqlAddPictoFloraTagConstraint2($p, $db, &$req) {
398
	// Note à propos des 4 "@ instruction" ci-dessous (notamment sur recupererTous())
401
	// 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
402
	// 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
403
	// l'utiliser, mais peut sortir une erreur en cas de REGEXP invalide
401
	// ex: REGEX "^(".
404
	// ex: REGEX "^(".
402
	// Pour l'heure nous ignorons ce type d'erreur car aucun de nos champ de recherche
405
	// 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
406
	// ne peuvent (ou ne devrait) comporter des meta-caractères
404
	// ([])?*+\\
407
	// ([])?*+\\
405
	if(isset($p['masque.tag_pictoflora']['AND'])) {
408
	if(isset($p['masque.tag_pictoflora']['AND'])) {
406
	    // optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
409
	    // 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 "|"
410
	    // donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
408
	    sort($p['masque.tag_pictoflora']['AND']);
411
	    sort($p['masque.tag_pictoflora']['AND']);
409
	    $req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
412
	    $req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
410
				      " GROUP BY ce_image".
413
				      " GROUP BY ce_image".
411
				      " HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s)",
414
				      " HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s)",
412
				      $db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND'])));
415
				      $db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND'])));
413
	}
416
	}
414
	else {
417
	else {
415
	    $req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
418
	    $req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
416
				      " GROUP BY ce_image".
419
				      " GROUP BY ce_image".
417
				      " HAVING GROUP_CONCAT(tag_normalise) REGEXP %s)",
420
				      " HAVING GROUP_CONCAT(tag_normalise) REGEXP %s)",
418
				      $db->proteger(implode('|', $p['masque.tag_pictoflora']['OR'])));
421
				      $db->proteger(implode('|', $p['masque.tag_pictoflora']['OR'])));
419
	}
422
	}
420
    }
423
    }
421
 
424
 
422
    // si l'on est bassiné par les "DEPENDENT SUBQUERY", nous la faisons donc indépendemment via cette fonction
425
    // 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) {
426
    static function sqlAddPictoFloraTagConstraint3($p, $db, &$req) {
424
	if(isset($p['masque.tag_pictoflora']['AND'])) {
427
	if(isset($p['masque.tag_pictoflora']['AND'])) {
425
	    // optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
428
	    // 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 "|"
429
	    // donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
427
	    sort($p['masque.tag_pictoflora']['AND']);
430
	    sort($p['masque.tag_pictoflora']['AND']);
428
 
431
 
429
	    // plutôt que db->connexion->query->fetchColumn(), une API pourrie nous oblige à ...
432
	    // plutôt que db->connexion->query->fetchColumn(), une API pourrie nous oblige à ...
430
	    $ids = @$db->recupererTous(sprintf(
433
	    $ids = @$db->recupererTous(sprintf(
431
		"SELECT ce_image FROM del_image_tag WHERE actif = 1".
434
		"SELECT ce_image FROM del_image_tag WHERE actif = 1".
432
		" GROUP BY ce_image".
435
		" GROUP BY ce_image".
433
		" HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s",
436
		" HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s",
434
		$db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND']))));
437
		$db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND']))));
435
 
438
 
436
	    // puis:
439
	    // puis:
437
	    $ids = @array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
440
	    $ids = @array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
-
 
441
	    $ids = !empty($ids) ? implode(',', $ids) : 'SELECT ce_image FROM del_image_tag  WHERE false';
438
	    if($ids) $req['where'][] = sprintf("vdi.id_image IN (%s)", implode(',', $ids));
442
	    $req['where'][] = sprintf("vdi.id_image IN (%s)", $ids);
439
 
443
 
440
	}
444
	}
441
	else {
445
	else {
442
	    $ids = @$db->recupererTous(sprintf(
446
	    $ids = @$db->recupererTous(sprintf(
443
		"SELECT ce_image FROM del_image_tag WHERE actif = 1".
447
		"SELECT ce_image FROM del_image_tag WHERE actif = 1".
444
		" GROUP BY ce_image".
448
		" GROUP BY ce_image".
445
		" HAVING GROUP_CONCAT(tag_normalise) REGEXP %s",
449
		" HAVING GROUP_CONCAT(tag_normalise) REGEXP %s",
446
		$db->proteger(implode('|', $p['masque.tag_pictoflora']['OR']))));
450
		$db->proteger(implode('|', $p['masque.tag_pictoflora']['OR']))));
447
 
451
 
448
	    $ids = @array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
452
	    $ids = @array_map(create_function('$e', 'return $e["ce_image"];'), $ids);
-
 
453
	    $ids = !empty($ids) ? implode(',', $ids) : 'SELECT ce_image FROM del_image_tag  WHERE false';
449
	    if($ids) $req['where'][] = sprintf("vdi.id_image IN (%s)", implode(',', $ids));
454
	    $req['where'][] = sprintf("vdi.id_image IN (%s)", $ids);
450
	}
455
	}
451
    }
456
    }
452
 
457
 
453
    static function getIdImages($p, $req, $db) {
458
    static function getIdImages($p, $req, $db) {
454
	return $db->recupererTous(sprintf(
459
	return $db->recupererTous(sprintf(
455
	    'SELECT SQL_CALC_FOUND_ROWS id_image' .
460
	    'SELECT SQL_CALC_FOUND_ROWS id_image' .
456
	    ' FROM v_del_image vdi'.
461
	    ' FROM v_del_image vdi'.
457
	    ' %s' . // LEFT JOIN if any
462
	    ' %s' . // LEFT JOIN if any
458
	    ' WHERE %s'. // where-clause ou TRUE
463
	    ' WHERE %s'. // where-clause ou TRUE
459
	    ' %s'. // group-by
464
	    ' %s'. // group-by
460
	    ' ORDER BY %s'.
465
	    ' ORDER BY %s'.
461
	    ' LIMIT %d, %d -- %s',
466
	    ' LIMIT %d, %d -- %s',
462
						 
467
						 
463
	    $req['join'] ? implode(' ', array_unique($req['join'])) : '',
468
	    $req['join'] ? implode(' ', array_unique($req['join'])) : '',
464
	    $req['where'] ? implode(' AND ', $req['where']) : 'TRUE',
469
	    $req['where'] ? implode(' AND ', $req['where']) : 'TRUE',
465
			
470
			
466
	    $req['groupby'] ? ('GROUP BY ' . implode(', ', array_unique($req['groupby']))) : '',
471
	    $req['groupby'] ? ('GROUP BY ' . implode(', ', array_unique($req['groupby']))) : '',
467
			
472
			
468
	    $req['orderby'],
473
	    $req['orderby'],
469
			
474
			
470
	    $p['navigation.depart'], $p['navigation.limite'], __FILE__ . ':' . __LINE__));
475
	    $p['navigation.depart'], $p['navigation.limite'], __FILE__ . ':' . __LINE__));
471
 
-
 
472
    }
476
    }
473
 
477
 
474
    static function chargerImages($db, $idImg) {
478
    static function chargerImages($db, $idImg) {
475
	$obs_fields = DelTk::sqlFieldsToAlias(self::$mappings['observations'], NULL);
479
	$obs_fields = DelTk::sqlFieldsToAlias(self::$mappings['observations'], NULL);
476
	$image_fields = DelTk::sqlFieldsToAlias(self::$mappings['images'], NULL);
480
	$image_fields = DelTk::sqlFieldsToAlias(self::$mappings['images'], NULL);
477
	
481
	
478
	return $db->recupererTous(sprintf('SELECT '.
482
	return $db->recupererTous(sprintf('SELECT '.
479
					  ' CONCAT(id_image, "-", id_observation) AS jsonindex,'.
483
					  ' CONCAT(id_image, "-", id_observation) AS jsonindex,'.
480
					  ' %1$s, %2$s FROM v_del_image '.
484
					  ' %1$s, %2$s FROM v_del_image '.
481
					  ' WHERE %3$s'.
485
					  ' WHERE %3$s'.
482
					  ' -- %4$s',
486
					  ' -- %4$s',
483
					  $obs_fields, $image_fields,
487
					  $obs_fields, $image_fields,
484
					  sprintf('id_image IN (%s)', implode(',', $idImg)),
488
					  sprintf('id_image IN (%s)', implode(',', $idImg)),
485
					  __FILE__ . ':' . __LINE__));
489
					  __FILE__ . ':' . __LINE__));
486
 
490
 
487
    }
491
    }
488
 
492
 
489
    /* "masque" ne fait jamais que faire une requête sur la plupart des champs, (presque) tous traités
493
    /* "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:
494
       de manière identique à la seule différence que:
491
       1) ils sont combinés par des "OU" logiques plutôt que des "ET".
495
       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:
496
       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
497
       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)
498
       (dit autrement, str_replace(" ", ".*", $mots-clefs-requête) =~ $mots-clefs-mysql)
495
       Pour plus d'information: (ListeObservations|DelTk)::sqlAddMasqueConstraint() */
499
       Pour plus d'information: (ListeObservations|DelTk)::sqlAddMasqueConstraint() */
496
    static function sqlAddMasqueConstraint($p, $db, &$req, Conteneur $c = NULL) {
500
    static function sqlAddMasqueConstraint($p, $db, &$req, Conteneur $c = NULL) {
497
	if(!empty($p['masque'])) {
501
	if(!empty($p['masque'])) {
498
	    $or_params = array('masque.auteur' => $p['masque'],
502
	    $or_params = array('masque.auteur' => $p['masque'],
499
			       'masque.departement' => $p['masque'],
503
			       'masque.departement' => $p['masque'],
500
			       'masque.commune' => $p['masque'], // TODO/XXX ?
504
			       'masque.commune' => $p['masque'], // TODO/XXX ?
501
			       'masque.id_zone_geo' => $p['masque'],
505
			       'masque.id_zone_geo' => $p['masque'],
502
 
506
 
503
			       /* tous-deux remplacent masque.tag
507
			       /* tous-deux remplacent masque.tag
504
				  mais sont traité séparément des requestFilterParams() */
508
				  mais sont traité séparément des requestFilterParams() */
505
			       // 'masque.tag_cel' => $p['masque'],
509
			       // 'masque.tag_cel' => $p['masque'],
506
			       // 'masque.tag_pictoflora' => $p['masque'],
510
			       // 'masque.tag_pictoflora' => $p['masque'],
507
 
511
 
508
			       'masque.ns' => $p['masque'],
512
			       'masque.ns' => $p['masque'],
509
			       'masque.famille' => $p['masque'],
513
			       'masque.famille' => $p['masque'],
510
			       'masque.date' => $p['masque'],
514
			       'masque.date' => $p['masque'],
511
			       'masque.genre' => $p['masque'],
515
			       'masque.genre' => $p['masque'],
512
			       'masque.milieu' => $p['masque'],
516
			       'masque.milieu' => $p['masque'],
513
 
517
 
514
			       // tri est aussi nécessaire car affecte les contraintes de JOIN
518
			       // tri est aussi nécessaire car affecte les contraintes de JOIN
515
			       'tri' => $p['tri'],
519
			       'tri' => $p['tri'],
516
			       'ordre' => $p['ordre']);
520
			       'ordre' => $p['ordre']);
517
 
521
 
518
	    /* Cependant les champs spécifiques ont priorité sur le masque général.
522
	    /* 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
523
	       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. */
524
	       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']);
525
	    if(isset($p['masque.auteur'])) unset($or_params['masque.auteur']);
522
	    if(isset($p['masque.departement'])) unset($or_params['masque.departement']);
526
	    if(isset($p['masque.departement'])) unset($or_params['masque.departement']);
523
	    if(isset($p['masque.commune'])) unset($or_params['masque.commune']);
527
	    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']);
528
	    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']);
529
	    if(isset($p['masque.ns'])) unset($or_params['masque.ns']);
526
	    if(isset($p['masque.famille'])) unset($or_params['masque.famille']);
530
	    if(isset($p['masque.famille'])) unset($or_params['masque.famille']);
527
	    if(isset($p['masque.date'])) unset($or_params['masque.date']);
531
	    if(isset($p['masque.date'])) unset($or_params['masque.date']);
528
	    if(isset($p['masque.genre'])) unset($or_params['masque.genre']);
532
	    if(isset($p['masque.genre'])) unset($or_params['masque.genre']);
529
	    if(isset($p['masque.milieu'])) unset($or_params['masque.milieu']);
533
	    if(isset($p['masque.milieu'])) unset($or_params['masque.milieu']);
530
	    if(isset($p['masque.tag_cel'])) unset($or_params['masque.tag_cel']);
534
	    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']);
535
	    if(isset($p['masque.tag_pictoflora'])) unset($or_params['masque.tag_pictoflora']);
532
 
536
 
533
	    $or_masque = array_merge(
537
	    $or_masque = array_merge(
534
		DelTk::requestFilterParams($or_params, NULL, $c /* pour masque.departement */),
538
		DelTk::requestFilterParams($or_params, NULL, $c /* pour masque.departement */),
535
		self::requestFilterParams($or_params));
539
		self::requestFilterParams($or_params));
536
 
540
 
537
	    /* Lorsqu'on utilise le masque général pour chercher des tags, ils sont
541
	    /* Lorsqu'on utilise le masque général pour chercher des tags, ils sont
538
	       postulés comme séparés par des espaces, et doivent être tous matchés. */
542
	       postulés comme séparés par des espaces, et doivent être tous matchés. */
539
	    if(isset($or_params['masque.tag_cel']))
543
	    if(isset($or_params['masque.tag_cel']))
540
		$or_masque['masque.tag_cel'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
544
		$or_masque['masque.tag_cel'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
541
	    if(isset($or_params['masque.tag_pictoflora']))
545
	    if(isset($or_params['masque.tag_pictoflora']))
542
		$or_masque['masque.tag_pictoflora'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
546
		$or_masque['masque.tag_pictoflora'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
543
 
547
 
544
 
548
 
545
	    // pas de select, groupby & co ici: uniquement 'join' et 'where'
549
	    // pas de select, groupby & co ici: uniquement 'join' et 'where'
546
	    $or_req = array('join' => array(), 'where' => array());
550
	    $or_req = array('join' => array(), 'where' => array());
547
	    DelTk::sqlAddConstraint($or_masque, $db, $or_req);
551
	    DelTk::sqlAddConstraint($or_masque, $db, $or_req);
548
	    self::sqlAddConstraint($or_masque, $db, $or_req);
552
	    self::sqlAddConstraint($or_masque, $db, $or_req);
549
 
553
 
550
	    if($or_req['where']) {
554
	    if($or_req['where']) {
551
		$req['where'][] = '(' . implode(' OR ', $or_req['where']) . ')';
555
		$req['where'][] = '(' . implode(' OR ', $or_req['where']) . ')';
552
		// utile au cas ou des jointures seraient rajoutées
556
		// utile au cas ou des jointures seraient rajoutées
553
		$req['join'] = array_unique(array_merge($req['join'], $or_req['join']));
557
		$req['join'] = array_unique(array_merge($req['join'], $or_req['join']));
554
	    }
558
	    }
555
	}
559
	}
556
    }
560
    }
557
 
561
 
558
 
562
 
559
    // cf Observation::reformateObservationSimpleIndex() et ListeObservations::reformateObservation()
563
    // cf Observation::reformateObservationSimpleIndex() et ListeObservations::reformateObservation()
560
    // (trop de variétés de formatage, à unifier côté client pour unifier côté backend ...)
564
    // (trop de variétés de formatage, à unifier côté client pour unifier côté backend ...)
561
    static function reformateImagesDoubleIndex($obs, $url_pattern = '', $image_format = 'XL') {
565
    static function reformateImagesDoubleIndex($obs, $url_pattern = '', $image_format = 'XL') {
562
	// XXX: cf Observation.php::consulter(), nous pourriouns ici
566
	// XXX: cf Observation.php::consulter(), nous pourriouns ici
563
	// conserver les valeurs vides (pour les phptests notamment, ou non)
567
	// conserver les valeurs vides (pour les phptests notamment, ou non)
564
	// $obs = array_map('array_filter', $obs);
568
	// $obs = array_map('array_filter', $obs);
565
	$obs_merged = $obs_keyed_by_id_image = array();
569
	$obs_merged = $obs_keyed_by_id_image = array();
566
	foreach($obs as $o) {
570
	foreach($obs as $o) {
567
	    // ceci nous complique la tâche pour le reste du processing...
571
	    // ceci nous complique la tâche pour le reste du processing...
568
	    $id = $o['jsonindex'];
572
	    $id = $o['jsonindex'];
569
	    // ainsi nous utilisons deux tableaux: le final, indexé par couple d'id(image-obs)
573
	    // ainsi nous utilisons deux tableaux: le final, indexé par couple d'id(image-obs)
570
	    // et celui indexé par simple id_image qui est fort utile pour mapVotesToImages()
574
	    // et celui indexé par simple id_image qui est fort utile pour mapVotesToImages()
571
	    // mais tout deux partage leur référence à "protocole"
575
	    // mais tout deux partage leur référence à "protocole"
572
	    $image = array(
576
	    $image = array(
573
		'id_image' => $o['id_image'],
577
		'id_image' => $o['id_image'],
574
		'binaire.href' => sprintf($url_pattern, $o['id_image'], $image_format),
578
		'binaire.href' => sprintf($url_pattern, $o['id_image'], $image_format),
575
		'mots_cles_texte' => @$o['i_mots_cles_texte'], // @, peut avoir été filtré par array_map() ci-dessus
579
		'mots_cles_texte' => @$o['i_mots_cles_texte'], // @, peut avoir été filtré par array_map() ci-dessus
576
	    );
580
	    );
577
	    unset($o['id_image'], $o['i_mots_cles_texte'], $o['jsonindex']);
581
	    unset($o['id_image'], $o['i_mots_cles_texte'], $o['jsonindex']);
578
	    if(!isset($obs_merged[$id])) $obs_merged[$id] = $image;
582
	    if(!isset($obs_merged[$id])) $obs_merged[$id] = $image;
579
	    $obs_merged[$id]['observation'] = $o;
583
	    $obs_merged[$id]['observation'] = $o;
580
	    $obs_merged[$id]['protocoles_votes'] = array();
584
	    $obs_merged[$id]['protocoles_votes'] = array();
581
			
585
			
582
	    $obs_keyed_by_id_image[$image['id_image']]['protocoles_votes'] = &$obs_merged[$id]['protocoles_votes'];
586
	    $obs_keyed_by_id_image[$image['id_image']]['protocoles_votes'] = &$obs_merged[$id]['protocoles_votes'];
583
	}
587
	}
584
 
588
 
585
	return array($obs_merged,$obs_keyed_by_id_image);
589
	return array($obs_merged,$obs_keyed_by_id_image);
586
    }
590
    }
587
 
591
 
588
 
592
 
589
 
593
 
590
    // complete & override DelTk::requestFilterParams() (même usage)
594
    // complete & override DelTk::requestFilterParams() (même usage)
591
    static function requestFilterParams(Array $params, $parametres_autorises = NULL) {
595
    static function requestFilterParams(Array $params, $parametres_autorises = NULL) {
592
	if($parametres_autorises) { // filtrage de toute clef inconnue
596
	if($parametres_autorises) { // filtrage de toute clef inconnue
593
	    $params = array_intersect_key($params, array_flip($parametres_autorises));
597
	    $params = array_intersect_key($params, array_flip($parametres_autorises));
594
	}
598
	}
595
 
599
 
596
	$p = array();
600
	$p = array();
597
	$p['tri'] = DelTk::unsetIfInvalid($params, 'tri', self::$tri_possible);
601
	$p['tri'] = DelTk::unsetIfInvalid($params, 'tri', self::$tri_possible);
598
	$p['format'] = DelTk::unsetIfInvalid($params, 'format', self::$format_image_possible);
602
	$p['format'] = DelTk::unsetIfInvalid($params, 'format', self::$format_image_possible);
599
 
603
 
600
	// "milieu" inutile pour IdentiPlantes ?
604
	// "milieu" inutile pour IdentiPlantes ?
601
	if(isset($params['masque.milieu'])) $p['masque.milieu'] = trim($params['masque.milieu']);
605
	if(isset($params['masque.milieu'])) $p['masque.milieu'] = trim($params['masque.milieu']);
602
 
606
 
603
	// compatibilité
607
	// compatibilité
604
	if(isset($params['masque.tag'])) {
608
	if(isset($params['masque.tag'])) {
605
	    $params['masque.tag_cel'] = $params['masque.tag_pictoflora'] = $params['masque.tag'];
609
	    $params['masque.tag_cel'] = $params['masque.tag_pictoflora'] = $params['masque.tag'];
606
	}
610
	}
607
 
611
 
608
	if($p['tri'] == 'votes' || $p['tri'] == 'tags') {
612
	if($p['tri'] == 'votes' || $p['tri'] == 'tags') {
609
	    // ces critère de tri des image à privilégier ne s'applique qu'à un protocole donné
613
	    // ces critère de tri des image à privilégier ne s'applique qu'à un protocole donné
610
	    if(!isset($params['protocole']) || !is_numeric($params['protocole']))
614
	    if(!isset($params['protocole']) || !is_numeric($params['protocole']))
611
		$p['protocole'] = self::$default_proto;
615
		$p['protocole'] = self::$default_proto;
612
	    else
616
	    else
613
		$p['protocole'] = intval($params['protocole']);
617
		$p['protocole'] = intval($params['protocole']);
614
	}
618
	}
615
 
619
 
616
	return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
620
	return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
617
    }
621
    }
618
 
622
 
619
 
623
 
620
 
624
 
621
    // met à jour *toutes* les stats de nombre de tags et de moyenne des votes
625
    // met à jour *toutes* les stats de nombre de tags et de moyenne des votes
622
    static function _update_statistics($db) {
626
    static function _update_statistics($db) {
623
	$db->requeter("TRUNCATE TABLE del_image_stat");
627
	$db->requeter("TRUNCATE TABLE del_image_stat");
624
	$db->requeter(<<<EOF
628
	$db->requeter(<<<EOF
625
INSERT INTO `del_image_stat` (
629
INSERT INTO `del_image_stat` (
626
	SELECT id_image, divo.ce_protocole, divo.moyenne, divo.nb_votes, dit.ctags 
630
	SELECT id_image, divo.ce_protocole, divo.moyenne, divo.nb_votes, dit.ctags 
627
	FROM `tb_cel`.`cel_images` ci 
631
	FROM `tb_cel`.`cel_images` ci 
628
	LEFT JOIN 
632
	LEFT JOIN 
629
	( SELECT ce_image, ce_protocole, AVG(valeur) AS moyenne, COUNT(valeur) AS nb_votes FROM del_image_vote 
633
	( SELECT ce_image, ce_protocole, AVG(valeur) AS moyenne, COUNT(valeur) AS nb_votes FROM del_image_vote 
630
	  GROUP BY ce_image, ce_protocole ) AS divo
634
	  GROUP BY ce_image, ce_protocole ) AS divo
631
	ON ci.id_image = divo.ce_image 
635
	ON ci.id_image = divo.ce_image 
632
	LEFT JOIN 
636
	LEFT JOIN 
633
	( SELECT ce_image, COUNT(id_tag) as ctags FROM del_image_tag 
637
	( SELECT ce_image, COUNT(id_tag) as ctags FROM del_image_tag 
634
	  GROUP BY ce_image ) AS dit 
638
	  GROUP BY ce_image ) AS dit 
635
	ON ci.id_image = dit.ce_image )
639
	ON ci.id_image = dit.ce_image )
636
EOF
640
EOF
637
	);
641
	);
638
    }
642
    }
639
 
643
 
640
    static function revOrderBy($orderby) {
644
    static function revOrderBy($orderby) {
641
	return $orderby == 'asc' ? 'desc' : 'asc';
645
	return $orderby == 'asc' ? 'desc' : 'asc';
642
    }
646
    }
643
}
647
}