Subversion Repositories eFlore/Applications.del

Rev

Rev 1486 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 1486 Rev 1490
Line 20... Line 20...
20
 * Par contre, le tri par moyenne des votes, sous-entend "pour un protocole donné".
20
 * Par contre, le tri par moyenne des votes, sous-entend "pour un protocole donné".
21
 * Dès lors le choix d'un protocole doit avoir été fait afin de régler le JOIN et ainsi l'ORDER BY.
21
 * Dès lors le choix d'un protocole doit avoir été fait afin de régler le JOIN et ainsi l'ORDER BY.
22
 * (cf requestFilterParams())
22
 * (cf requestFilterParams())
23
 *
23
 *
24
 * Histoire: auparavant (pré-r142x) un AVG + GROUP BY étaient utilisés pour générer on-the-fly les valeurs
24
 * Histoire: auparavant (pré-r142x) un AVG + GROUP BY étaient utilisés pour générer on-the-fly les valeurs
25
 * utilsées ensuite pour l'ORDER BY. La situation à base de del_image_stat
25
 * utilisées ensuite pour l'ORDER BY. La situation à base de del_image_stat
26
 * est déjà bien meilleur sans être pour autant optimale. cf commentaire de sqlAddConstraint()
26
 * est déjà bien meilleure sans être pour autant optimale. cf commentaire de sqlAddConstraint()
27
 *
27
 *
28
 *
28
 *
29
 * Tags:
29
 * Tags:
30
 * Le comportement habituel dans le masque *général*: les mots sont séparés par des espaces,
30
 * Le comportement habituel dans le masque *général*: les mots sont séparés par des espaces,
31
 * implod()ed par des AND (tous les mots doivent matcher).
31
 * implod()ed par des AND (tous les mots doivent matcher).
Line 45... Line 45...
45
 * - subqueries dans le FROM pour les critère WHERE portant directement sur v_del_image
45
 * - subqueries dans le FROM pour les critère WHERE portant directement sur v_del_image
46
 * plutôt que dans WHERE (qui nécessite dès lors un FULL-JOIN)
46
 * plutôt que dans WHERE (qui nécessite dès lors un FULL-JOIN)
47
 * (http://www.mysqlperformanceblog.com/2007/04/06/using-delayed-join-to-optimize-count-and-limit-queries/)
47
 * (http://www.mysqlperformanceblog.com/2007/04/06/using-delayed-join-to-optimize-count-and-limit-queries/)
48
 * - éviter de dépendre d'une jointure systématique sur `cel_obs`, uniquement pour `(date_)transmission
48
 * - éviter de dépendre d'une jointure systématique sur `cel_obs`, uniquement pour `(date_)transmission
49
 * (cf VIEW del_image)
49
 * (cf VIEW del_image)
50
 * - réorganiser les méthodes statiques parmis Observation, ListeObservations et ListImages2
50
 * - poursuivre la réorganisation des méthodes statiques parmis Observation, ListeObservations et ListImages2
51
 * - *peut-être*: passer requestFilterParams() en méthode de classe
51
 * - *peut-être*: passer requestFilterParams() en méthode de classe
52
 *
52
 *
53
 *
53
 *
54
 * MySQL sux:
54
 * MySQL sux:
55
 * 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);
55
 * 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);
Line 63... Line 63...
63
 *	5.1: DEPENDENT SUBQUERY	del_image_tag	index_subquery	ce_image ce_image 8 func 1 Using where
63
 *	5.1: DEPENDENT SUBQUERY	del_image_tag	index_subquery	ce_image ce_image 8 func 1 Using where
64
 * FORCE INDEX/IGNORE INDEX semble incapable de résoudre le problème de l'optimiseur MySQL
64
 * FORCE INDEX/IGNORE INDEX semble incapable de résoudre le problème de l'optimiseur MySQL
65
 *
65
 *
66
 */
66
 */
Line -... Line 67...
-
 
67
 
67
 
68
require_once(dirname(__FILE__) . '/../DelTk.php');
68
require_once(dirname(__FILE__) . '/../observations/ListeObservations.php');
69
require_once(dirname(__FILE__) . '/../observations/ListeObservations.php');
69
require_once(dirname(__FILE__) . '/../observations/Observation.php');
70
require_once(dirname(__FILE__) . '/../observations/Observation.php');
70
restore_error_handler();
71
restore_error_handler();
71
restore_exception_handler();
72
restore_exception_handler();
Line 81... Line 82...
81
	// TODO: PHP-x.y, ces variables devrait être des "const"
82
	// TODO: PHP-x.y, ces variables devrait être des "const"
82
	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');
Line 83... Line 84...
83
 
84
 
Line 84... Line 85...
84
	static $tri_possible = array('date_transmission', 'date_observation', 'votes', 'tags');
85
	static $tri_possible = array('date_transmission', 'date_observation', 'votes', 'tags');
85
 
86
 
Line 86... Line 87...
86
	// en plus de ceux dans ListeObservations
87
	// en plus de ceux dans DelTk
87
	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');
88
 
89
 
Line 149... Line 150...
149
 
150
 
150
		// 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
151
		// ($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.
152
		// É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
153
		// 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).
154
		$params_ip = ListeObservations::requestFilterParams($parametres,
155
		$params_ip = DelTk::requestFilterParams($parametres,
155
        array_diff(ListeObservations::$parametres_autorises,
156
        array_diff(DelTk::$parametres_autorises,
156
        array('masque.type')),
157
        array('masque.type')),
Line 157... Line 158...
157
        $this->conteneur);
158
        $this->conteneur);
158
 
159
 
159
		// notre propre filtrage sur l'INPUT
160
		// notre propre filtrage sur l'INPUT
160
		$params_pf = self::requestFilterParams($parametres,
161
		$params_pf = self::requestFilterParams($parametres,
Line 161... Line 162...
161
        array_merge(ListeObservations::$parametres_autorises,
162
        array_merge(DelTk::$parametres_autorises,
162
        self::$parametres_autorises));
163
        self::$parametres_autorises));
163
 
164
 
164
		/* filtrage des tags + sémantique des valeurs multiples:
165
		/* filtrage des tags + sémantique des valeurs multiples:
165
		   Lorsqu'on utilise masque.tag* pour chercher des tags, ils sont
166
		   Lorsqu'on utilise masque.tag* pour chercher des tags, ils sont
Line -... Line 167...
-
 
167
		   postulés comme séparés par des virgule, et l'un au moins des tags doit matcher. */
166
		   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', ',');
167
		$params_pf['masque.tag_cel'] = self::buildTagsAST(@$parametres['masque.tag_cel'], 'OR', ',');
169
		$params_pf['masque.tag_pictoflora'] = DelTk::buildTagsAST(@$parametres['masque.tag_pictoflora'], 'OR', ',');
168
		$params_pf['masque.tag_pictoflora'] = self::buildTagsAST(@$parametres['masque.tag_pictoflora'], 'OR', ',');
170
 
169
 
171
		$params = array_merge(
Line 170... Line 172...
170
		$params = array_merge(ListeObservations::$default_params, // paramètre par défaut Identiplante
172
            DelTk::$default_params, // paramètre par défaut Identiplante
171
        self::$default_params, // paramètres par défaut PictoFlora
173
            self::$default_params, // paramètres par défaut PictoFlora
172
        $params_ip, // les paramètres passés, traités par Identiplante
174
            $params_ip, // les paramètres passés, traités par Identiplante
Line 173... Line 175...
173
        $params_pf); // les paramètres passés, traités par PictoFlora
175
            $params_pf); // les paramètres passés, traités par PictoFlora
-
 
176
 
-
 
177
		// XXX: temp tweak
174
 
178
		/* $this->conteneur->setParametre('url_images', sprintf($this->conteneur->getParametre('url_images'),
175
		// XXX: temp tweak
179
		   "%09d", $params['format']));*/
176
		/* $this->conteneur->setParametre('url_images', sprintf($this->conteneur->getParametre('url_images'),
180
 
177
		   "%09d", $params['format']));*/
181
		// création des contraintes (génériques de DelTk)
178
 
182
		DelTk::sqlAddConstraint($params, $db, $req);
Line 190... Line 194...
190
		$idobs_tab = self::getIdImages($params, $req, $db);
194
		$idobs_tab = self::getIdImages($params, $req, $db);
Line 191... Line 195...
191
 
195
 
192
		// Ce n'est pas la peine de continuer s'il n'y a pas eu de résultats dans la table del_obs_images
196
		// Ce n'est pas la peine de continuer s'il n'y a pas eu de résultats dans la table del_obs_images
193
		if(!$idobs_tab) {
197
		if(!$idobs_tab) {
194
			$resultat = new ResultatService();
198
			$resultat = new ResultatService();
195
			$resultat->corps = array('entete' => ListeObservations::makeJSONHeader(0, $params, Config::get('url_service')),
199
			$resultat->corps = array('entete' => DelTk::makeJSONHeader(0, $params, Config::get('url_service')),
196
            'resultats' => array());
200
            'resultats' => array());
197
			return $resultat;
201
			return $resultat;
198
			/*
202
			/*
199
              header('HTTP/1.0 404 Not Found');
203
              header('HTTP/1.0 404 Not Found');
Line 214... Line 218...
214
        $o = new Observation($this->conteneur);
218
        $o = new Observation($this->conteneur);
215
        foreach($idobs as $i) {
219
        foreach($idobs as $i) {
216
        $images[$i] = $o->consulter(array($i), array('justthrow' => 1));
220
        $images[$i] = $o->consulter(array($i), array('justthrow' => 1));
217
        }
221
        }
218
		*/
222
		*/
219
		list($images, $images_keyed_by_id_image) = ListeObservations::reformateImagesDoubleIndex(
223
		list($images, $images_keyed_by_id_image) = self::reformateImagesDoubleIndex(
220
			$liaisons,
224
			$liaisons,
221
			$this->conteneur->getParametre('url_images'),
225
			$this->conteneur->getParametre('url_images'),
222
			$params['format']);
226
			$params['format']);
Line 235... Line 239...
235
		// les deux masques de tags sont transformés en AST dans le processus de construction de la requête.
239
		// les deux masques de tags sont transformés en AST dans le processus de construction de la requête.
236
		// Reprenous les paramètres originaux non-nettoyés (ils sont valables car le nettoyage est déterministe)
240
		// Reprenous les paramètres originaux non-nettoyés (ils sont valables car le nettoyage est déterministe)
237
		$params_header = array_merge($params, array_filter(array('masque.tag_cel' => @$parametres['masque.tag_cel'],
241
		$params_header = array_merge($params, array_filter(array('masque.tag_cel' => @$parametres['masque.tag_cel'],
238
        'masque.tag_pictoflora' => @$parametres['masque.tag_pictoflora'])));
242
        'masque.tag_pictoflora' => @$parametres['masque.tag_pictoflora'])));
239
		$resultat = new ResultatService();
243
		$resultat = new ResultatService();
240
		$resultat->corps = array('entete' => ListeObservations::makeJSONHeader($total, $params_header, Config::get('url_service')),
244
		$resultat->corps = array('entete' => DelTk::makeJSONHeader($total, $params_header, Config::get('url_service')),
241
        'resultats' => $images);
245
        'resultats' => $images);
242
		return $resultat;
246
		return $resultat;
243
	}
247
	}
Line 244... Line 248...
244
 
248
 
Line 290... Line 294...
290
	 * équivalent à:
294
	 * équivalent à:
291
	 * (split sur " ", "OR" entre chaque condition, "AND" pour chaque valeur de tag)
295
	 * (split sur " ", "OR" entre chaque condition, "AND" pour chaque valeur de tag)
292
	 *
296
	 *
293
	 */
297
	 */
294
	static function sqlAddConstraint($p, $db, &$req, Conteneur $c = NULL) {
298
	static function sqlAddConstraint($p, $db, &$req, Conteneur $c = NULL) {
295
		// TODO implement dans ListeObservations ?
299
		// TODO implement dans DelTk ?
296
		if(!empty($p['masque.milieu'])) {
300
		if(!empty($p['masque.milieu'])) {
297
			$req['where'][] = 'vdi.milieu LIKE '.$db->proteger('%' . $p['masque.milieu'].'%');
301
			$req['where'][] = 'vdi.milieu LIKE '.$db->proteger('%' . $p['masque.milieu'].'%');
298
		}
302
		}
Line 438... Line 442...
438
			$p['navigation.depart'], $p['navigation.limite'], __FILE__ . ':' . __LINE__));
442
			$p['navigation.depart'], $p['navigation.limite'], __FILE__ . ':' . __LINE__));
Line 439... Line 443...
439
 
443
 
Line 440... Line 444...
440
	}
444
	}
441
 
445
 
442
	static function chargerImages($db, $idImg) {
446
	static function chargerImages($db, $idImg) {
Line 443... Line 447...
443
		$obs_fields = Observation::sqlFieldsToAlias(self::$mappings['observations'], NULL);
447
		$obs_fields = DelTk::sqlFieldsToAlias(self::$mappings['observations'], NULL);
444
		$image_fields = Observation::sqlFieldsToAlias(self::$mappings['images'], NULL);
448
		$image_fields = DelTk::sqlFieldsToAlias(self::$mappings['images'], NULL);
445
	
449
	
446
		return $db->recupererTous(sprintf('SELECT '.
450
		return $db->recupererTous(sprintf('SELECT '.
Line 482... Line 486...
482
            // tri est aussi nécessaire car affecte les contraintes de JOIN
486
            // tri est aussi nécessaire car affecte les contraintes de JOIN
483
            'tri' => $p['tri'],
487
            'tri' => $p['tri'],
484
            'ordre' => $p['ordre']);
488
            'ordre' => $p['ordre']);
Line 485... Line 489...
485
 
489
 
486
			$or_masque = array_merge(
490
			$or_masque = array_merge(
487
				ListeObservations::requestFilterParams($or_params, NULL, $c /* pour masque.departement */),
491
				DelTk::requestFilterParams($or_params, NULL, $c /* pour masque.departement */),
Line 488... Line 492...
488
				self::requestFilterParams($or_params));
492
				self::requestFilterParams($or_params));
489
 
493
 
490
			/* Lorsqu'on utilise le masque général pour chercher des tags, ils sont
494
			/* Lorsqu'on utilise le masque général pour chercher des tags, ils sont
491
			   postulés comme séparés par des espaces, et doivent être tous matchés. */
495
			   postulés comme séparés par des espaces, et doivent être tous matchés. */
Line 492... Line 496...
492
			$or_masque['masque.tag_cel'] = self::buildTagsAST($p['masque'], 'AND', ' ');
496
			$or_masque['masque.tag_cel'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
493
			$or_masque['masque.tag_pictoflora'] = self::buildTagsAST($p['masque'], 'AND', ' ');
497
			$or_masque['masque.tag_pictoflora'] = DelTk::buildTagsAST($p['masque'], 'AND', ' ');
-
 
498
 
494
 
499
 
495
 
500
			// pas de select, groupby & co ici: uniquement 'join' et 'where'
Line 496... Line 501...
496
			// pas de select, groupby & co ici: uniquement 'join' et 'where'
501
			$or_req = array('join' => array(), 'where' => array());
497
			$or_req = array('join' => array(), 'where' => array());
502
			DelTk::sqlAddConstraint($or_masque, $db, $or_req);
Line 504... Line 509...
504
				$req['join'] = array_unique(array_merge($req['join'], $or_req['join']));
509
				$req['join'] = array_unique(array_merge($req['join'], $or_req['join']));
505
			}
510
			}
506
		}
511
		}
507
	}
512
	}
Line -... Line 513...
-
 
513
 
-
 
514
 
-
 
515
	// cf Observation::reformateObservationSimpleIndex() et ListeObservations::reformateObservation()
-
 
516
    // (trop de variétés de formatage, à unifier côté client pour unifier côté backend ...)
-
 
517
	static function reformateImagesDoubleIndex($obs, $url_pattern = '', $image_format = 'XL') {
-
 
518
		// XXX: cf Observation.php::consulter(), nous pourriouns ici
-
 
519
		// conserver les valeurs vides (pour les phptests notamment, ou non)
-
 
520
		// $obs = array_map('array_filter', $obs);
-
 
521
		$obs_merged = $obs_keyed_by_id_image = array();
-
 
522
		foreach($obs as $o) {
-
 
523
			// ceci nous complique la tâche pour le reste du processing...
-
 
524
			$id = $o['jsonindex'];
-
 
525
			// ainsi nous utilisons deux tableaux: le final, indexé par couple d'id(image-obs)
-
 
526
			// et celui indexé par simple id_image qui est fort utile pour mapVotesToImages()
-
 
527
			// mais tout deux partage leur référence à "protocole"
-
 
528
			$image = array(
-
 
529
				'id_image' => $o['id_image'],
-
 
530
				'binaire.href' => sprintf($url_pattern, $o['id_image'], $image_format),
-
 
531
				'mots_cles_texte' => @$o['i_mots_cles_texte'], // @, peut avoir été filtré par array_map() ci-dessus
-
 
532
			);
-
 
533
			unset($o['id_image'], $o['i_mots_cles_texte'], $o['jsonindex']);
-
 
534
			if(!isset($obs_merged[$id])) $obs_merged[$id] = $image;
-
 
535
			$obs_merged[$id]['observation'] = $o;
-
 
536
			$obs_merged[$id]['protocoles_votes'] = array();
-
 
537
			
-
 
538
			$obs_keyed_by_id_image[$image['id_image']]['protocoles_votes'] = &$obs_merged[$id]['protocoles_votes'];
-
 
539
		}
-
 
540
 
-
 
541
		return array($obs_merged,$obs_keyed_by_id_image);
-
 
542
	}
-
 
543
 
-
 
544
 
508
 
545
 
509
	// complete & override ListeObservations::requestFilterParams() (même usage)
546
	// complete & override DelTk::requestFilterParams() (même usage)
510
	static function requestFilterParams(Array $params, $parametres_autorises = NULL) {
547
	static function requestFilterParams(Array $params, $parametres_autorises = NULL) {
511
		if($parametres_autorises) { // filtrage de toute clef inconnue
548
		if($parametres_autorises) { // filtrage de toute clef inconnue
512
			$params = array_intersect_key($params, array_flip($parametres_autorises));
549
			$params = array_intersect_key($params, array_flip($parametres_autorises));
Line 513... Line 550...
513
		}
550
		}
514
 
551
 
515
		$p = array();
552
		$p = array();
Line 516... Line 553...
516
		$p['tri'] = ListeObservations::unsetIfInvalid($params, 'tri', self::$tri_possible);
553
		$p['tri'] = DelTk::unsetIfInvalid($params, 'tri', self::$tri_possible);
517
		$p['format'] = ListeObservations::unsetIfInvalid($params, 'format', self::$format_image_possible);
554
		$p['format'] = DelTk::unsetIfInvalid($params, 'format', self::$format_image_possible);
Line 518... Line 555...
518
 
555
 
Line 534... Line 571...
534
 
571
 
535
		return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
572
		return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
Line 536... Line -...
536
	}
-
 
537
 
-
 
538
 
-
 
539
	/* Construit un (vulgaire) abstract syntax tree:
-
 
540
	   "AND" => [ "tag1", "tag2" ]
-
 
541
	   Idéalement (avec un parser simple comme proposé par http://hoa-project.net/Literature/Hack/Compiler.html#Langage_PP)
-
 
542
	   nous aurions:
-
 
543
	   "AND" => [ "tag1", "tag2", "OR" => [ "tag3", "tag4" ] ]
-
 
544
 
-
 
545
	   Ici nous devons traiter les cas suivants:
-
 
546
	   tags séparés par des "ET/AND OU/OR", séparés par des espaces ou des virgules.
-
 
547
	   Mais la chaîne peut aussi avoir été issue du "masque général" (la barre de recherche générique).
-
 
548
	   ce qui implique des comportement par défaut différents afin de préserver la compatibilité.
-
 
549
 
-
 
550
	   Théorie:
-
 
551
	   1) tags passés par "champ tag":
-
 
552
	   - support du ET/OU, et explode par virgule.
-
 
553
	   - si pas d'opérande détectée: "OU"
-
 
554
 
-
 
555
	   2) tags passés par "recherche générale":
-
 
556
	   - support du ET/OU, et explode par whitespace.
-
 
557
	   - si pas d'opérande détectée: "ET"
-
 
558
 
-
 
559
	   La présence de $additional_sep s'explique car ET/OU sous-entendent une séparation par des espaces.
-
 
560
	   Mais ce n'est pas toujours pertinent car: 1) la compatibilité suggère de considérer parfois
-
 
561
	   la virgule comme séparateur et 2) les tags *peuvent* contenir des espaces. Par conséquent:
-
 
562
	   * a,b,c => "a" $default_op "b" $default_op "c"
-
 
563
	   * a,b AND c => "a" AND "b" AND "c"
-
 
564
	   * a OR b AND c,d => "a" AND "b" AND "c" AND "d"
-
 
565
	   C'est à dire par ordre décroissant de priorité:
-
 
566
	   1) opérande contenu dans la chaîne
-
 
567
	   2) opérande par défaut
-
 
568
	   3) les séparateurs présents sont substitués par l'opérande déterminée par 1) ou 2)
-
 
569
 
-
 
570
	   // TODO: support des parenthèses, imbrications & co: "(", ")"
-
 
571
	   // http://codehackit.blogspot.fr/2011/08/expression-parser-in-php.html
-
 
572
	   // http://blog.angeloff.name/post/2012/08/05/php-recursive-patterns/
-
 
573
 
-
 
574
	   @param $str: la chaîne à "parser"
-
 
575
	   @param $default_op: "AND" ou "OR"
-
 
576
	   @param $additional_sep: séparateur de mots:
-
 
577
	*/
-
 
578
	static function buildTagsAST($str = NULL, $default_op, $additional_sep = ',') {
-
 
579
		if(!$str) return;
-
 
580
		$words = preg_split('/ (OR|AND|ET|OU) /', $str, -1, PREG_SPLIT_NO_EMPTY);
-
 
581
 
-
 
582
		if(preg_match('/\b(ET|AND)\b/', $str)) $op = 'AND';
-
 
583
		elseif(preg_match('/\b(OU|OR)\b/', $str)) $op = 'OR';
-
 
584
		else $op = $default_op;
-
 
585
 
-
 
586
		if($additional_sep) {
-
 
587
			array_walk($words,
-
 
588
            create_function('&$v, $k, $sep', '$v = preg_split("/".$sep."/", $v, -1, PREG_SPLIT_NO_EMPTY);'),
-
 
589
            $additional_sep);
-
 
590
		}
-
 
591
		$words = self::array_flatten($words);
-
 
592
		$words = array_map('trim', $words);
-
 
Line 593... Line 573...
593
		return array($op => array_filter($words));
573
	}
594
	}
574
 
595
 
575
 
596
 
576
 
Line 614... Line 594...
614
	}
594
	}
Line 615... Line 595...
615
 
595
 
616
	static function revOrderBy($orderby) {
596
	static function revOrderBy($orderby) {
617
		return $orderby == 'asc' ? 'desc' : 'asc';
597
		return $orderby == 'asc' ? 'desc' : 'asc';
618
	}
-
 
619
 
-
 
620
	static function array_flatten($arr) {
-
 
621
		$arr = array_values($arr);
-
 
622
		while (list($k,$v)=each($arr)) {
-
 
623
			if (is_array($v)) {
-
 
624
				array_splice($arr,$k,1,$v);
-
 
625
				next($arr);
-
 
626
			}
-
 
627
		}
-
 
628
		return $arr;
-
 
629
	}
598
	}