Subversion Repositories eFlore/Applications.del

Rev

Rev 1889 | 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.
 * Rempli un tableau des clauses "join", "where", "group by" et "oder by" nécessaire à la *recherche* des ids des
 * observations/images correspondantes aux filtres passés dans l'url du web service de recherche
 * (ListeImages et ListeObservations).
 *
 * Attention, cela signifie que toutes les tables ne sont pas *forcément* jointées, par exemple si aucune
 * contrainte ne le nécessite.
 * La requête construite ici est utile pour récupérer la liste des ids d'observations/images qui match.
 * Pour la récupération effective de "toutes" les données nécessaire au retour du web service en json, c'est une autre
 * requête directement dans le web service qui s'en charge. Cette technique en deux étapes est la plus rapide !
 *
 * Note: toujours rajouter les préfixes de table (di, do ou du), en fonction de ce que défini
 * les JOIN qui sont utilisés :
 * - le préfix de del_image est "di"
 * - le préfix de del_observation est "do"
 * - le préfix de del_utilisateur est "du"
 *
 * @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 {

        const APPLI_IMG = 'IMG';
        const APPLI_OBS = 'OBS';

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

        private $champsPrenom = array('du.prenom', 'prenom_utilisateur');
        private $champsNom = array('du.nom',  '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 setAppli($appliType) {
                if ($appliType == 'IMG' || $appliType == 'OBS') {
                        $this->appli = $appliType;
                } else {
                        throw new Exception("Les types d'appli disponible sont : IMG (pour PictoFlora) et OBS (pour IdentiPlante)");
                }
        }

        private function getPrefixe() {
                return $this->appli === 'IMG' ? 'di' : 'do';
        }

        private function etreAppliImg() {
                return $this->appli === 'IMG' ? true : false;
        }

        private function etreAppliObs() {
                return $this->appli === 'OBS' ? true : false;
        }

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

        private function addJoin($join) {
                if (!isset($this->requete['join'][$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'].' ';
        }

        /**

         *
         * @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
                        $prefixe = $this->getPrefixe();
                        $this->addJoin("LEFT JOIN del_utilisateur AS du ON (du.id_utilisateur = $prefixe.ce_utilisateur) ");

                        if (is_numeric($auteur)) {
                                $this->ajouterContrainteAuteurId();
                        } elseif(preg_match('/@[a-z0-9-]+(?:\.[a-z0-9-]+)*\.[a-z]{2,}$/i', $auteur)) {
                                $this->ajouterContrainteAuteurEmail();
                        } else {
                                $this->ajouterContrainteAuteurIntitule();
                        }
                }
        }

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

        private function ajouterContrainteAuteurEmail() {
                $email = $this->parametres['masque.auteur'];
                $prefixe = $this->getPrefixe();
                $sqlTpl = "(du.courriel LIKE %1\$s OR $prefixe.courriel_utilisateur 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'
         */
        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';
                $prefixe = $this->getPrefixe();
                $champsPrenomSql = self::ajouterIfNullPourConcat($this->champsPrenom, $prefixe);
                $champsNomSql = self::ajouterIfNullPourConcat($this->champsNom, $prefixe);
                $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)';
                $prefixe = $this->getPrefixe();
                $champsPrenomSql = self::ajouterIfNullPourConcat($this->champsPrenom, $prefixe);
                $champsNomSql = self::ajouterIfNullPourConcat($this->champsNom, $prefixe);
                $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, $prefixe) {
                $champsProteges = array();
                foreach ($champs as $champ) {
                        if (strstr($champ, '.') === false) {
                                $champ = "$prefixe.$champ";
                        }
                        $champsProteges[] = "IFNULL($champ, '')";
                }
                return implode(',', $champsProteges);
        }

        private function ajouterContrainteDate() {
                if (isset($this->parametres['masque.date'])) {
                        $date = $this->parametres['masque.date'];
                        if (preg_match('/^\d{4}$/', $date) && $date < 2030 && $date > 1600) {
                                $sqlTpl = "YEAR(do.date_observation) = %d";
                                $dateWhere = sprintf($sqlTpl, $date);
                                $this->addWhere('masque.date', $dateWhere);
                        } else {
                                $sqlTpl = "do.date_observation = %s";
                                $dateP = $this->bdd->proteger($date);
                                $dateWhere = sprintf($sqlTpl, $dateP);
                                $this->addWhere('masque.date', $dateWhere);
                        }

                        if ($this->etreAppliImg()) {
                                $this->addJoin('LEFT JOIN del_observation AS do ON (di.ce_observation = do.id_observation) ');
                        }
                }
        }

        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', "do.ce_zone_geo LIKE $deptMotif");

                        if ($this->etreAppliImg()) {
                                $this->addJoin('LEFT JOIN del_observation AS do ON (di.ce_observation = do.id_observation) ');
                        }
                }
        }

        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', "do.ce_zone_geo = $idZgMotif");

                        if ($this->etreAppliImg()) {
                                $this->addJoin('LEFT JOIN del_observation AS do ON (di.ce_observation = do.id_observation) ');
                        }
                }
        }

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

                        if ($this->etreAppliImg()) {
                                $this->addJoin('LEFT JOIN del_observation AS do ON (di.ce_observation = do.id_observation) ');
                        }
                }
        }

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

                        if ($this->etreAppliImg()) {
                                $this->addJoin('LEFT JOIN del_observation AS do ON (di.ce_observation = do.id_observation) ');
                        }
                }
        }

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

                        if ($this->etreAppliImg()) {
                                $this->addJoin('LEFT JOIN del_observation AS do ON (di.ce_observation = do.id_observation) ');
                        }
                }
        }

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

                        if ($this->etreAppliImg()) {
                                $this->addJoin('LEFT JOIN del_observation AS do ON (di.ce_observation = do.id_observation) ');
                        }
                }
        }

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

                        if ($this->etreAppliImg()) {
                                $this->addJoin('LEFT JOIN del_observation AS do ON (di.ce_observation = do.id_observation) ');
                        }
                }
        }

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

                        if ($this->etreAppliImg()) {
                                $this->addJoin('LEFT JOIN del_observation AS do ON (di.ce_observation = do.id_observation) ');
                        }
                }
        }

        public function ajouterConstrainteAppliObs() {
                $this->ajouterContrainteTagCel();
                $this->ajouterContrainteType();
                // TODO : ATTENTION -> vue que l'on utilise une vue basée sur les images, nous devons grouper par obs
                $this->addGroupBy('do.id_observation');
        }

        private function ajouterContrainteType() {
                if (isset($this->parametres['masque.type'])) {
                        if (array_key_exists('adeterminer', $this->parametres['masque.type'])) {
                                // Récupèration de toutes les observations qui on le tag "aDeterminer" *ou* qui n'ont pas de nom d'espèce
                                // *ou* qui ont la "certitude" à ("aDeterminer" *ou* "douteux")
                                $this->addWhere('masque.type', '('.
                                        'do.certitude = "aDeterminer" '.
                                        'OR do.certitude = "douteux" '.
                                        'OR do.mots_cles_texte LIKE "%aDeterminer%" '.
                                        'OR do.nom_sel_nn IS NULL '.
                                        'OR do.nom_sel_nn = 0 '.// il ne DEVRAIT pas y avoir d'entrées à 0, mais il y en a quand-même !!
                                        ')');
                        }
                        if (array_key_exists('validees', $this->parametres['masque.type'])) {
                                // Récupèration de toutes les observations ayant un commentaire doté de proposition_retenue = 1
                                $this->addJoin('INNER JOIN del_commentaire AS dc '.
                                        'ON (do.id_observation = dc.ce_observation AND dc.proposition_retenue = 1) ');
                        }

                        if (array_key_exists('endiscussion', $this->parametres['masque.type'])) {
                                $nbreCommentaire =(int) ($this->conteneur->getParametre('observations.nb_commentaires_discussion'));
                                $this->addWhere('masque.type', '(SELECT COUNT(id_commentaire) FROM del_commentaire AS dc '.
                                        "WHERE ce_observation = id_observation) > $nbreCommentaire ");
                        }

                        if ($this->etreAppliImg()) {
                                $this->addJoin('LEFT JOIN del_observation AS do ON (di.ce_observation = do.id_observation) ');
                        }
                }
        }

        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', "do.milieu LIKE $milieuMotif");

                        if ($this->etreAppliImg()) {
                                $this->addJoin('LEFT JOIN del_observation AS do ON (di.ce_observation = do.id_observation) ');
                        }
                }
        }

        private function ajouterContrainteTri() {
                if (isset($this->parametres['tri'])) {
                        $tri = $this->parametres['tri'];

                        if (isset($this->parametres['protocole'])  && ($tri == 'moyenne-arithmetique' || $tri == 'points')) {
                                // $this->parametres['protocole'] *est* défini (cf Outils::filtrerUrlsParams...())
                                $sqlTpl = 'LEFT JOIN del_image_stat AS dis ON di.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 AS dis ON di.id_image = dis.ce_image");
                        }
                }
        }

        private function ajouterContrainteTagCel() {
                if (isset($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(do.mots_cles_texte,''),IFNULL(di.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(do.mots_cles_texte,''),IFNULL(di.mots_cles_texte,'')) REGEXP %s";
                                $tagMotif = $this->bdd->proteger(implode('|', $tags));
                                $tagSql = sprintf($sqlTpl, $tagMotif);
                                $this->addWhere('masque.tag_cel', $tagSql);
                        }
                        if ($this->etreAppliImg()) {
                                $this->addJoin('LEFT JOIN del_observation AS do ON (di.ce_observation = do.id_observation) ');
                        }
                        if ($this->etreAppliObs()) {
                                $this->addJoin('LEFT JOIN del_image AS di ON (do.id_observation = di.ce_observation) ');
                        }
                }
        }

        /**
         * Plusieurs solutions sont disponibles dans les anciennes versions (voir DelTk et l'historique SVN de ce fichier).
         */
        private function ajouterContrainteTagDel() {
                if (isset($this->parametres['masque.tag_del'])) {
                        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 "di.id_image IN ($clauseIn)";
        }

        /**
         * Partie spécifique à PictoFlora:
         * Attention : si le critère de tri n'est pas suffisant, les résultats affichés peuvent varier à chaque appel
         * de la même page de résultat de PictoFlora.
         */
        public function definirOrdreSqlAppliImg() {
                $ordre = $this->parametres['ordre'];

                switch ($this->parametres['tri']) {
                        case 'moyenne-arithmetique' :
                                $this->addOrderBy("dis.moyenne $ordre, dis.nb_votes $ordre, id_image $ordre");
                                break;
                        case 'points' :
                                $this->addOrderBy("dis.nb_points $ordre, dis.moyenne $ordre, dis.nb_votes $ordre, id_image $ordre");
                                break;
                        case 'tags' :
                                $this->addOrderBy("dis.nb_tags $ordre, id_image $ordre");
                                break;
                        case 'date_observation' :
                                $this->addOrderBy("date_observation $ordre, ce_observation $ordre");
                                break;
                        case 'date_transmission' :
                        default:
                                $this->addOrderBy("di.date_transmission $ordre, ce_observation $ordre");
                }
        }

        public function definirOrdreSqlAppliObs() {
                $ordre = $this->parametres['ordre'];

                // parmi self::$tri_possible
                switch ($this->parametres['tri']) {
                        case 'date_observation' :
                                $this->addOrderBy("date_observation $ordre, id_observation $ordre");
                                break;
                        default:
                                $this->addOrderBy("do.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));
        }

        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.
         */
        // TODO : supprimer cette "subtilité" source d'erreurs
        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;
                }
        }

        public function getTotalLignesTrouvees() {
                $resultat = $this->bdd->recuperer('SELECT FOUND_ROWS() AS nbre -- '.__FILE__.':'.__LINE__);
                return intval($resultat['nbre']);
        }
}