Subversion Repositories eFlore/Applications.cel

Rev

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