Subversion Repositories eFlore/Applications.cel

Rev

Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1636 raphael 1
<?php
2447 jpm 2
// declare(encoding='UTF-8');
1636 raphael 3
/**
4
 * Service d'import de données d'observation du CEL au format XLS
1649 raphael 5
 *
6
 * Sont define()'d commme n° de colonne tous les abbrevs retournés par
1656 raphael 7
 * FormateurGroupeColonne::nomEnsembleVersListeColonnes() préfixés par C_  cf: detectionEntete()
1649 raphael 8
 *
9
 * Exemple d'un test:
10
 * $ GET "/jrest/ExportXLS/22506?format=csv&range=*&limite=13" \
11
 *   | curl -F "upload=@-" -F utilisateur=22506 "/jrest/ImportXLS"
12
 * # 13 observations importées
13
 * + cf MySQL general_log = 1
2447 jpm 14
 *
2458 jpm 15
 * @internal   Mininum PHP version : 5.2
16
 * @category   CEL
2447 jpm 17
 * @package    Services
2458 jpm 18
 * @subpackage Observations
2447 jpm 19
 * @version    0.1
20
 * @author     Mathias CHOUET <mathias@tela-botanica.org>
21
 * @author     Raphaël DROZ <raphael@tela-botanica.org>
22
 * @author     Jean-Pascal MILCENT <jpm@tela-botanica.org>
23
 * @author     Aurelien PERONNET <aurelien@tela-botanica.org>
24
 * @license    GPL v3 <http://www.gnu.org/licenses/gpl.txt>
25
 * @license    CECILL v2 <http://www.cecill.info/licences/Licence_CeCILL_V2-en.txt>
26
 * @copyright  1999-2014 Tela Botanica <accueil@tela-botanica.org>
27
 */
1636 raphael 28
set_include_path(get_include_path() . PATH_SEPARATOR . dirname(dirname(realpath(__FILE__))) . '/lib');
29
// TERM
30
error_reporting(-1);
31
ini_set('html_errors', 0);
32
ini_set('xdebug.cli_color', 2);
2459 jpm 33
require_once 'lib/PHPExcel/Classes/PHPExcel.php';
34
require_once 'FormateurGroupeColonne.php';
1636 raphael 35
 
2447 jpm 36
date_default_timezone_set('Europe/Paris');
1640 raphael 37
 
38
// nombre d'INSERT à cumuler par requête SQL
39
// (= nombre de lignes XLS à bufferiser)
1648 raphael 40
//define('NB_LIRE_LIGNE_SIMUL', 30);
41
define('NB_LIRE_LIGNE_SIMUL', 5);
1640 raphael 42
 
1933 raphael 43
// en cas d'import d'un fichier CSV, utilise fgetcsv() plutôt
44
// que PHPExcel ce qui se traduit par un gain de performances très substanciel
45
define('QUICK_CSV_IMPORT', TRUE);
46
 
1640 raphael 47
// Numbers of days between January 1, 1900 and 1970 (including 19 leap years)
48
// see traiterDateObs()
1675 raphael 49
// define("MIN_DATES_DIFF", 25569);
2447 jpm 50
class MyReadFilter implements PHPExcel_Reader_IReadFilter {
51
	// exclusion de colonnes
52
	public $exclues = array();
1640 raphael 53
 
2447 jpm 54
	// lecture par morceaux
55
	public $ligne_debut = 0;
56
	public $ligne_fin = 0;
1640 raphael 57
 
2447 jpm 58
	public static $gestion_mots_cles = null;
1640 raphael 59
 
2447 jpm 60
	public function __construct() {}
1640 raphael 61
 
2447 jpm 62
	public function def_interval($debut, $nb) {
63
		$this->ligne_debut = $debut;
64
		$this->ligne_fin = $debut + $nb;
65
	}
1636 raphael 66
 
2447 jpm 67
	public function readCell($colonne, $ligne, $worksheetName = '') {
68
		if(@$this->exclues[$colonne]) return false;
69
		// si des n° de morceaux ont été initialisés, on filtre...
70
		if($this->ligne_debut && ($ligne < $this->ligne_debut || $ligne >= $this->ligne_fin)) return false;
71
		return true;
72
	}
73
}
74
 
1675 raphael 75
function __anonyme_1($v) {	return !$v['importable']; }
76
function __anonyme_2(&$v) {	$v = $v['nom']; }
77
function __anonyme_3($cell) { return !is_null($cell); };
78
function __anonyme_5($item) { return is_null($item) ? '?' : $item; }
79
function __anonyme_6() { return NULL; }
80
 
1636 raphael 81
class ImportXLS extends Cel  {
2447 jpm 82
	static function __anonyme_4(&$item, $key) { $item = self::quoteNonNull(trim($item)); }
1636 raphael 83
 
2447 jpm 84
	static $ordre_BDD = Array(
85
		"ce_utilisateur",
86
		"prenom_utilisateur",
87
		"nom_utilisateur",
88
		"courriel_utilisateur",
89
		"ordre",
90
		"nom_sel",
91
		"nom_sel_nn",
92
		"nom_ret",
93
		"nom_ret_nn",
94
		"nt",
95
		"famille",
96
		"nom_referentiel",
97
		"zone_geo",
98
		"ce_zone_geo",
99
		"date_observation",
100
		"lieudit",
101
		"station",
102
		"milieu",
103
		"mots_cles_texte",
104
		"commentaire",
105
		"transmission",
106
		"date_creation",
107
		"date_modification",
108
		"date_transmission",
109
		"latitude",
110
		"longitude",
111
		"altitude",
112
		"abondance",
113
		"certitude",
114
		"phenologie",
115
		"code_insee_calcule"
116
	);
1636 raphael 117
 
2447 jpm 118
	// cf: initialiser_pdo_ordered_statements()
119
	// eg: "INSERT INTO cel_obs (ce_utilisateur, ..., phenologie, code_insee_calcule) VALUES"
120
	// colonnes statiques d'abord, les autres ensuite, dans l'ordre de $ordre_BDD
121
	static $insert_prefix_ordre;
122
	// eg: "(<id>, <prenom>, <nom>, <email>, now(), now(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
123
	// dont le nombre de placeholder dépend du nombre de colonnes non-statiques
124
	// colonnes statiques d'abord, les autres ensuite, dans l'ordre de $ordre_BDD
125
	static $insert_ligne_pattern_ordre;
1648 raphael 126
 
2447 jpm 127
	// seconde (meilleure) possibilité
128
	// cf: initialiser_pdo_statements()
129
	// eg: "INSERT INTO cel_obs (ce_utilisateur, ..., date_creation, ...phenologie, code_insee_calcule) VALUES"
130
	static $insert_prefix;
131
	// eg: "(<id>, <prenom>, <nom>, <email>, ?, ?, ?, now(), now(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
132
	// dont le nombre de placeholder dépend du nombre de colonnes non-statiques
133
	static $insert_ligne_pattern;
1648 raphael 134
 
2447 jpm 135
	/*
136
	 Ces colonnes:
137
	 - sont propres à l'ensemble des enregistrements uploadés
138
	 - sont indépendantes du numéro de lignes
139
	 - n'ont pas de valeur par défaut dans la structure de la table
140
	 - nécessitent une initialisation dans le cadre de l'upload
141
	 initialiser_colonnes_statiques() y merge les données d'identification utilisateur
142
	*/
143
	public $colonnes_statiques = array(
144
		"ce_utilisateur" => NULL,
145
		"prenom_utilisateur" => NULL,
146
		"nom_utilisateur" => NULL,
147
		"courriel_utilisateur" => NULL,
1649 raphael 148
 
2447 jpm 149
		// fixes (fonction SQL)
150
		// XXX future: mais pourraient varier dans le futur si la mise-à-jour
151
		// d'observation est implémentée
152
		"date_creation" => "now()",
153
		"date_modification" => "now()",
154
	);
1640 raphael 155
 
2447 jpm 156
	public static $prefixe_colonnes_etendues = 'ext:';
157
	public static $indexes_colonnes_etendues = Array();
158
	public static $gestion_champs_etendus = null;
1640 raphael 159
 
2447 jpm 160
	public $id_utilisateur = NULL;
1649 raphael 161
 
2447 jpm 162
	// erreurs d'import
163
	public $bilan = Array();
1642 raphael 164
 
2447 jpm 165
	// cache (pour traiterLocalisation() pour l'instant)
166
	static $cache = Array('geo' => array());
1649 raphael 167
 
2447 jpm 168
	function ImportXLS($config) {
169
		parent::__construct($config);
1636 raphael 170
	}
171
 
2447 jpm 172
	function createElement($pairs) {
173
		if (!isset($pairs['utilisateur']) || trim($pairs['utilisateur']) == '') {
174
			exit('0');
175
		}
1649 raphael 176
 
2447 jpm 177
		$id_utilisateur = intval($pairs['utilisateur']);
178
		$this->id_utilisateur = $id_utilisateur; // pour traiterImage();
1640 raphael 179
 
2447 jpm 180
		if (!isset($_SESSION)) {
181
			session_start();
182
		}
183
		$this->controleUtilisateur($id_utilisateur);
1636 raphael 184
 
2447 jpm 185
		$this->utilisateur = $this->getInfosComplementairesUtilisateur($id_utilisateur);
186
		$this->initialiser_colonnes_statiques($id_utilisateur);
187
		list(self::$insert_prefix, self::$insert_ligne_pattern) = $this->initialiser_pdo_statements($this->colonnes_statiques);
2034 aurelien 188
 
2447 jpm 189
		$infos_fichier = array_pop($_FILES);
1649 raphael 190
 
2447 jpm 191
		// renomme le fichier pour lui ajouter son extension initiale, ce qui
192
		// permet (une sorte) d'autodétection du format.
193
		$fichier = $infos_fichier['tmp_name'];
194
		$extension = pathinfo($infos_fichier['name'], PATHINFO_EXTENSION);
195
		if ( (strlen($extension) == 3 || strlen($extension) == 4) && (@rename($fichier, "$fichier.$extension"))) {
196
			$fichier = "$fichier.$extension";
197
		}
1636 raphael 198
 
2447 jpm 199
		$objReader = PHPExcel_IOFactory::createReaderForFile($fichier);
200
		// TODO: check if compatible with toArray(<1>,<2>,TRUE,<4>)
201
		$objReader->setReadDataOnly(true);
1636 raphael 202
 
2447 jpm 203
		// TODO: is_a obsolete entre 5.0 et 5.3, retirer le @ à terme
204
		$IS_CSV = @is_a($objReader, 'PHPExcel_Reader_CSV') && QUICK_CSV_IMPORT;
205
		// en cas d'usage de fgetcsv, testons que nous pouvons compter les lignes
206
		if ($IS_CSV) {
207
			$nb_lignes = intval(exec("wc -l $fichier"));
208
		}
209
		// et, le cas échéant, fallback sur PHPExcel à nouveau. La raison de ce test ici est
210
		// l'instabilité du serveur (safe_mode, safe_mode_exec_dir, symlink vers binaires pour exec(), ... multiples points-of-failure)
211
		if ($IS_CSV && !$nb_lignes) {
212
			$IS_CSV = FALSE;
213
		}
1636 raphael 214
 
2447 jpm 215
		if ($IS_CSV) {
216
			$objReader->setDelimiter(',')
217
				->setEnclosure('"')
218
				->setLineEnding("\n")
219
				->setSheetIndex(0);
220
		}
1636 raphael 221
 
2447 jpm 222
		// on ne conserve que l'en-tête
223
		$filtre = new MyReadFilter();
224
		$filtre->def_interval(1, 2);
225
		$objReader->setReadFilter($filtre);
1642 raphael 226
 
2447 jpm 227
		$objPHPExcel = $objReader->load($fichier);
228
		$obj_infos = $objReader->listWorksheetInfo($fichier);
1640 raphael 229
 
2447 jpm 230
		if ($IS_CSV) {
231
			// $nb_lignes est déjà défini ci-dessus
232
			$csvFileHandler = fopen($fichier, 'r');
233
			// nous utilisons la valeur de retour dans un but informatif de l'utilisateur à la
234
			// fin de l'import, *mais aussi* dans un array_diff_key() ci-dessous car bien que dans le
235
			// fond le "parser" fgetcsv() n'ait pas d'intérêt à connaître les colonnes à ignorer,
236
			// il se trouve que celles-ci peuvent interférer sur des fonctions comme traiterEspece()
237
			// cf test "ref-nom-num.test.php" pour lequel l'élément C_NOM_SEL vaudrait 3 et $ligne serait array(3 => -42)
238
			$filtre->exclues = self::detectionEntete(fgetcsv($csvFileHandler), TRUE);
239
		} else {
240
			// XXX: indépendant du readFilter ?
241
			$nb_lignes = $obj_infos[0]['totalRows'];
242
			$donnees = $objPHPExcel->getActiveSheet()->toArray(NULL, FALSE, TRUE, TRUE);
243
			$filtre->exclues = self::detectionEntete($donnees[1]);
244
		}
1933 raphael 245
 
2447 jpm 246
		$obs_ajouts = 0;
247
		$obs_maj = 0;
248
		$nb_images_ajoutees = 0;
249
		$nb_mots_cle_ajoutes = 0;
250
		$nb_champs_etendus_inseres = 0;
1642 raphael 251
 
2447 jpm 252
		$dernier_ordre = Cel::db()->requeter("SELECT MAX(ordre) AS ordre FROM cel_obs WHERE ce_utilisateur = $id_utilisateur");
253
		$dernier_ordre = intval($dernier_ordre[0]['ordre']) + 1;
254
		if (! $dernier_ordre) {
255
			$dernier_ordre = 0;
256
		}
1640 raphael 257
 
2447 jpm 258
		// on catch to les trigger_error(E_USER_NOTICE);
259
		set_error_handler(array($this, 'erreurs_stock'), E_USER_NOTICE);
260
		$this->taxon_info_webservice = new RechercheInfosTaxonBeta($this->config, NULL);
1636 raphael 261
 
2447 jpm 262
		// lecture par morceaux (chunks), NB_LIRE_LIGNE_SIMUL lignes à fois
263
		// pour aboutir des requêtes SQL d'insert groupés.
264
		for ($ligne = 2; $ligne < $nb_lignes + NB_LIRE_LIGNE_SIMUL; $ligne += NB_LIRE_LIGNE_SIMUL) {
265
			if (!$IS_CSV) {
266
				$filtre->def_interval($ligne, NB_LIRE_LIGNE_SIMUL);
267
				$objReader->setReadFilter($filtre);
1636 raphael 268
 
2447 jpm 269
				$objPHPExcel = $objReader->load($fichier)->getActiveSheet();
1677 raphael 270
 
2447 jpm 271
				// set col typing
272
				if (C_CE_ZONE_GEO != 'C_CE_ZONE_GEO') {
273
					$objPHPExcel->getStyle(C_CE_ZONE_GEO . '2:' . C_CE_ZONE_GEO . $objPHPExcel->getHighestRow())->getNumberFormat()->setFormatCode('00000');
274
				}
275
				// TODO: set to string type
276
				if (C_ZONE_GEO != 'C_ZONE_GEO') {
277
					$objPHPExcel->getStyle(C_ZONE_GEO . '2:' . C_ZONE_GEO . $objPHPExcel->getHighestRow())->getNumberFormat()->setFormatCode('00000');
278
				}
279
				$donnees = $objPHPExcel->toArray(NULL, FALSE, TRUE, TRUE);
280
			} else {
281
				$i = NB_LIRE_LIGNE_SIMUL;
282
				$donnees = array();
283
				while ($i--) {
284
					$tab = fgetcsv($csvFileHandler);
285
					if (!$tab) {
286
						continue;
287
					}
288
					$donnees[] = array_diff_key($tab, $filtre->exclues);
289
				}
290
			}
1640 raphael 291
 
2447 jpm 292
			list($enregistrements, $images, $mots_cle, $champs_etendus) = self::chargerLignes($this, $donnees, $this->colonnes_statiques, $dernier_ordre);
293
			if (! $enregistrements) {
294
				break;
295
			}
1642 raphael 296
 
2447 jpm 297
			self::trierColonnes($enregistrements);
298
			// normalement: NB_LIRE_LIGNE_SIMUL, sauf si une enregistrement ne semble pas valide
299
			// ou bien lors du dernier chunk
1933 raphael 300
 
2447 jpm 301
			$nb_rec = count($enregistrements);
302
			$sql_pattern = self::$insert_prefix.
303
				str_repeat(self::$insert_ligne_pattern.', ', $nb_rec - 1).
304
				self::$insert_ligne_pattern;
1640 raphael 305
 
2447 jpm 306
			Cel::db()->beginTransaction();
307
			$stmt = Cel::db()->prepare($sql_pattern);
308
			$donnees = array();
309
			foreach ($enregistrements as $e) {
310
				$donnees = array_merge($donnees, array_values($e));
311
			}
1640 raphael 312
 
2447 jpm 313
			$stmt->execute($donnees);
1818 raphael 314
 
2447 jpm 315
			$dernier_autoinc = Cel::db()->lastInsertId();
316
			Cel::db()->commit();
1818 raphael 317
 
2447 jpm 318
			if (! $dernier_autoinc) {
319
				trigger_error("l'insertion semble avoir échoué", E_USER_NOTICE);
320
			}
1818 raphael 321
 
2447 jpm 322
			$obs_ajouts += count($enregistrements);
1933 raphael 323
 
2447 jpm 324
			$ordre_ids = self::chargerCorrespondancesIdOrdre($this, $enregistrements);
1640 raphael 325
 
2447 jpm 326
			$nb_images_ajoutees += self::stockerImages($enregistrements, $images, $ordre_ids);
327
			$nb_mots_cle_ajoutes += self::stockerMotsCle($enregistrements, $mots_cle, $dernier_autoinc);
328
			$nb_champs_etendus_inseres += self::stockerChampsEtendus($champs_etendus, $ordre_ids, $this->config);
329
		}
1642 raphael 330
 
2447 jpm 331
		restore_error_handler();
1648 raphael 332
 
2447 jpm 333
		if ($this->bilan) {
334
			echo implode("\n", $this->bilan) . "\n";
335
		}
336
		printf('%1$d observation%2$s ajoutée%2$s' . "\n" .
337
			'%3$d image%4$s attachée%4$s' . "\n" .
338
			(count($filtre->exclues) > 0 ? 'colonne%7$s non-traitée%7$s: %8$s' . "\n" : ''),
1648 raphael 339
 
2447 jpm 340
			$obs_ajouts,
341
			$obs_ajouts > 1 ? 's' : '',
342
			$nb_images_ajoutees,
343
			$nb_images_ajoutees > 1 ? 's' : '',
344
			$nb_mots_cle_ajoutes,
345
			$nb_mots_cle_ajoutes > 1 ? 's' : '',
346
			count($filtre->exclues) > 1 ? 's' : '',
347
			implode(', ', $filtre->exclues));
348
		die();
1929 raphael 349
	}
1642 raphael 350
 
2447 jpm 351
	/* detectionEntete() sert deux rôles:
352
	 1) détecter le type de colonne attendu à partir des textes de la ligne d'en-tête afin de define()
353
	 2) permet d'identifier les colonnes non-supportées/inutiles afin d'alléger le processus de parsing de PHPExcel
354
	 grace au ReadFilter (C'est le rôle de la valeur de retour)
1642 raphael 355
 
2447 jpm 356
	 La raison de la présence du paramètre $numeric_keys est que pour réussir à identifier les colonnes à exclure nous
357
	 devons traiter un tableau représentant la ligne d'en-tête aussi bien:
358
	  - sous forme associative pour PHPExcel (les clefs sont les lettres de l'alphabet)
359
	  - sous forme de clefs numériques (fgetcsv())
360
	  Le détecter après coup est difficile et pourtant cette distinction est importante car le comportement
361
	  d'array_merge() (réordonnancement des clefs numérique) n'est pas souhaitable dans le second cas. */
362
	static function detectionEntete($entete, $numeric_keys = FALSE) {
363
		$colonnes_reconnues = Array();
364
		$cols = FormateurGroupeColonne::nomEnsembleVersListeColonnes('standard,avance');
1792 raphael 365
 
2447 jpm 366
		foreach ($entete as $k => $v) {
367
			// traite les colonnes en faisant fi de la casse et des accents
368
			$entete_simple = iconv('UTF-8', 'ASCII//TRANSLIT', strtolower(trim($v)));
369
			foreach ($cols as $col) {
370
				$entete_officiel_simple = iconv('UTF-8', 'ASCII//TRANSLIT', strtolower(trim($col['nom'])));
371
				$entete_officiel_abbrev = $col['abbrev'];
372
				if ($entete_simple == $entete_officiel_simple || $entete_simple == $entete_officiel_abbrev) {
373
					// debug echo "define C_" . strtoupper($entete_officiel_abbrev) . ", $k ($v)\n";
374
					define("C_" . strtoupper($entete_officiel_abbrev), $k);
375
					$colonnes_reconnues[$k] = 1;
376
					break;
377
				}
1636 raphael 378
 
2447 jpm 379
				if (strpos($v, self::$prefixe_colonnes_etendues) === 0) {
380
					$colonnes_reconnues[$k] = 1;
381
					self::$indexes_colonnes_etendues[$k] = $v;
382
					break;
383
				}
2381 aurelien 384
			}
2447 jpm 385
		}
2381 aurelien 386
 
2447 jpm 387
		// défini tous les index que nous utilisons à une valeur d'index de colonne Excel qui n'existe pas dans
388
		// le tableau renvoyé par PHPExcel
389
		// Attention cependant d'utiliser des indexes différenciés car traiterLonLat() et traiterEspece()
390
		// les utilisent
391
		foreach ($cols as $col) {
392
			if (!defined('C_'.strtoupper($col['abbrev']))) {
393
				define('C_'.strtoupper($col['abbrev']), 'C_'.strtoupper($col['abbrev']));
2381 aurelien 394
			}
2447 jpm 395
		}
1636 raphael 396
 
2447 jpm 397
		// prépare le filtre de PHPExcel qui évitera le traitement de toutes les colonnes superflues
398
		$colonnesID_non_reconnues = array_diff_key($entete, $colonnes_reconnues);
1640 raphael 399
 
2447 jpm 400
		// des colonnes de FormateurGroupeColonne::nomEnsembleVersListeColonnes()
401
		// ne retient que celles marquées "importables"
402
		$colonnes_automatiques = array_filter($cols, '__anonyme_1');
1636 raphael 403
 
2447 jpm 404
		// ne conserve que le nom long pour matcher avec la ligne XLS d'entête
405
		array_walk($colonnes_automatiques, '__anonyme_2');
1640 raphael 406
 
2447 jpm 407
		$colonnesID_a_exclure = array_intersect($entete, $colonnes_automatiques);
1640 raphael 408
 
2447 jpm 409
		if ($numeric_keys) {
410
			return $colonnesID_non_reconnues + $colonnesID_a_exclure;
411
		}
412
		return array_merge($colonnesID_non_reconnues, $colonnesID_a_exclure);
413
	}
1636 raphael 414
 
2447 jpm 415
	static function chargerCorrespondancesIdOrdre($cel, $lignes) {
416
		$ordresObs = array();
417
		foreach ($lignes as &$ligne) {
418
			$ordresObs[] = $ligne['ordre'];
419
		}
420
		$ordresObsConcat = implode(',', $ordresObs);
421
		$idUtilisateurP = Cel::db()->proteger($cel->id_utilisateur);
422
		$requete = 'SELECT id_observation, ordre '.
423
			'FROM cel_obs '.
424
			"WHERE ordre IN ($ordresObsConcat) ".
425
			"AND ce_utilisateur = $idUtilisateurP ".
426
			' -- '.__FILE__.':'.__LINE__;
427
		$resultats = Cel::db()->requeter($requete);
428
		$ordresIds = array();
429
		foreach ($resultats as &$infos) {
430
			$ordresIds[$infos['ordre']] = $infos['id_observation'];
431
		}
432
		return $ordresIds;
1933 raphael 433
	}
1636 raphael 434
 
2447 jpm 435
	/*
436
	 * charge un groupe de lignes
437
	 */
438
	static function chargerLignes($cel, $lignes, $colonnes_statiques, &$dernier_ordre) {
439
		$enregistrement = NULL;
440
		$enregistrements = array();
441
		$toutes_images = array();
442
		$tous_mots_cle = array();
443
		$tous_champs_etendus = array();
1640 raphael 444
 
2447 jpm 445
		foreach ($lignes as $ligne) {
446
			// dans le cas de fgetcsv, on peut avoir des false additionnel (cf do/while l. 279)
447
			if ($ligne === false) {
448
				continue;
449
			}
1933 raphael 450
 
2447 jpm 451
			// on a besoin des NULL pour éviter des notice d'index indéfini
452
			if (! array_filter($ligne, '__anonyme_3')) {
453
				continue;
454
			}
1640 raphael 455
 
2447 jpm 456
			if ($enregistrement = self::chargerLigne($ligne, $dernier_ordre, $cel)) {
457
				// $enregistrements[] = array_merge($colonnes_statiques, $enregistrement);
458
				$enregistrements[] = $enregistrement;
459
				$pos = count($enregistrements) - 1;
460
				$last = &$enregistrements[$pos];
1640 raphael 461
 
2447 jpm 462
				if (isset($enregistrement['_images'])) {
463
					// ne dépend pas de cel_obs, et seront insérées *après* les enregistrements
464
					// mais nous ne voulons pas nous priver de faire des INSERT multiples pour autant
465
					$toutes_images[] = array(
466
						'images' => $last['_images'],
467
						'obs_pos' => $pos);
468
					// ce champ n'a pas à faire partie de l'insertion dans cel_obs,
469
					// mais est utile pour la liaison avec les images
470
					unset($last['_images']);
471
				}
1640 raphael 472
 
2447 jpm 473
				if (isset($enregistrement['_mots_cle'])) {
474
					// ne dépend pas de cel_obs, et seront insérés *après* les enregistrements
475
					// mais nous ne voulons pas nous priver de faire des INSERT multiples pour autant
476
					$tous_mots_cle[] = array(
477
						'mots_cle' => $last['_mots_cle'],
478
						'obs_pos' => $pos);
479
					unset($last['_mots_cle']);
480
				}
481
 
482
				if (isset($enregistrement['_champs_etendus'])) {
483
					$tous_champs_etendus[] = array(
484
						'champs_etendus' => $last['_champs_etendus'],
485
						'ordre' => $dernier_ordre);
486
					unset($last['_champs_etendus']);
487
				}
488
				$dernier_ordre++;
489
			}
1636 raphael 490
		}
2447 jpm 491
		return array($enregistrements, $toutes_images, $tous_mots_cle, $tous_champs_etendus);
492
	}
1640 raphael 493
 
2447 jpm 494
	static function trierColonnes(&$enregistrements) {
495
		foreach ($enregistrements as &$enregistrement) {
496
			$enregistrement = self::sortArrayByArray($enregistrement, self::$ordre_BDD);
2381 aurelien 497
		}
1642 raphael 498
	}
1640 raphael 499
 
2447 jpm 500
	static function stockerMotsCle($enregistrements, $tous_mots_cle, $lastid) {
501
		$c = 0;
502
		// debug: var_dump($tous_mots_cle);die;
503
		foreach ($tous_mots_cle as $v) {
504
			$c += count($v['mots_cle']['to_insert']);
505
		}
506
		return $c;
1677 raphael 507
	}
508
 
2447 jpm 509
	static function stockerImages($enregistrements, $toutes_images, $ordre_ids) {
510
		$idsImagesObs = array();
511
		foreach ($toutes_images as $images_pour_obs) {
512
			$obs = $enregistrements[$images_pour_obs['obs_pos']];
513
			$id_obs = $ordre_ids[$obs['ordre']]; // id réel de l'observation correspondant à l'ordre
514
			foreach ($images_pour_obs['images'] as $image) {
515
				$idsImagesObs[] = sprintf('(%d,%d, NOW())', $image['id_image'], $id_obs);
516
			}
517
		}
1642 raphael 518
 
2447 jpm 519
		if ($idsImagesObs) {
520
			$clauseValues = implode(', ', $idsImagesObs);
521
			// Utilisation de INSET pour faire des UPDATE multiples en une seule requête
522
			$requete = 'INSERT INTO cel_images (id_image, ce_observation, date_liaison) '.
523
				"VALUES $clauseValues ".
524
				'ON DUPLICATE KEY UPDATE ce_observation = VALUES(ce_observation), date_liaison = NOW() '.
525
				' -- '.__FILE__.':'.__LINE__;
526
			Cel::db()->requeter($requete);
527
		}
528
		return count($idsImagesObs);
1636 raphael 529
	}
530
 
2447 jpm 531
	/*
532
	 Aucune des valeurs présentes dans $enregistrement n'est quotée
533
	 cad aucune des valeurs retournée par traiter{Espece|Localisation}()
534
	 car ce tableau est passé à un PDO::preparedStatement() qui applique
535
	  proprement les règle d'échappement.
536
	*/
537
	static function chargerLigne($ligne, $dernier_ordre, $cel) {
538
		// évite des notices d'index lors des trigger_error()
539
		$ref_ligne = !empty($ligne[C_NOM_SEL]) ? trim($ligne[C_NOM_SEL]) : '';
1770 raphael 540
 
2447 jpm 541
		// en premier car le résultat est utile pour
542
		// * traiter espèce (traiterEspece())
543
		// * traiter longitude et latitude (traiterLonLat())
544
		$referentiel = self::identReferentiel(trim(strtolower(@$ligne[C_NOM_REFERENTIEL])), $ligne, $ref_ligne);
1636 raphael 545
 
2447 jpm 546
		// $espece est rempli de plusieurs informations
547
		$espece = array(
548
			C_NOM_SEL => NULL,
549
			C_NOM_SEL_NN => NULL,
550
			C_NOM_RET => NULL,
551
			C_NOM_RET_NN => NULL,
552
			C_NT => NULL,
553
			C_FAMILLE => NULL);
554
		self::traiterEspece($ligne, $espece, $referentiel, $cel->taxon_info_webservice);
1636 raphael 555
 
2447 jpm 556
		if (!$espece[C_NOM_SEL]) {
557
			$referentiel = Cel::$fallback_referentiel;
558
		}
559
		if ($espece[C_NOM_SEL] && !$espece[C_NOM_SEL_NN]) {
560
			$referentiel = Cel::$fallback_referentiel;
561
		}
1852 raphael 562
 
2447 jpm 563
		// $localisation est rempli à partir de plusieurs champs: C_ZONE_GEO et C_CE_ZONE_GEO
564
		$localisation = Array(C_ZONE_GEO => NULL, C_CE_ZONE_GEO => NULL);
565
		self::traiterLocalisation($ligne, $localisation);
1636 raphael 566
 
2447 jpm 567
		// $transmission est utilisé pour date_transmission
568
		// XXX: @ contre "Undefined index"
569
		@$transmission = in_array(strtolower(trim($ligne[C_TRANSMISSION])), array(1, 'oui')) ? 1 : 0;
1649 raphael 570
 
571
 
2447 jpm 572
		// Dans ce tableau, seules devraient apparaître les données variable pour chaque ligne.
573
		// Dans ce tableau, l'ordre des clefs n'importe pas (cf: self::sortArrayByArray())
574
		$enregistrement = Array(
575
			"ordre" => $dernier_ordre,
1640 raphael 576
 
2447 jpm 577
			"nom_sel" => $espece[C_NOM_SEL],
578
			"nom_sel_nn" => $espece[C_NOM_SEL_NN],
579
			"nom_ret" => $espece[C_NOM_RET],
580
			"nom_ret_nn" => $espece[C_NOM_RET_NN],
581
			"nt" => $espece[C_NT],
582
			"famille" => $espece[C_FAMILLE],
1640 raphael 583
 
2447 jpm 584
			"nom_referentiel" => $referentiel,
1640 raphael 585
 
2447 jpm 586
			"zone_geo" => $localisation[C_ZONE_GEO],
587
			"ce_zone_geo" => $localisation[C_CE_ZONE_GEO],
1640 raphael 588
 
2447 jpm 589
			// $ligne: uniquement pour les infos en cas de gestion d'erreurs (date incompréhensible)
590
			"date_observation" => isset($ligne[C_DATE_OBSERVATION]) ? self::traiterDateObs($ligne[C_DATE_OBSERVATION], $ref_ligne) : NULL,
1640 raphael 591
 
2447 jpm 592
			"lieudit" => isset($ligne[C_LIEUDIT]) ? trim($ligne[C_LIEUDIT]) : NULL,
593
			"station" => isset($ligne[C_STATION]) ? trim($ligne[C_STATION]) : NULL,
594
			"milieu" => isset($ligne[C_MILIEU]) ? trim($ligne[C_MILIEU]) : NULL,
1642 raphael 595
 
2447 jpm 596
			"mots_cles_texte" => NULL, // TODO: foreign-key
597
			// XXX: @ contre "Undefined index"
598
			"commentaire" => isset($ligne[C_COMMENTAIRE]) ? trim($ligne[C_COMMENTAIRE]) : NULL,
1642 raphael 599
 
2447 jpm 600
			"transmission" => $transmission,
601
			"date_transmission" => $transmission ? date("Y-m-d H:i:s") : NULL, // pas de fonction SQL dans un PDO statement, <=> now()
1642 raphael 602
 
2447 jpm 603
			// $ligne: uniquement pour les infos en cas de gestion d'erreurs (lon/lat incompréhensible)
604
			"latitude" => isset($ligne[C_LATITUDE]) ? self::traiterLonLat(NULL, $ligne[C_LATITUDE], $referentiel, $ref_ligne) : NULL,
605
			"longitude" => isset($ligne[C_LONGITUDE]) ? self::traiterLonLat($ligne[C_LONGITUDE], NULL, $referentiel, $ref_ligne) : NULL,
606
			"altitude" => isset($ligne[C_ALTITUDE]) ? intval($ligne[C_ALTITUDE]) : NULL, // TODO: guess alt from lon/lat
1648 raphael 607
 
2447 jpm 608
			// @ car potentiellement optionnelles ou toutes vides => pas d'index dans PHPExcel (tableau optimisé)
609
			"abondance" => @$ligne[C_ABONDANCE],
610
			"certitude" => @$ligne[C_CERTITUDE],
611
			"phenologie" => @$ligne[C_PHENOLOGIE],
1648 raphael 612
 
2447 jpm 613
			"code_insee_calcule" => substr($localisation[C_CE_ZONE_GEO], -5) // varchar(5)
614
		);
1642 raphael 615
 
2447 jpm 616
		// passage de $enregistrement par référence, ainsi ['_images'] n'est défini
617
		// que si des résultats sont trouvés
618
		// "@" car PHPExcel supprime les colonnes null sur toute la feuille (ou tout le chunk)
619
		if (@$ligne[C_IMAGES]) {
620
			self::traiterImage($ligne[C_IMAGES], $cel->id_utilisateur, $enregistrement);
621
		}
1642 raphael 622
 
2447 jpm 623
		if (@$ligne[C_MOTS_CLES_TEXTE]) {
624
			self::traiterMotsCle($ligne[C_MOTS_CLES_TEXTE], $cel->id_utilisateur, $enregistrement);
625
		}
1677 raphael 626
 
2447 jpm 627
		$champs_etendus = self::traiterChampsEtendus($ligne, self::$indexes_colonnes_etendues);
628
		if (!empty($champs_etendus)) {
629
			$enregistrement['_champs_etendus'] = $champs_etendus;
630
		}
1636 raphael 631
 
2447 jpm 632
		return $enregistrement;
2381 aurelien 633
	}
1675 raphael 634
 
2447 jpm 635
	static function traiterChampsEtendus(&$ligne, &$indexes_colonnes_etendues) {
636
		$champs_etendus_indexes = array();
637
		foreach($indexes_colonnes_etendues as $index_num => $label) {
638
			if (isset($ligne[$index_num])) {
639
				$champs_etendus_indexes[str_replace(self::$prefixe_colonnes_etendues, '', $label)] = $ligne[$index_num];
640
			}
641
		}
642
		return $champs_etendus_indexes;
643
	}
1640 raphael 644
 
2447 jpm 645
	static function traiterImage($str, $id_utilisateur, &$enregistrement) {
646
		$liste_images = array_filter(explode('/', $str));
647
		array_walk($liste_images, array(__CLASS__, '__anonyme_4'));
1640 raphael 648
 
2447 jpm 649
		$nomsOrignalConcat = implode(',', $liste_images);
650
		$requete = 'SELECT id_image, nom_original '.
651
			'FROM cel_images '.
652
			"WHERE ce_utilisateur = $id_utilisateur AND nom_original IN ($nomsOrignalConcat) ".
653
			' -- '.__FILE__.':'.__LINE__;
654
		$resultat = Cel::db()->requeter($requete);
1642 raphael 655
 
2447 jpm 656
		if ($resultat) {
657
			$enregistrement['_images'] = $resultat;
658
		}
659
	}
1678 raphael 660
 
2447 jpm 661
	static function traiterMotsCle($str, $id_utilisateur, &$enregistrement) {
662
		$liste_mots_cle = $liste_mots_cle_recherche = array_map('trim', array_unique(array_filter(explode(',', $str))));
663
		array_walk($liste_mots_cle_recherche, array(__CLASS__, '__anonyme_4'));
1642 raphael 664
 
2447 jpm 665
		if (self::$gestion_mots_cles == null) {
666
			$gestion_mots_cles = new GestionMotsCles($this->config, 'obs');
667
		}
668
		$mots_cles_ids = $gestion_mots_cles->obtenirIdsMotClesPourMotsCles($liste_mots_cle, $id_utilisateur);
669
		foreach ($mots_cles_ids as $mot_cle) {
670
			$resultat[$mot_cle['id_mot_cle']] = $mot_cle['mot_cle'];
671
		}
1677 raphael 672
 
2447 jpm 673
		$enregistrement['mots_cles_texte'] = implode(',', $liste_mots_cle);
674
		$enregistrement['_mots_cle'] = array(
675
			'existing' => $resultat,
676
			'to_insert' => array_diff($liste_mots_cle, $resultat));
2055 aurelien 677
	}
1677 raphael 678
 
1640 raphael 679
 
2447 jpm 680
	/* FONCTIONS de TRANSFORMATION de VALEUR DE CELLULE */
681
	// TODO: PHP 5.3, utiliser date_parse_from_format()
682
	// TODO: parser les heures (cf product-owner)
683
	// TODO: passer par le timestamp pour s'assurer de la validité
684
	static function traiterDateObs($date, $ref_ligne) {
685
		// TODO: see https://github.com/PHPOffice/PHPExcel/issues/208
686
		// TODO: PHPExcel_Shared_Date::ExcelToPHP()
687
		if (is_double($date)) {
688
			if ($date > 0) {
689
				return PHPExcel_Style_NumberFormat::toFormattedString($date, PHPExcel_Style_NumberFormat::FORMAT_DATE_YYYYMMDD2) . " 00:00:00";
690
			}
1770 raphael 691
 
2447 jpm 692
			$msg = "ligne «{$ref_ligne}»: Attention: date antérieure à 1970 et format de cellule «DATE» utilisés ensemble";
693
			trigger_error($msg, E_USER_NOTICE);
694
		} else {
695
			// attend l'un des formats de
696
			// http://www.php.net/manual/fr/datetime.formats.date.php
697
			// le plus simple: YYYY/MM/DD (utilisé à l'export), mais DD-MM-YYYY est aussi supporté
698
			$matches = NULL;
699
			// et on essaie d'être sympa et supporter aussi DD/MM/YYYY
700
			if (preg_match(';^([0-3]?\d)/([01]\d)/([12]\d\d\d)$;', $date, $matches)) {
701
				$date = $matches[3] . '/' . $matches[2] . '/' . $matches[1];
702
			}
703
			$timestamp = strtotime($date);
704
			if (! $timestamp || $timestamp > time() + 3600 * 24 * 1) { // une journée d'avance maxi autorisée (décallage horaire ?)
705
				if ($date) {
706
					$msg = "ligne «{$ref_ligne}»: Attention: date erronée ($date)";
707
					trigger_error($msg, E_USER_NOTICE);
708
				}
709
				return NULL;
710
			}
711
			return strftime('%Y-%m-%d 00:00:00', $timestamp);
712
		}
713
	}
1640 raphael 714
 
2447 jpm 715
	static function identReferentiel($referentiel, $ligne, $ref_ligne) {
716
		// SELECT DISTINCT nom_referentiel, COUNT(id_observation) AS count FROM cel_obs GROUP BY nom_referentiel ORDER BY count DESC;
717
		if (strpos($referentiel, 'bdtfx') !== FALSE) {
718
			return 'bdtfx'; //:v1.01';
719
		}
720
		if (strpos($referentiel, 'bdtxa') !== FALSE) {
721
			return 'bdtxa'; //:v1.00';
722
		}
723
		if (strpos($referentiel, 'bdnff') !== FALSE) {
724
			return 'bdtfx';
725
		}
726
		if (strpos($referentiel, 'isfan') !== FALSE) {
727
			return 'isfan'; //:v1.00';
728
		}
729
		if (strpos($referentiel, 'apd') !== FALSE) {
730
			return 'apd'; //:v1.00';
731
		}
732
		if (strpos($referentiel, 'autre') !== FALSE) {
733
			return 'autre';
734
		}
1640 raphael 735
 
2447 jpm 736
		if ($referentiel && isset($ligne[C_NOM_SEL]) && $ligne[C_NOM_SEL]) {
737
			$msg = "ligne «{$ref_ligne}»: Attention: référentiel «{$referentiel}» inconnu";
738
			trigger_error($msg, E_USER_NOTICE);
739
			return 'autre';
740
		}
741
		return NULL;
742
	}
1640 raphael 743
 
2447 jpm 744
	static function traiterLonLat($lon = NULL, $lat = NULL, $referentiel = 'bdtfx', $ref_ligne) {
745
		// en CSV ces valeurs sont des string, avec séparateur en français (","; cf défauts dans ExportXLS)
746
		if ($lon && is_string($lon)) {
747
			$lon = str_replace(',', '.', $lon);
748
		}
749
		if ($lat && is_string($lat)) {
750
			$lat = str_replace(',', '.', $lat);
751
		}
1636 raphael 752
 
2447 jpm 753
		// sprintf applique une précision à 5 décimale (comme le ferait MySQL)
754
		// tout en uniformisant le format de séparateur des décimales (le ".")
755
		if ($lon && is_numeric($lon) && $lon >= -180 && $lon <= 180) {
756
			return sprintf('%.5F', $lon);
757
		}
758
		if ($lat && is_numeric($lat) && $lat >= -90 && $lat <= 90) {
759
			return sprintf('%.5F', $lat);
760
		}
1642 raphael 761
 
2447 jpm 762
		if ($lon || $lat) {
763
			trigger_error("ligne \"{$ref_ligne}\": " .
764
				  "Attention: longitude ou latitude erronée",
765
				  E_USER_NOTICE);
766
		}
1640 raphael 767
		return NULL;
1636 raphael 768
	}
769
 
2447 jpm 770
	/*
771
	  TODO: s'affranchir du webservice pour la détermination du nom scientifique en s'appuyant sur cel_references,
772
	  pour des questions de performances
773
	*/
774
	static function traiterEspece($ligne, Array &$espece, &$referentiel, $taxon_info_webservice) {
775
		if (empty($ligne[C_NOM_SEL])) {
776
			return;
777
		}
1640 raphael 778
 
2447 jpm 779
		// nom_sel reste toujours celui de l'utilisateur
780
		$espece[C_NOM_SEL] = trim($ligne[C_NOM_SEL]);
1642 raphael 781
 
2447 jpm 782
		// XXX/attention, nous ne devrions pas accepter un référentiel absent !
783
		if (!$referentiel) {
784
			$referentiel = 'bdtfx';
785
		}
786
		$taxon_info_webservice->setReferentiel($referentiel);
787
		$ascii = iconv('UTF-8', 'ASCII//TRANSLIT', $ligne[C_NOM_SEL]);
1642 raphael 788
 
2447 jpm 789
		$determ = $taxon_info_webservice->rechercherInfosSurTexteCodeOuNumTax(trim($ligne[C_NOM_SEL]));
1640 raphael 790
 
2447 jpm 791
		// note: rechercherInfosSurTexteCodeOuNumTax peut ne retourner qu'une seule clef "nom_sel"
792
		if (! $determ) {
793
			// on supprime les noms retenus et renvoi tel quel
794
			// on réutilise les define pour les noms d'indexes, tant qu'à faire
795
			// XXX; tout à NULL sauf C_NOM_SEL ci-dessus ?
796
			$espece[C_NOM_SEL_NN] = @$ligne[C_NOM_SEL_NN];
797
			$espece[C_NOM_RET] = @$ligne[C_NOM_RET];
798
			$espece[C_NOM_RET_NN] = @$ligne[C_NOM_RET_NN];
799
			$espece[C_NT] = @$ligne[C_NT];
800
			$espece[C_FAMILLE] = @$ligne[C_FAMILLE];
1929 raphael 801
 
2447 jpm 802
			return;
803
		}
1636 raphael 804
 
2447 jpm 805
		// succès de la détection, mais résultat partiel
806
		if (!isset($determ->id)) {
807
			$determ = $taxon_info_webservice->effectuerRequeteInfosComplementairesSurNumNom($determ->{"nom_retenu.id"});
808
		}
1642 raphael 809
 
2447 jpm 810
		// ne devrait jamais arriver !
811
		if (!$determ) {
812
			die("erreur critique: " . __FILE__ . ':' . __LINE__);
813
		}
1651 raphael 814
 
2447 jpm 815
		// un schéma <ref>:(nt|nn):<num> (ie: bdtfx:nt:8503) a été passé
816
		// dans ce cas on met à jour le référentiel avec celui passé dans le champ espèce
817
		if (isset($determ->ref)) {
818
			$referentiel = $determ->ref;
819
		}
1651 raphael 820
 
2447 jpm 821
		// succès de la détection
822
		// nom_sel est remplacé, mais seulement si un motif spécial à été utilisé (bdtfx:nn:4567)
823
		if ($taxon_info_webservice->is_notation_spe) {
824
			$espece[C_NOM_SEL] = $determ->nom_sci;
825
		}
1640 raphael 826
 
2447 jpm 827
		// écrasement des numéros (nomenclatural, taxonomique) saisis...
828
		$espece[C_NOM_SEL_NN] = $determ->id;
829
		$espece[C_NOM_RET] = RechercheInfosTaxonBeta::supprimerBiblio($determ->nom_retenu_complet);
830
		$espece[C_NOM_RET_NN] = $determ->{"nom_retenu.id"};
831
		$espece[C_NT] = $determ->num_taxonomique;
832
		$espece[C_FAMILLE] = $determ->famille;
833
		return;
834
	}
1688 raphael 835
 
2447 jpm 836
	static function detectFromNom($nom) {
837
		$r = Cel::db()->requeter(sprintf("SELECT num_nom, num_tax_sup FROM bdtfx_v1_01 WHERE (nom_sci LIKE '%s') ".
838
			"ORDER BY nom_sci ASC LIMIT 0, 1",
839
			Cel::db()->proteger($nom)));
840
		if ($r) {
841
			return $r;
842
		}
1640 raphael 843
 
2447 jpm 844
		Cel::db()->requeter(sprintf("SELECT num_nom, num_tax_sup FROM bdtfx_v1_01 WHERE (nom_sci LIKE '%s' OR nom LIKE '%s') ".
845
			"ORDER BY nom_sci ASC LIMIT 0, 1",
846
			Cel::db()->proteger($nom),
847
			Cel::db()->proteger(str_replace(' ', '% ', $nom))));
848
		return $r;
1929 raphael 849
	}
1781 raphael 850
 
2447 jpm 851
	static function traiterLocalisation($ligne, Array &$localisation) {
852
		if (empty($ligne[C_ZONE_GEO])) {
853
			$ligne[C_ZONE_GEO] = NULL;
854
		}
855
		if (empty($ligne[C_CE_ZONE_GEO])) {
856
			$ligne[C_CE_ZONE_GEO] = NULL;
857
		}
1784 raphael 858
 
2447 jpm 859
		$identifiant_commune = trim($ligne[C_ZONE_GEO]);
860
		if (!$identifiant_commune) {
861
			$departement = trim($ligne[C_CE_ZONE_GEO]);
1784 raphael 862
 
2447 jpm 863
			if (strpos($departement, 'INSEE-C:', 0) === 0) {
864
				$localisation[C_CE_ZONE_GEO] = trim($ligne[C_CE_ZONE_GEO]);
865
				if (array_key_exists($localisation[C_CE_ZONE_GEO], self::$cache['geo'])) {
866
					$localisation[C_ZONE_GEO] = self::$cache['geo'][$localisation[C_CE_ZONE_GEO]];
867
				} else {
868
					$nom = Cel::db()->requeter(sprintf("SELECT nom FROM cel_zones_geo WHERE code = %s LIMIT 1",
869
						self::quoteNonNull(substr($localisation[C_CE_ZONE_GEO], strlen("INSEE-C:")))));
870
					if ($nom) {
871
						$localisation[C_ZONE_GEO] = $nom[0]['nom'];
872
					}
873
					self::$cache['geo'][$localisation[C_CE_ZONE_GEO]] = @$nom[0]['nom'];
874
				}
875
				return;
876
			}
1640 raphael 877
 
2447 jpm 878
			if (!is_numeric($departement)) {
879
				$localisation[C_CE_ZONE_GEO] = $ligne[C_CE_ZONE_GEO];
880
				return;
881
			}
1688 raphael 882
 
2447 jpm 883
			$cache_attempted = FALSE;
884
			if(array_key_exists($departement, self::$cache['geo'])) {
885
				$cache_attempted = TRUE;
886
				if (self::$cache['geo'][$departement][0] && self::$cache['geo'][$departement][1]) {
887
					$localisation[C_ZONE_GEO] = self::$cache['geo'][$departement][0];
888
					$localisation[C_CE_ZONE_GEO] = self::$cache['geo'][$departement][1];
889
					return;
890
				}
891
			}
1697 raphael 892
 
2447 jpm 893
			$requete = "SELECT DISTINCT nom, CONCAT('INSEE-C:', code) AS code ".
894
				'FROM cel_zones_geo '.
895
				'WHERE code = %s '.
896
				'LIMIT 1 '.
897
				' -- '.__FILE__.':'.__LINE__;
898
			$resultat_commune = Cel::db()->requeter(sprintf($requete, self::quoteNonNull($departement)));
899
			if (! $cache_attempted && $resultat_commune) {
900
				$localisation[C_ZONE_GEO] = $resultat_commune[0]['nom'];
901
				$localisation[C_CE_ZONE_GEO] = $resultat_commune[0]['code'];
902
				self::$cache['geo'][$departement] = array($resultat_commune[0]['nom'], $resultat_commune[0]['code']);
903
				return;
904
			}
905
			$localisation[C_CE_ZONE_GEO] = $ligne[C_CE_ZONE_GEO];
906
			return;
907
		}
1770 raphael 908
 
2447 jpm 909
		$select = "SELECT DISTINCT nom, code FROM cel_zones_geo";
1697 raphael 910
 
2447 jpm 911
		if (preg_match('/(.+) \((\d{1,5})\)/', $identifiant_commune, $elements)) {
912
			// commune + departement : montpellier (34)
913
			$nom_commune=$elements[1];
914
			$code_commune=$elements[2];
915
			if (strlen($code_commune) <= 2) {
916
				$requete = sprintf("%s WHERE nom = %s AND code LIKE %s",
917
					$select, self::quoteNonNull($nom_commune),
918
					self::quoteNonNull($code_commune.'%'));
919
			} else {
920
				$requete = sprintf("%s WHERE nom = %s AND code = %d",
921
					$select, self::quoteNonNull($nom_commune),
922
					$code_commune);
923
			}
924
		} elseif (preg_match('/^(\d+|(2[ab]\d+))$/i', $identifiant_commune, $elements)) {
925
			// Code insee seul
926
			$code_insee_commune=$elements[1];
927
			$requete = sprintf("%s WHERE code = %s", $select, self::quoteNonNull($code_insee_commune));
928
		} else {
929
			// Commune seule (le departement sera recupere dans la colonne departement si elle est presente)
930
			// on prend le risque ici de retourner une mauvaise Commune
931
			$nom_commune = str_replace(" ", "%", iconv('UTF-8', 'ASCII//TRANSLIT', $identifiant_commune));
932
			$requete = sprintf("%s WHERE nom LIKE %s", $select, self::quoteNonNull($nom_commune.'%'));
933
		}
1697 raphael 934
 
2447 jpm 935
		if (array_key_exists($identifiant_commune, self::$cache['geo'])) {
936
			$resultat_commune = self::$cache['geo'][$identifiant_commune];
937
		} else {
938
			$resultat_commune = Cel::db()->requeter($requete);
939
			self::$cache['geo'][$identifiant_commune] = $resultat_commune;
940
		}
1697 raphael 941
 
2447 jpm 942
		// cas de la commune introuvable dans le référentiel
943
		// réinitialisation aux valeurs du fichier XLS
944
		if (! $resultat_commune) {
945
			$localisation[C_ZONE_GEO] = trim($ligne[C_ZONE_GEO]);
946
			$localisation[C_CE_ZONE_GEO] = trim($ligne[C_CE_ZONE_GEO]);
947
		} else {
948
			$localisation[C_ZONE_GEO] = $resultat_commune[0]['nom'];
949
			$localisation[C_CE_ZONE_GEO] = "INSEE-C:" . $resultat_commune[0]['code'];
950
			return;
951
		}
1697 raphael 952
 
2447 jpm 953
		$departement =& $localisation[C_CE_ZONE_GEO];
1697 raphael 954
 
2447 jpm 955
		if (strpos($departement, "INSEE-C:", 0) === 0) {
956
			$localisation[C_ZONE_GEO] = $localisation[C_ZONE_GEO];
957
			$localisation[C_CE_ZONE_GEO] = $localisation[C_CE_ZONE_GEO];
958
		}
1697 raphael 959
 
2447 jpm 960
		if (!is_numeric($departement)) {
961
			$localisation[C_ZONE_GEO] = $localisation[C_ZONE_GEO];
962
			$localisation[C_CE_ZONE_GEO] = $localisation[C_CE_ZONE_GEO];
963
		}
1929 raphael 964
 
2447 jpm 965
		if (strlen($departement) == 4) {
966
			$departement = "INSEE-C:0$departement";
1697 raphael 967
		}
2447 jpm 968
		if (strlen($departement) == 5) {
969
			$departement = "INSEE-C:$departement";
1697 raphael 970
		}
2447 jpm 971
		$departement = trim($departement);
1697 raphael 972
 
2447 jpm 973
		$localisation[C_ZONE_GEO] = $localisation[C_ZONE_GEO];
974
		$localisation[C_CE_ZONE_GEO] = $localisation[C_CE_ZONE_GEO];
975
	}
1929 raphael 976
 
2447 jpm 977
	public static function stockerChampsEtendus($champs_etendus, $ordre_ids, $config) {
978
		// singleton du pauvre mais l'export est suffisamment inefficace pour s'en priver
979
		self::$gestion_champs_etendus = self::$gestion_champs_etendus == null ?
980
			new GestionChampsEtendus($config, 'obs') :
981
			self::$gestion_champs_etendus;
1697 raphael 982
 
2447 jpm 983
		$champs_etendus_obs = array();
984
		foreach ($champs_etendus as $champ_etendu_a_obs) {
985
			$id_obs = $ordre_ids[$champ_etendu_a_obs['ordre']]; // id réel de l'observation correspondant à l'ordre
986
			foreach ($champ_etendu_a_obs['champs_etendus'] as $label => $champ) {
987
				// XXX: insère t'on des valeurs vides ?
988
				$valeur = $champ;
989
				$cle = $label;
1697 raphael 990
 
2447 jpm 991
				if (!empty($cle) && !empty($valeur)) {
992
					$champ_etendu_a_inserer = new ChampEtendu();
993
					$champ_etendu_a_inserer->id = $id_obs;
994
					$champ_etendu_a_inserer->cle = $cle;
995
					$champ_etendu_a_inserer->valeur = $valeur;
1697 raphael 996
 
2447 jpm 997
					$champs_etendus_obs[] = $champ_etendu_a_inserer;
998
				}
999
			}
1000
		}
1697 raphael 1001
 
2447 jpm 1002
		self::$gestion_champs_etendus->ajouterParLots($champs_etendus_obs);
1003
		//TODO: que faire si l'insertion des champs étendus échoue ?
1004
		return count($champs_etendus_obs);
1929 raphael 1005
	}
1697 raphael 1006
 
2447 jpm 1007
	/* HELPERS */
1697 raphael 1008
 
2447 jpm 1009
	// http://stackoverflow.com/questions/348410/sort-an-array-based-on-another-array
1010
	// XXX; utilisé aussi (temporairement ?) par FormateurGroupeColonne.
1011
	static function sortArrayByArray($array, $orderArray) {
1012
		$ordered = array();
1013
		foreach($orderArray as $key) {
1014
			if (array_key_exists($key, $array)) {
1015
				$ordered[$key] = $array[$key];
1016
				unset($array[$key]);
1017
			}
1018
		}
1019
		return $ordered + $array;
1697 raphael 1020
	}
1021
 
2447 jpm 1022
	// retourne une BBox [N,S,E,O) pour un référentiel donné
1023
	static function getReferentielBBox($referentiel) {
1024
		if ($referentiel == 'bdtfx') {
1025
			return Array(
1026
				'NORD' => 51.2, // Dunkerque
1027
				'SUD' => 41.3, // Bonifacio
1028
				'EST' => 9.7, // Corse
1029
				'OUEST' => -5.2); // Ouessan
1030
		}
1031
		return FALSE;
1929 raphael 1032
	}
1642 raphael 1033
 
2447 jpm 1034
	// ces valeurs ne sont pas inséré via les placeholders du PDO::preparedStatement
1035
	// et doivent donc être échappées correctement.
1036
	public function initialiser_colonnes_statiques() {
1037
		$this->colonnes_statiques = array_merge($this->colonnes_statiques,
1038
			array(
1039
				'ce_utilisateur' => self::quoteNonNull($this->id_utilisateur), // peut-être un hash ou un id
1040
				'prenom_utilisateur' => self::quoteNonNull($this->utilisateur['prenom']),
1041
				'nom_utilisateur' => self::quoteNonNull($this->utilisateur['nom']),
1042
				'courriel_utilisateur' => self::quoteNonNull($this->utilisateur['courriel']),
1043
			));
1929 raphael 1044
	}
1045
 
2447 jpm 1046
	static function initialiser_pdo_ordered_statements($colonnes_statiques) {
1047
		return Array(
1048
			// insert_ligne_pattern_ordre
1049
			sprintf('INSERT INTO cel_obs (%s, %s) VALUES',
1050
				implode(', ', array_keys($colonnes_statiques)),
1051
				implode(', ', array_diff(self::$ordre_BDD, array_keys($colonnes_statiques)))),
1929 raphael 1052
 
2447 jpm 1053
			// insert_ligne_pattern_ordre
1054
			sprintf('(%s, %s ?)',
1055
				implode(', ', $colonnes_statiques),
1056
				str_repeat('?, ', count(self::$ordre_BDD) - count($colonnes_statiques) - 1))
1057
		);
1929 raphael 1058
	}
1059
 
2447 jpm 1060
	static function initialiser_pdo_statements($colonnes_statiques) {
1061
		return Array(
1062
			// insert_prefix
1063
			sprintf('INSERT INTO cel_obs (%s) VALUES ',
1064
				implode(', ', self::$ordre_BDD)),
1929 raphael 1065
 
1066
 
2447 jpm 1067
			// insert_ligne_pattern, cf: self::$insert_ligne_pattern
1068
			'(' .
1069
			// 3) créé une chaîne de liste de champ à inséré en DB
1070
			implode(', ', array_values(
1071
			// 2) garde les valeurs fixes (de $colonnes_statiques),
1072
			// mais remplace les NULL par des "?"
1073
			array_map('__anonyme_5',
1074
				  // 1) créé un tableau genre (nom_sel_nn => NULL) depuis self::$ordre_BDD
1075
				  // et écrase certaines valeurs avec $colonnes_statiques (initilisé avec les données utilisateur)
1076
				  array_merge(array_map('__anonyme_6', array_flip(self::$ordre_BDD)), $colonnes_statiques
1077
				  )))) .
1078
			')'
1079
		);
1080
	}
1929 raphael 1081
 
2447 jpm 1082
	// équivalent à Bdd2->proteger() (qui wrap PDO::quote),
1083
	// sans transformer NULL en ""
1084
	static function quoteNonNull($chaine) {
1085
		if (is_null($chaine)) {
1086
			return 'NULL';
1087
		}
1088
		if (!is_string($chaine) && !is_integer($chaine)) {
1089
			die('erreur: ' . __FILE__ . ':' . __LINE__);
1090
		}
1091
		return Cel::db()->quote($chaine);
1636 raphael 1092
	}
1640 raphael 1093
 
2447 jpm 1094
	public function erreurs_stock($errno, $errstr) {
1095
		$this->bilan[] = $errstr;
1642 raphael 1096
	}
2447 jpm 1097
}