65,9 → 65,10 |
|
class ListeImages2 { |
|
// TODO: PHP-x.y, ces variables devrait être des "const" |
static $format_image_possible = array('O','CRX2S','CRS','CXS','CS','XS','S','M','L','XL','X2L','X3L'); |
|
static $tri_possible = array('date_observation', 'votes', 'tags'); |
static $tri_possible = array('date_transmission', 'date_observation', 'votes', 'tags'); |
|
// en plus de ceux dans ListeObservations2 |
static $parametres_autorises = array('protocole', 'masque.tag_cel', 'masque.tag_pictoflora', 'masque.milieu'); |
142,18 → 143,16 |
array('masque.type')), |
$this->conteneur); |
|
// Cette variable est utile au filtrage des tags |
// Elle peut-être redéfinie en interne par sqlAddMasqueConstraint() ci-dessous |
// lorsque celui-ci transforme le masque général en masque par champ |
// et ré-appelle self::requestFilterParams() |
$parametres['tag_explode_char'] = ','; // " " (whitespace) ou "," ou NULL |
// tag_explode_semantic est lui utilisé lors de l'ajout des contraintes sur tags self::sqlAddConstraint() |
$parametres['tag_explode_semantic'] = 'OR'; // "AND" ou "OR" (si 'tag_explode_char' NOT NULL) |
// notre propre filtrage sur l'INPUT |
$params_pf = self::requestFilterParams($parametres, |
array_merge(ListeObservations2::$parametres_autorises, |
self::$parametres_autorises)); |
|
/* filtrage des tags + sémantique des valeurs multiples: |
Lorsqu'on utilise masque.tag* pour chercher des tags, ils sont |
postulés comme séparés par des virgule, et l'un au moins des tags doit matcher. */ |
$params_pf['masque.tag_cel'] = self::buildTagsAST(@$parametres['masque.tag_cel'], 'OR', ','); |
$params_pf['masque.tag_pictoflora'] = self::buildTagsAST(@$parametres['masque.tag_pictoflora'], 'OR', ','); |
|
$params = array_merge(ListeObservations2::$default_params, // paramètre par défaut Identiplante |
self::$default_params, // paramètres par défaut PictoFlora |
232,7 → 231,7 |
static function sqlOrderBy($p, $db, &$req) { |
// parmi self::$tri_possible |
if($p['tri'] == 'votes') { // LEFT JOIN sur "dis" ci-dessous |
$req['orderby'] = 'dis.moyenne ' . $p['ordre']; |
$req['orderby'] = 'dis.moyenne ' . $p['ordre'] . ', dis.nb_votes ' . $p['ordre']; |
return; |
} |
|
241,11 → 240,15 |
return; |
} |
|
if($p['tri'] == 'date_observation') { |
$req['orderby'] = 'date_observation ' . $p['ordre'] . ', id_observation ' . $p['ordre']; |
return; |
} |
|
// tri == 'date_transmission' |
$req['orderby'] = 'date_transmission ' . $p['ordre']; |
// avant cel:r1860, date_transmission pouvait être NULL |
// or nous voulons de la consistence (notamment pour phpunit) |
$req['orderby'] .= ', id_observation ' . $p['ordre']; |
$req['orderby'] = 'date_transmission ' . $p['ordre'] . ', id_observation ' . $p['ordre']; |
} |
|
/* |
315,53 → 318,51 |
$req['groupby'][] = 'vdi.id_observation'; |
} |
|
|
// TODO: support du "ET", "OU", "(", ")" |
// http://codehackit.blogspot.fr/2011/08/expression-parser-in-php.html |
// http://blog.angeloff.name/post/2012/08/05/php-recursive-patterns/ |
if(!empty($p['masque.tag_cel'])) { |
if($p['tag_explode_semantic'] == 'AND') { |
// 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. |
if($p['masque.tag_cel']) { |
if(isset($p['masque.tag_cel']['AND'])) { |
// TODO: utiliser les tables de mots clefs normaliées dans tb_cel ? |
// et auquel cas laisser au client le choix du couteux "%" ? |
$tags = $p['masque.tag_cel']; |
$tags = $p['masque.tag_cel']['AND']; |
array_walk($tags, create_function('&$val, $k, $db', |
'$val = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) LIKE %s", |
$db->proteger("%".$val."%"));'), |
$db); |
$req['where'][] = implode(' AND ', $tags); |
$req['where'][] = '(' . implode(' AND ', $tags) . ')'; |
} |
else { // OR assumed |
$req['where'][] = sprintf("CONCAT(vdi.mots_cles_texte,vdi.mots_cles_texte) REGEXP %s", |
$db->proteger(implode('|', $p['masque.tag_cel']))); |
else { |
$req['where'][] = sprintf("CONCAT(vdi.mots_cles_texte,vdi.i_mots_cles_texte) REGEXP %s", |
$db->proteger(implode('|', $p['masque.tag_cel']['OR']))); |
} |
} |
|
|
// XXX: utiliser tag plutôt que tag_normalise ? |
if(!empty($p['masque.tag_pictoflora'])) { |
if($p['masque.tag_pictoflora']) { |
// pas de LEFT JOIN ? ou bien peut-être en cas de tri, mais nous parlons bien ici d'un masque |
/* $req['join'][] = 'LEFT JOIN del_image_tag dit ON dit.ce_image = vdi.id_image'; |
$req['where'][] = 'dit.actif = 1'; */ |
|
if($p['tag_explode_semantic'] == 'AND') { |
if(isset($p['masque.tag_pictoflora']['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_pictoflora']); |
sort($p['masque.tag_pictoflora']['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_pictoflora']))); |
$db->proteger(implode('.*', $p['masque.tag_pictoflora']['AND']))); |
} |
else { // OR assumed |
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_pictoflora']))); |
$db->proteger(implode('|', $p['masque.tag_pictoflora']['OR']))); |
} |
} |
|
} |
|
|
static function getIdImages($p, $req, $db) { |
return $db->recupererTous(sprintf( |
'SELECT SQL_CALC_FOUND_ROWS id_image' . |
412,9 → 413,10 |
'masque.commune' => $p['masque'], // TODO/XXX ? |
'masque.id_zone_geo' => $p['masque'], |
|
// tous-deux remplacent masque.tag |
'masque.tag_cel' => $p['masque'], |
'masque.tag_pictoflora' => $p['masque'], |
/* tous-deux remplacent masque.tag |
mais sont traité séparément des requestFilterParams() */ |
// 'masque.tag_cel' => $p['masque'], |
// 'masque.tag_pictoflora' => $p['masque'], |
|
'masque.ns' => $p['masque'], |
'masque.famille' => $p['masque'], |
428,10 → 430,15 |
|
$or_masque = array_merge( |
ListeObservations2::requestFilterParams($or_params, NULL, $c /* pour masque.departement */), |
self::requestFilterParams($or_params), |
array('tag_explode_char' => ' ', 'tag_explode_semantic' => 'AND')); // spéciaux |
self::requestFilterParams($or_params)); |
|
// pas de select, groupby & co ici |
/* Lorsqu'on utilise le masque général pour chercher des tags, ils sont |
postulés comme séparés par des espaces, et doivent être tous matchés. */ |
$or_masque['masque.tag_cel'] = self::buildTagsAST($p['masque'], 'AND', ' '); |
$or_masque['masque.tag_pictoflora'] = self::buildTagsAST($p['masque'], 'AND', ' '); |
|
|
// pas de select, groupby & co ici: uniquement 'join' et 'where' |
$or_req = array('join' => array(), 'where' => array()); |
ListeObservations2::sqlAddConstraint($or_masque, $db, $or_req); |
self::sqlAddConstraint($or_masque, $db, $or_req); |
444,7 → 451,7 |
} |
} |
|
// complete & override ListeObservations2::requestFilterParams() |
// complete & override ListeObservations2::requestFilterParams() (même usage) |
static function requestFilterParams(Array $params, $parametres_autorises = NULL) { |
if($parametres_autorises) { // filtrage de toute clef inconnue |
$params = array_intersect_key($params, array_flip($parametres_autorises)); |
454,7 → 461,7 |
$p['tri'] = ListeObservations2::unsetIfInvalid($params, 'tri', self::$tri_possible); |
$p['format'] = ListeObservations2::unsetIfInvalid($params, 'format', self::$format_image_possible); |
|
// et pour identiplantes ? |
// "milieu" inutile pour IdentiPlantes ? |
if(isset($params['masque.milieu'])) $p['masque.milieu'] = trim($params['masque.milieu']); |
|
// compatibilité |
462,35 → 469,6 |
$params['masque.tag_cel'] = $params['masque.tag_pictoflora'] = $params['masque.tag']; |
} |
|
/* Nous n'implod()ons pas ici, car la sémantique des éléments multiples est |
variable (tag_explode_semantic) et considérée plus tard, notamment lors de la |
construction du SQL (sqlAddConstraint()). |
Cette sémantique peut varier selon que ces tags proviennent du formulaire de |
recherche avancée ou via le masque général (et l'appel récursif à requestFilterParams() |
qu'il implique) */ |
if(isset($params['masque.tag_cel'])) { |
if(isset($params['tag_explode_char'])) { |
$x = explode($params['tag_explode_char'], $params['masque.tag_cel']); |
$x = array_map('trim', $x); |
$p['masque.tag_cel'] = array_filter($x); |
} else { |
// toujours un tableau |
$p['masque.tag_cel'] = array(trim($params['masque.tag_cel'])); |
} |
|
} |
|
if(isset($params['masque.tag_pictoflora'])) { |
if(isset($params['tag_explode_char'])) { |
$x = explode($params['tag_explode_char'], $params['masque.tag_pictoflora']); |
$x = array_map('trim', $x); |
$p['masque.tag_pictoflora'] = array_filter($x); |
} else { |
// toujours un tableau |
$p['masque.tag_pictoflora'] = array(trim($params['masque.tag_pictoflora'])); |
} |
} |
|
if($p['tri'] == 'votes' || $p['tri'] == 'tags') { |
// ces critère de tri des image à privilégier ne s'applique qu'à un protocole donné |
if(!isset($params['protocole']) || !is_numeric($params['protocole'])) |
503,23 → 481,95 |
} |
|
|
/* Construit un (vulgaire) abstract syntax tree: |
"AND" => [ "tag1", "tag2" ] |
Idéalement (avec un parser simple comme proposé par http://hoa-project.net/Literature/Hack/Compiler.html#Langage_PP) |
nous aurions: |
"AND" => [ "tag1", "tag2", "OR" => [ "tag3", "tag4" ] ] |
|
Ici nous devons traiter les cas suivants: |
tags séparés par des "ET/AND OU/OR", séparés par des espaces ou des virgules. |
Mais la chaîne peut aussi avoir été issue du "masque général" (la barre de recherche générique). |
ce qui implique des comportement par défaut différents afin de préserver la compatibilité. |
|
Théorie: |
1) tags passés par "champ tag": |
- support du ET/OU, et explode par virgule. |
- si pas d'opérande détectée: "OU" |
|
2) tags passés par "recherche générale": |
- support du ET/OU, et explode par whitespace. |
- si pas d'opérande détectée: "ET" |
|
La présence de $additional_sep s'explique car ET/OU sous-entendent une séparation par des espaces. |
Mais ce n'est pas toujours pertinent car: 1) la compatibilité suggère de considérer parfois |
la virgule comme séparateur et 2) les tags *peuvent* contenir des espaces. Par conséquent: |
* a,b,c => "a" $default_op "b" $default_op "c" |
* a,b AND c => "a" AND "b" AND "c" |
* a OR b AND c,d => "a" AND "b" AND "c" AND "d" |
C'est à dire par ordre décroissant de priorité: |
1) opérande contenu dans la chaîne |
2) opérande par défaut |
3) les séparateurs présents sont substitués par l'opérande déterminée par 1) ou 2) |
|
// TODO: support des parenthèses, imbrications & co: "(", ")" |
// http://codehackit.blogspot.fr/2011/08/expression-parser-in-php.html |
// http://blog.angeloff.name/post/2012/08/05/php-recursive-patterns/ |
|
@param $str: la chaîne à "parser" |
@param $default_op: "AND" ou "OR" |
@param $additional_sep: séparateur de mots: |
*/ |
static function buildTagsAST($str = NULL, $default_op, $additional_sep = ',') { |
if(!$str) return; |
$words = preg_split('/ (OR|AND|ET|OU) /', $str, -1, PREG_SPLIT_NO_EMPTY); |
|
if(preg_match('/\b(ET|AND)\b/', $str)) $op = 'AND'; |
elseif(preg_match('/\b(OU|OR)\b/', $str)) $op = 'OR'; |
else $op = $default_op; |
|
if($additional_sep) { |
array_walk($words, |
create_function('&$v, $k, $sep', '$v = preg_split("/".$sep."/", $v, -1, PREG_SPLIT_NO_EMPTY);'), |
$additional_sep); |
} |
$words = self::array_flatten($words); |
$words = array_map('trim', $words); |
return array($op => array_filter($words)); |
} |
|
|
// met à jour *toutes* les stats de nombre de tags et de moyenne des votes |
static function _update_statistics($db) { |
$db->requeter("TRUNCATE TABLE del_image_stat"); |
$db->requeter(<<<EOF |
INSERT INTO `del_image_stat` ( |
SELECT id_image, divo.ce_protocole, divo.moyenne, dit.ctags |
FROM `BASECEL`.`cel_images` ci |
LEFT JOIN |
( SELECT ce_image, ce_protocole, AVG(valeur) AS moyenne FROM del_image_vote |
GROUP BY ce_image, ce_protocole ) AS divo |
ON ci.id_image = divo.ce_image |
LEFT JOIN |
( SELECT ce_image, COUNT(id_tag) as ctags FROM del_image_tag |
GROUP BY ce_image ) AS dit |
ON ci.id_image = dit.ce_image ) |
SELECT id_image, divo.ce_protocole, divo.moyenne, divo.nb_votes, dit.ctags |
FROM `tb_cel`.`cel_images` ci |
LEFT JOIN |
( SELECT ce_image, ce_protocole, AVG(valeur) AS moyenne, COUNT(valeur) AS nb_votes FROM del_image_vote |
GROUP BY ce_image, ce_protocole ) AS divo |
ON ci.id_image = divo.ce_image |
LEFT JOIN |
( SELECT ce_image, COUNT(id_tag) as ctags FROM del_image_tag |
GROUP BY ce_image ) AS dit |
ON ci.id_image = dit.ce_image ) |
EOF |
); |
} |
} |
|
static function revOrderBy($orderby) { |
return $orderby == 'asc' ? 'desc' : 'asc'; |
} |
|
static function array_flatten($arr) { |
$arr = array_values($arr); |
while (list($k,$v)=each($arr)) { |
if (is_array($v)) { |
array_splice($arr,$k,1,$v); |
next($arr); |
} |
} |
return $arr; |
} |
} |