Subversion Repositories eFlore/Applications.del

Rev

Rev 1845 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

<?php
// declare(encoding='UTF-8');
/**
 * Classe contenant des méthodes permettant de construire les requêtes SQL complexe concernant les images et obs.
 *
 * @category  DEL
 * @package   Services
 * @package   Bibliotheque
 * @version   0.1
 * @author    Mathias CHOUET <mathias@tela-botanica.org>
 * @author    Jean-Pascal MILCENT <jpm@tela-botanica.org>
 * @author    Aurelien PERONNET <aurelien@tela-botanica.org>
 * @license   GPL v3 <http://www.gnu.org/licenses/gpl.txt>
 * @license   CECILL v2 <http://www.cecill.info/licences/Licence_CeCILL_V2-en.txt>
 * @copyright 1999-2014 Tela Botanica <accueil@tela-botanica.org>
 */
class Sql {

        private $conteneur;
        private $bdd;
        private $parametres = array();
        private $requete = array(
                'select' => array(),
                'join' => array(),
                'where' => array(),
                'groupby' => array(),
                'orderby' => array());

        private $champsPrenom = array('du.prenom', 'vdi.prenom_utilisateur');
        private $champsNom = array('du.nom',  'vdi.nom_utilisateur');


        public function __construct(Conteneur $conteneur) {
                $this->conteneur = $conteneur;
                $this->bdd = $this->conteneur->getBdd();
        }

        public function setParametres(Array $parametres) {
                $this->parametres = $parametres;
        }

        public function getRequeteSql() {
                return $this->requete;
        }

        private function addJoin($join) {
                $this->requete['join'][] = $join;
        }

        public function getJoin() {
                return ($this->requete['join'] ? implode(' ', array_unique($this->requete['join'])).' ' : '');
        }

        private function addJoinDis($join) {
                $this->requete['join']['dis'] = $join;
        }

        private function addWhere($idParam, $where) {
                if (isset($this->parametres['_parametres_condition_or_'])
                                && in_array($idParam, $this->parametres['_parametres_condition_or_'])) {
                        $this->requete['where']['OR'][] = $where;
                } else {
                        $this->requete['where']['AND'][] = $where;
                }
        }
        public function getWhere() {
                if (isset($this->requete['where']['OR']) && count($this->requete['where']['OR']) > 0) {
                        $this->requete['where']['AND'][] = '('.implode(' OR ', $this->requete['where']['OR']).')';
                }

                $where = ' TRUE ';
                if (isset($this->requete['where']['AND']) && count($this->requete['where']['AND']) > 0) {
                        $where = implode(' AND ', $this->requete['where']['AND']).' ';
                }
                return $where;
        }

        private function addGroupBy($groupBy) {
                $this->requete['groupby'][] = $groupBy;
        }

        public function getGroupBy() {
                $groupby = '';
                if (isset($this->requete['groupby']) && count($this->requete['groupby']) > 0) {
                        $groupby = 'GROUP BY '.implode(', ', array_unique($this->requete['groupby'])).' ';
                }
                return $groupby;
        }

        private function addOrderBy($orderby) {
                $this->requete['orderby'][] = $orderby;
        }

        public function getOrderBy() {
                $orderby = '';
                if (isset($this->requete['orderby']) && count($this->requete['orderby']) > 0) {
                        $orderby = 'ORDER BY '.implode(', ', array_unique($this->requete['orderby'])).' ';
                }
                return $orderby;
        }

        public function getLimit() {
                return 'LIMIT '.$this->parametres['navigation.depart'].','.$this->parametres['navigation.limite'].' ';
        }

        /**
         * - Rempli le tableau des contraintes "where" et "join" nécessaire
         * à la *recherche* des observations demandées ($req) utilisées par self::getIdObs()
         *
         * Attention, cela signifie que toutes les tables ne sont pas *forcément*
         * join'ées, par exemple si aucune contrainte ne le nécessite.
         * $req tel qu'il est rempli ici est utile pour récupéré la seule liste des
         * id d'observation qui match.
         * Pour la récupération effective de "toutes" les données correspondante, il faut
         * réinitialiser $req["join"] afin d'y ajouter toutes les autres tables.
         *
         * Note: toujours rajouter les préfixes de table (vdi,du,doi ou di), en fonction de ce que défini
         * les JOIN qui sont utilisés.
         * le préfix de v_del_image est "vdi" (cf: "FROM" de self::getIdObs())
         * le préfix de del_utilisateur sur id_utilisateur = vdi.ce_utilisateur est "du"
         *
         * @param $p les paramètres (notamment de masque) passés par l'URL et déjà traités/filtrés (sauf quotes)
         * @param $req le tableau, passé par référence représentant les composants de la requête à bâtir
         */
        public function ajouterContraintes() {
                $this->ajouterContrainteAuteur();
                $this->ajouterContrainteDate();
                $this->ajouterContrainteDepartement();
                $this->ajouterContrainteIdZoneGeo();
                $this->ajouterContrainteGenre();
                $this->ajouterContrainteFamille();
                $this->ajouterContrainteNs();
                $this->ajouterContrainteNn();
                $this->ajouterContrainteReferentiel();
                $this->ajouterContrainteCommune();
        }

        private function ajouterContrainteAuteur() {
                if (isset($this->parametres['masque.auteur'])) {
                        $auteur = $this->parametres['masque.auteur'];
                        // id du poster de l'obs
                        $this->addJoin('LEFT JOIN del_utilisateur AS du ON (du.id_utilisateur = vdi.ce_utilisateur) ');
                        // id du poster de l'image... NON, c'est le même que le posteur de l'obs
                        // Cette jointure de table est ignoré ci-dessous pour les recherches d'auteurs
                        // $req['join'][] = 'LEFT JOIN del_utilisateur AS dui ON dui.id_utilisateur = vdi.i_ce_utilisateur';

                        if (is_numeric($auteur)) {
                                $this->ajouterContrainteAuteurId();
                        } elseif(preg_match('/^.{5,}@[a-z0-9-.]{5,}$/i', $auteur)) {
                                $this->ajouterContrainteAuteurEmail();
                        } else {
                                $this->ajouterContrainteAuteurIntitule();
                        }
                }
        }

        private function ajouterContrainteAuteurId() {
                $id = $this->parametres['masque.auteur'];
                $sqlTpl = '(du.id_utilisateur = %1$d OR vdi.ce_utilisateur = %1$d)';
                $whereAuteur = sprintf($sqlTpl, $id);
                $this->addWhere('masque.auteur', $whereAuteur);
        }

        private function ajouterContrainteAuteurEmail() {
                $email = $this->parametres['masque.auteur'];
                $sqlTpl = '(du.courriel LIKE %1$s OR vdi.courriel LIKE %1$s )';
                $emailP = $this->bdd->proteger("$email%");
                $whereAuteur = sprintf($sqlTpl, $emailP);
                $this->addWhere('masque.auteur', $whereAuteur);
        }

        /**
         * Retourne une clause where du style:
         * CONCAT(IF(du.prenom IS NULL, "", du.prenom), [...] vdi.i_nomutilisateur) REGEXP 'xxx'
         * Note; i_(nom|prenom_utilisateur), alias pour cel_images.(nom|prenom), n'est pas traité
         * car cette information est redondante dans cel_image et devrait être supprimée.
         */
        private function ajouterContrainteAuteurIntitule() {
                $auteurExplode = explode(' ', $this->parametres['masque.auteur']);
                $nbreMots = count($auteurExplode);

                if ($nbreMots == 1) {
                        $this->ajouterContrainteAuteurPrenomOuNom();
                } else if ($nbreMots == 2) {
                        $this->ajouterContrainteAuteurPrenomEtNom();
                }
        }

        private function ajouterContrainteAuteurPrenomOuNom() {
                $prenomOuNom = $this->parametres['masque.auteur'];

                $sqlTpl = 'CONCAT(%s,%s) LIKE %s';
                $champsPrenomSql = self::ajouterIfNullPourConcat($this->champsPrenom);
                $champsNomSql = self::ajouterIfNullPourConcat($this->champsNom);
                $auteurMotif = $this->bdd->proteger("%$prenomOuNom%");

                $auteurWhere = sprintf($sqlTpl, $champsPrenomSql, $champsNomSql, $auteurMotif);
                $this->addWhere('masque.auteur', $auteurWhere);
        }

        private function ajouterContrainteAuteurPrenomEtNom() {
                list($prenom, $nom) = explode(' ', $this->parametres['masque.auteur']);

                $sqlTpl = '(CONCAT(%1$s,%2$s) LIKE %3$s AND CONCAT(%1$s,%2$s) LIKE %4$s)';
                $champsPrenomSql = self::ajouterIfNullPourConcat($this->champsPrenom);
                $champsNomSql = self::ajouterIfNullPourConcat($this->champsNom);
                $prenomMotif = $this->bdd->proteger("%$prenom%");
                $nomMotif = $this->bdd->proteger("%$nom%");

                $auteurWhere = sprintf($sqlTpl, $champsPrenomSql, $champsNomSql, $prenomMotif, $nomMotif);
                $this->addWhere('masque.auteur', $auteurWhere);
        }

                /**
         * Lorsque l'on concatène des champs, un seul NULL prend le dessus,
         * Il faut donc utiliser la syntaxe IFNULL(%s, "").
         * (Cette fonction effectue aussi l'implode() "final"
         */
        private static function ajouterIfNullPourConcat($champs) {
                $champsProteges = array();
                foreach ($champs as $champ) {
                        $champsProteges[] = "IFNULL($champ, '')";
                }
                return implode(',', $champsProteges);
        }

        private function ajouterContrainteDate() {
                if (isset($this->parametres['masque.date'])) {
                        $date = $this->parametres['masque.date'];
                        if (is_integer($date) && $date < 2030 && $date > 1600) {
                                $sqlTpl = "YEAR(vdi.date_observation) = %d";
                                $dateWhere = sprintf($sqlTpl, $date);
                                $this->addWhere('masque.date', $dateWhere);
                        } else {
                                $sqlTpl = "DATE_FORMAT(vdi.date_observation, '%%Y-%%m-%%d') = %s";
                                $dateP = $this->bdd->proteger(strftime('%Y-%m-%d', $date));
                                $dateWhere = sprintf($sqlTpl, $dateP);
                                $this->addWhere('masque.date', $dateWhere);
                        }
                }
        }

        private function ajouterContrainteDepartement() {
                if (isset($this->parametres['masque.departement'])) {
                        $dept = $this->parametres['masque.departement'];
                        $deptMotif = $this->bdd->proteger("INSEE-C:$dept");
                        $this->addWhere('masque.departement', "vdi.ce_zone_geo LIKE $deptMotif");
                }
        }

        private function ajouterContrainteIdZoneGeo() {
                if (isset($this->parametres['masque.id_zone_geo'])) {
                        $idZgMotif = $this->bdd->proteger($this->parametres['masque.id_zone_geo']);
                        $this->addWhere('masque.id_zone_geo', "vdi.ce_zone_geo = $idZgMotif");
                }
        }

        private function ajouterContrainteGenre() {
                if (isset($this->parametres['masque.genre'])) {
                        $genre = $this->parametres['masque.genre'];
                        $genreMotif = $this->bdd->proteger("%$genre% %");
                        $this->addWhere('masque.genre', "vdi.nom_sel LIKE $genreMotif");
                }
        }

        private function ajouterContrainteFamille() {
                if (isset($this->parametres['masque.famille'])) {
                        $familleMotif = $this->bdd->proteger($this->parametres['masque.famille']);
                        $this->addWhere('masque.famille', "vdi.famille = $familleMotif");
                }
        }

        private function ajouterContrainteNs() {
                if (isset($this->parametres['masque.ns'])) {
                        $ns = $this->parametres['masque.ns'];
                        $nsMotif = $this->bdd->proteger("$ns%");
                        $this->addWhere('masque.ns', "vdi.nom_sel LIKE $nsMotif");
                }
        }

        private function ajouterContrainteNn() {
                if (isset($this->parametres['masque.nn'])) {
                        $sqlTpl = '(vdi.nom_sel_nn = %1$d OR vdi.nom_ret_nn = %1$d)';
                        $nnWhere = sprintf($sqlTpl, $this->parametres['masque.nn']);
                        $this->addWhere('masque.nn', $nnWhere);
                }
        }

        private function ajouterContrainteReferentiel() {
                if (isset($this->parametres['masque.referentiel'])) {
                        $ref = $this->parametres['masque.referentiel'];
                        $refMotif = $this->bdd->proteger("$ref%");
                        $this->addWhere('masque.referentiel', "vdi.nom_referentiel LIKE $refMotif");
                }
        }

        private function ajouterContrainteCommune() {
                if (isset($this->parametres['masque.commune'])) {
                        $commune = $this->parametres['masque.commune'];
                        $communeMotif = $this->bdd->proteger("$commune%");
                        $this->addWhere('masque.commune', "vdi.zone_geo LIKE $communeMotif");
                }
        }

        /**
         * in $p: un tableau de paramètres, dont:
         * - 'masque.tag_cel': *tableau* de mots-clefs à chercher parmi cel_image.mots_clefs_texte
         * - 'masque.tag_del': *tableau* de mots-clefs à chercher parmi del_image_tag.tag_normalise
         * - 'tag_explode_semantic': défini si les éléments sont tous recherchés ou NON
         *
         * in/ou: $req: un tableau de structure de requête MySQL
         *
         * Attention, le fait que nous cherchions masque.tag_cel OU/ET masque.tag_cel
         * ne dépend pas de nous, mais du niveau supérieur de construction de la requête:
         * Soit directement $this->consulter() si des masque.tag* sont passés
         * (split sur ",", "AND" entre chaque condition, "OR" pour chaque valeur de tag)
         * Soit via sqlAddMasqueConstraint():
         * (pas de split, "OR" entre chaque condition) [ comportement historique ]
         * équivalent à:
         * (split sur " ", "OR" entre chaque condition, "AND" pour chaque valeur de tag)
         *
         */
        public function ajouterConstrainteAppliImg() {
                $this->ajouterContrainteMilieu();
                $this->ajouterContrainteTri();
                $this->ajouterContrainteTagCel();
                $this->ajouterContrainteTagDel();
        }

        private function ajouterContrainteMilieu() {
                if (isset($this->parametres['masque.milieu'])) {
                        $milieu = $this->parametres['masque.milieu'];
                        $milieuMotif = $this->bdd->proteger("%$milieu%");
                        $this->addWhere('masque.milieu', "vdi.milieu LIKE $milieuMotif");
                }
        }

        /** Pour le tri par AVG() des votes nous avons toujours un protocole donné,
         * celui-ci indique sur quels votes porte l'AVG.
         * (c'est un *vote* qui porte sur un protocole et non l'image elle-même)
         * TODO: perf problème:
         * 1) SQL_CALC_FOUND_ROWS: fixable en:
         * - dissociant le comptage de la récup d'id + javascript async
         * - ou ne rafraîchir le total *que* pour les requête impliquant un changement de pagination
         * (paramètre booléen "with-total" par exemple)
         * 2) jointure forcées: en utilisant `del_imagè`, nous forçons les 2 premiers
         * JOIN sur cel_obs_images et cel_obs pour filtrer sur "transmission".
         * Dénormaliser cette valeur et l'intégrer à `cel_images` ferait économiser cette couteuse
         * jointure, ... lorsqu'aucun masque portant sur `cel_obs` n'est utilisé
         * 3) non-problème: l'ordre des joins est forcé par l'usage de la vue:
         * (cel_images/cel_obs_images/cel_obs/del_image_stat)
         * Cependant c'est à l'optimiseur de définir son ordre préféré.
         */
        private function ajouterContrainteTri() {
                if (isset($this->parametres['tri'])) {
                        $tri = $this->parametres['tri'];

                        if (isset($this->parametres['protocole'])  && ($tri == 'votes' || $tri == 'points')) {
                                // $this->parametres['protocole'] *est* défini (cf Outils::filtrerUrlsParams...())
                                $sqlTpl = 'LEFT JOIN del_image_stat dis ON vdi.id_image = dis.ce_image AND dis.ce_protocole = %d';
                                $triSql = sprintf($sqlTpl, $this->parametres['protocole']);
                                $this->addJoinDis($triSql);
                        }

                        if (isset($this->parametres['ordre']) && $tri == 'tags') {
                                $typeJointure = ($this->parametres['ordre'] == 'desc') ? 'INNER' : 'LEFT';
                                $this->addJoin("$typeJointure JOIN del_image_stat dis ON vdi.id_image = dis.ce_image");
                                // nécessaire (dup ce_image dans del_image_stat)
                                $this->addGroupBy('vdi.id_observation');
                        }
                }
        }

        /**
         * Car il ne sont pas traités par la générique requestFilterParams() les clefs "masque.tag_*"
         * sont toujours présentes; bien que parfois NULL.
         */
        // TODO: utiliser les tables de mots clefs normaliées dans tb_cel ? et auquel cas laisser au client le choix du couteux "%" ?
        private function ajouterContrainteTagCel() {
                if ($this->parametres['masque.tag_cel']) {
                        if (isset($this->parametres['masque.tag_cel']['AND'])) {
                                $tags = $this->parametres['masque.tag_cel']['AND'];
                                $clausesWhere = array();
                                foreach ($tags as $tag) {
                                        $tagMotif = $this->bdd->proteger("%$tag%");
                                        $sqlTpl = "CONCAT(IFNULL(vdi.mots_cles_texte,''),IFNULL(vdi.i_mots_cles_texte,'')) LIKE %s";
                                        $clausesWhere[] = sprintf($sqlTpl, $tagMotif);
                                }
                                $whereTags = implode(' AND ', $clausesWhere);
                                $this->addWhere('masque.tag_cel', "($whereTags)");
                        } else if (isset($this->parametres['masque.tag_cel']['OR'])) {
                                $tags = $this->parametres['masque.tag_cel']['OR'];
                                $sqlTpl = "CONCAT(IFNULL(vdi.mots_cles_texte,''),IFNULL(vdi.i_mots_cles_texte,'')) REGEXP %s";
                                $tagMotif = $this->bdd->proteger(implode('|', $tags));
                                $tagSql = sprintf($sqlTpl, $tagMotif);
                                $this->addWhere('masque.tag_cel', $tagSql);
                        }
                }
        }

        /**
         * Plusieurs solutions disponibles pour la gestion des contraintes des tags DEL :
         * - inutilisable pour l'instant : ajouterContrainteTagDelSolution1();
         * - intéressante, mais problème d'optimiseur MySQL 5.5 (dependant subquery) : ajouterContrainteTagDelSolution2();
         * - approche fiable mais sous-optimale : ajouterContrainteTagDelSolution3();
         */
        private function ajouterContrainteTagDel() {
                if (isset($this->parametres['masque.tag_del'])) {
                        $this->ajouterContrainteTagDelSolution3();
                }
        }

        /** Approche intéressante si les deux problèmes suivants peuvent être résolu:
         * - LEFT JOIN => dup => *gestion de multiples GROUP BY* (car in-fine un LIMIT est utilisé)
         * - dans le cas d'un ET logique, comment chercher les observations correspondantes ?
         */
        private function ajouterContrainteTagDelSolution1() {
                // XXX: utiliser tag plutôt que tag_normalise ?
                $req['join'][] = 'LEFT JOIN del_image_tag dit ON dit.ce_image = vdi.id_image';
                $req['where'][] = 'dit.actif = 1';
                $req['groupby'][] = 'vdi.id_image'; // TODO: nécessaire (car dup') mais risque de conflict en cas de tri (multiple GROUP BY)
                // XXX: en cas de ET, possibilité du GROUP_CONCAT(), mais probablement sans grand intérêt, d'où une boucle
                if (isset($p['masque.tag_del']['AND'])) {
                        // TODO/XXX : comment matcher les observations ayant tous les mots-clef passés ?
                        // ... le LEFT-JOIN n'y semble pas adapté
                } else {
                        $protected_tags = array();
                        foreach ($p['masque.tag_del']['OR'] as $tag) {
                                $protected_tags[] = $db->proteger(strtolower($tag));
                        }
                        $req['where'][] = sprintf('tag_normalise IN (%s)', implode(',', $protected_tags));
                }
        }

        /**
         * Inutilisé pour l'instant pour cause de soucis d'optimiseur MySQL (cf commentaire en intro)
         */
        private function ajouterContrainteTagDelSolution2() {
                // Note à propos des 4 "@ instruction" ci-dessous (notamment sur recupererTous())
                // REGEXP permet un puissant mécanisme de sélection des obs/image à qui sait
                // l'utiliser, mais peut sortir une erreur en cas de REGEXP invalide
                // ex: REGEX "^(".
                // Pour l'heure nous ignorons ce type d'erreur car aucun de nos champ de recherche
                // ne peuvent (ou ne devrait) comporter des meta-caractères
                // ([])?*+\\
                if (isset($p['masque.tag_del']['AND'])) {
                        // optimsation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
                        // donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
                        sort($p['masque.tag_del']['AND']);
                        $req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
                                " GROUP BY ce_image".
                                " HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP %s)",
                                $db->proteger(implode('.*', $p['masque.tag_del']['AND'])));
                } else {
                        $req['where'][] = sprintf("vdi.id_image IN (SELECT ce_image FROM del_image_tag WHERE actif = 1".
                                " GROUP BY ce_image".
                                " HAVING GROUP_CONCAT(tag_normalise) REGEXP %s)",
                                $db->proteger(implode('|', $p['masque.tag_del']['OR'])));
                }
        }

        /**
         * Si l'on est bassiné par les "DEPENDENT SUBQUERY", nous la faisons donc indépendemment via cette fonction
         */
        private function ajouterContrainteTagDelSolution3() {
                if (isset($this->parametres['masque.tag_del']['AND'])) {
                        $tags = $this->parametres['masque.tag_del']['AND'];
                        // optimisation: en cas de "AND" on sort() l'input et le GROUP_CONCAT()
                        // donc nous utilisons des ".*" plutôt que de multiples conditions et "|"
                        sort($tags);
                        $tagsMotif = $this->bdd->proteger(implode('.*', $tags));
                        $requete = 'SELECT ce_image '.
                                'FROM del_image_tag '.
                                'WHERE actif = 1 '.
                                'GROUP BY ce_image '.
                                "HAVING GROUP_CONCAT(tag_normalise ORDER BY tag_normalise) REGEXP $tagsMotif ".
                                ' -- '.__FILE__.' : '.__LINE__;
                        $sql = $this->recupererSqlContrainteTag($requete);
                        $this->addWhere('masque.tag_del', $sql);

                } else if (isset($this->parametres['masque.tag_del']['OR'])) {
                        $tags = $this->parametres['masque.tag_del']['OR'];
                        $tagsMotif = $this->bdd->proteger(implode('|', $tags));
                        $requete = 'SELECT ce_image '.
                                'FROM del_image_tag '.
                                'WHERE actif = 1 '.
                                'GROUP BY ce_image '.
                                "HAVING GROUP_CONCAT(tag_normalise) REGEXP $tagsMotif ".
                                ' -- '.__FILE__.' : '.__LINE__;
                        $sql = $this->recupererSqlContrainteTag($requete);
                        $this->addWhere('masque.tag_del', $sql);
                }
        }

        private function recupererSqlContrainteTag($requete) {
                $resultats = $this->bdd->recupererTous($requete);
                $ids = array();
                foreach ($resultats as $resultat) {
                        $ids[] = $resultat['ce_image'];
                }

                if (!empty($ids)) {
                        $clauseIn = implode(',', $ids);
                } else {
                        $clauseIn = 'SELECT ce_image FROM del_image_tag WHERE false';
                }
                return "vdi.id_image IN ($clauseIn)";
        }

        /**
         * Partie spécifique à PictoFlora:
         * génération de la clause ORDER BY (génère la valeur de la clef orderby' de $req)
         * nécessaire ? tableau sprintf(key (tri) => value (ordre), key => value ...).
         * Cependant il est impensable de joindre sur un AVG() des valeurs des votes pour
         * *chaque* couple (id_image, protocole) de la base afin de trouver les images
         * les "mieux notées", ou bien les images ayant le "plus de tags" (COUNT())
         */
        public function definirOrdreSqlAppliImg() {
                $ordre = $this->parametres['ordre'];

                // parmi self::$tri_possible
                switch ($this->parametres['tri']) {
                        case 'votes' :
                                $this->addOrderBy("dis.moyenne $ordre, dis.nb_votes $ordre");
                                break;
                        case 'points' :
                                $this->addOrderBy("dis.nb_points $ordre, dis.moyenne $ordre, dis.nb_votes $ordre");
                                break;
                        case 'tags' :
                                $this->addOrderBy("dis.nb_tags $ordre");
                                break;
                        case 'date_observation' :
                                $this->addOrderBy("date_observation $ordre, id_observation $ordre");
                                break;
                        default:
                                $this->addOrderBy("date_transmission $ordre, id_observation $ordre");
                }
        }

        public function getAliasDesChamps($champsEtAlias, $select = null, $prefix = null) {
                $arr = ($select) ? array_intersect_key($champsEtAlias, array_flip($select)) :  $champsEtAlias;
                $keys = array_keys($arr);

                if ($prefix) {
                        array_walk($keys, create_function('&$val, $k, $prefix', '$val = sprintf("%s.`%s`", $prefix, $val);'), $prefix);
                } else {
                        array_walk($keys, create_function('&$val, $k', '$val = sprintf("`%s`", $val);'));
                }

                return implode(', ', array_map(create_function('$v, $k', 'return sprintf("%s AS `%s`", $k, $v);'), $arr, $keys));
        }

                // Charger les images et leurs votes associés
        public function getVotesDesImages($idsImages, $protocole = null) {
                if (!$idsImages) return;

                $mappingVotes = $this->conteneur->getParametreTableau('votes.mapping');
                $mappingProtocoles = $this->conteneur->getParametreTableau('protocoles.mapping');
                $selectVotes = array('id_vote', 'ce_image', 'ce_protocole', 'ce_utilisateur', 'valeur', 'date');
                $selectProtocole = array('id_protocole', 'intitule', 'descriptif', 'tag');
                $voteChamps = $this->getAliasDesChamps($mappingVotes, $selectVotes, 'v'); // "v": cf alias dans la requête
                $protoChamps = $this->getAliasDesChamps($mappingProtocoles, $selectProtocole, 'p');
                $idImgsConcat = implode(',', $idsImages);

                $requete = "SELECT $voteChamps, $protoChamps ".
                        'FROM del_image_vote AS v '.
                        '       INNER JOIN del_image_protocole AS p ON (v.ce_protocole = p.id_protocole) '.
                        "WHERE v.ce_image IN ($idImgsConcat) ".
                        ($protocole ? " AND v.ce_protocole = $protocole " : '').
                        "ORDER BY FIELD(v.ce_image, $idImgsConcat) ".
                        '-- '.__FILE__.':'.__LINE__;

                return $this->bdd->recupererTous($requete);
        }

        /**
         * Ajoute les informations sur le protocole et les votes aux images.
         *
         * ATTENTION : Subtilité, nous passons ici le tableau d'images indexé par id_image qui est bien
         * plus pratique pour associer les vote à un tableau, puisque nous ne connaissons pas les id d'observation.
         * Mais magiquement (par référence), cela va remplir notre tableau indexé par couple d'id (id_image, id_observation)
         * cf ListeImages::reformateImagesDoubleIndex() à qui revient la tâche de créer ces deux versions
         * simultanément lorsque c'est encore possible.
         */
        public function ajouterInfosVotesProtocoles($votes, &$images) {
                if (!$votes) return;
                
                $mappingVotes = $this->conteneur->getParametreTableau('votes.mapping');
                $mappingProtocoles = $this->conteneur->getParametreTableau('protocoles.mapping');

                // pour chaque vote
                foreach ($votes as $vote) {
                        $imgId = $vote['image.id'];
                        $protoId = $vote['protocole.id'];

                        if (!array_key_exists('protocoles_votes', $images[$imgId]) ||
                           !array_key_exists($protoId, $images[$imgId]['protocoles_votes'])) {
                                // extrait les champs spécifique au protocole (le LEFT JOIN de chargerVotesImage les ramène en doublons
                                $protocole = array_intersect_key($vote, array_flip($mappingProtocoles));
                                $images[$imgId]['protocoles_votes'][$protoId] = $protocole;
                        }

                        $chpsVotes = array('id_vote', 'ce_image', 'ce_utilisateur', 'valeur', 'date');
                        $voteSelection = array_intersect_key($mappingVotes, array_flip($chpsVotes));
                        $vote = array_intersect_key($vote, array_flip($voteSelection));
                        $images[$imgId]['protocoles_votes'][$protoId]['votes'][$vote['vote.id']] = $vote;
                }
        }
}