Subversion Repositories eFlore/Applications.cel

Rev

Rev 2660 | Rev 2664 | 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) {
2663 aurelien 329
			$retour = array('import_erreurs' => '"'.implode("\n", $this->bilan).'"');
2657 aurelien 330
		} else {
331
			$retour = array(
2663 aurelien 332
						'import_obs_ajoutees' => '"'.$obs_ajouts.'"',
333
						'import_images_ajoutees' => '"'.$nb_images_ajoutees.'"',
334
						'import_mots_cles_ajoutes' => '"'.$nb_mots_cle_ajoutes.'"',
335
						'import_colonnes_non_traitees' => '"'.implode(', ', $filtre->exclues).'"'
2657 aurelien 336
					);
2447 jpm 337
		}
2657 aurelien 338
		// le JSON ci-dessus est envoyé à la main car provenant d'une requête synchrone
339
		// certains navigateurs/plugins le modifient à la volée pour l'afficher (genre jsonView)
340
		// ce qui fait rater le parsing de la requete
2663 aurelien 341
		echo json_encode($retour);
2447 jpm 342
		die();
1929 raphael 343
	}
1642 raphael 344
 
2447 jpm 345
	/* detectionEntete() sert deux rôles:
346
	 1) détecter le type de colonne attendu à partir des textes de la ligne d'en-tête afin de define()
347
	 2) permet d'identifier les colonnes non-supportées/inutiles afin d'alléger le processus de parsing de PHPExcel
348
	 grace au ReadFilter (C'est le rôle de la valeur de retour)
1642 raphael 349
 
2447 jpm 350
	 La raison de la présence du paramètre $numeric_keys est que pour réussir à identifier les colonnes à exclure nous
351
	 devons traiter un tableau représentant la ligne d'en-tête aussi bien:
352
	  - sous forme associative pour PHPExcel (les clefs sont les lettres de l'alphabet)
353
	  - sous forme de clefs numériques (fgetcsv())
354
	  Le détecter après coup est difficile et pourtant cette distinction est importante car le comportement
355
	  d'array_merge() (réordonnancement des clefs numérique) n'est pas souhaitable dans le second cas. */
356
	static function detectionEntete($entete, $numeric_keys = FALSE) {
357
		$colonnes_reconnues = Array();
358
		$cols = FormateurGroupeColonne::nomEnsembleVersListeColonnes('standard,avance');
1792 raphael 359
 
2447 jpm 360
		foreach ($entete as $k => $v) {
361
			// traite les colonnes en faisant fi de la casse et des accents
362
			$entete_simple = iconv('UTF-8', 'ASCII//TRANSLIT', strtolower(trim($v)));
363
			foreach ($cols as $col) {
364
				$entete_officiel_simple = iconv('UTF-8', 'ASCII//TRANSLIT', strtolower(trim($col['nom'])));
365
				$entete_officiel_abbrev = $col['abbrev'];
366
				if ($entete_simple == $entete_officiel_simple || $entete_simple == $entete_officiel_abbrev) {
367
					// debug echo "define C_" . strtoupper($entete_officiel_abbrev) . ", $k ($v)\n";
368
					define("C_" . strtoupper($entete_officiel_abbrev), $k);
369
					$colonnes_reconnues[$k] = 1;
370
					break;
371
				}
1636 raphael 372
 
2447 jpm 373
				if (strpos($v, self::$prefixe_colonnes_etendues) === 0) {
374
					$colonnes_reconnues[$k] = 1;
375
					self::$indexes_colonnes_etendues[$k] = $v;
376
					break;
377
				}
2381 aurelien 378
			}
2447 jpm 379
		}
2381 aurelien 380
 
2447 jpm 381
		// défini tous les index que nous utilisons à une valeur d'index de colonne Excel qui n'existe pas dans
382
		// le tableau renvoyé par PHPExcel
383
		// Attention cependant d'utiliser des indexes différenciés car traiterLonLat() et traiterEspece()
384
		// les utilisent
385
		foreach ($cols as $col) {
386
			if (!defined('C_'.strtoupper($col['abbrev']))) {
387
				define('C_'.strtoupper($col['abbrev']), 'C_'.strtoupper($col['abbrev']));
2381 aurelien 388
			}
2447 jpm 389
		}
1636 raphael 390
 
2447 jpm 391
		// prépare le filtre de PHPExcel qui évitera le traitement de toutes les colonnes superflues
392
		$colonnesID_non_reconnues = array_diff_key($entete, $colonnes_reconnues);
1640 raphael 393
 
2447 jpm 394
		// des colonnes de FormateurGroupeColonne::nomEnsembleVersListeColonnes()
395
		// ne retient que celles marquées "importables"
396
		$colonnes_automatiques = array_filter($cols, '__anonyme_1');
1636 raphael 397
 
2447 jpm 398
		// ne conserve que le nom long pour matcher avec la ligne XLS d'entête
399
		array_walk($colonnes_automatiques, '__anonyme_2');
1640 raphael 400
 
2447 jpm 401
		$colonnesID_a_exclure = array_intersect($entete, $colonnes_automatiques);
1640 raphael 402
 
2447 jpm 403
		if ($numeric_keys) {
404
			return $colonnesID_non_reconnues + $colonnesID_a_exclure;
405
		}
406
		return array_merge($colonnesID_non_reconnues, $colonnesID_a_exclure);
407
	}
1636 raphael 408
 
2447 jpm 409
	static function chargerCorrespondancesIdOrdre($cel, $lignes) {
410
		$ordresObs = array();
411
		foreach ($lignes as &$ligne) {
412
			$ordresObs[] = $ligne['ordre'];
413
		}
414
		$ordresObsConcat = implode(',', $ordresObs);
415
		$idUtilisateurP = Cel::db()->proteger($cel->id_utilisateur);
416
		$requete = 'SELECT id_observation, ordre '.
417
			'FROM cel_obs '.
418
			"WHERE ordre IN ($ordresObsConcat) ".
419
			"AND ce_utilisateur = $idUtilisateurP ".
420
			' -- '.__FILE__.':'.__LINE__;
421
		$resultats = Cel::db()->requeter($requete);
422
		$ordresIds = array();
423
		foreach ($resultats as &$infos) {
424
			$ordresIds[$infos['ordre']] = $infos['id_observation'];
425
		}
426
		return $ordresIds;
1933 raphael 427
	}
1636 raphael 428
 
2447 jpm 429
	/*
430
	 * charge un groupe de lignes
431
	 */
432
	static function chargerLignes($cel, $lignes, $colonnes_statiques, &$dernier_ordre) {
433
		$enregistrement = NULL;
434
		$enregistrements = array();
435
		$toutes_images = array();
436
		$tous_mots_cle = array();
437
		$tous_champs_etendus = array();
1640 raphael 438
 
2447 jpm 439
		foreach ($lignes as $ligne) {
440
			// dans le cas de fgetcsv, on peut avoir des false additionnel (cf do/while l. 279)
441
			if ($ligne === false) {
442
				continue;
443
			}
1933 raphael 444
 
2447 jpm 445
			// on a besoin des NULL pour éviter des notice d'index indéfini
446
			if (! array_filter($ligne, '__anonyme_3')) {
447
				continue;
448
			}
1640 raphael 449
 
2447 jpm 450
			if ($enregistrement = self::chargerLigne($ligne, $dernier_ordre, $cel)) {
451
				// $enregistrements[] = array_merge($colonnes_statiques, $enregistrement);
452
				$enregistrements[] = $enregistrement;
453
				$pos = count($enregistrements) - 1;
454
				$last = &$enregistrements[$pos];
1640 raphael 455
 
2447 jpm 456
				if (isset($enregistrement['_images'])) {
457
					// ne dépend pas de cel_obs, et seront insérées *après* les enregistrements
458
					// mais nous ne voulons pas nous priver de faire des INSERT multiples pour autant
459
					$toutes_images[] = array(
460
						'images' => $last['_images'],
461
						'obs_pos' => $pos);
462
					// ce champ n'a pas à faire partie de l'insertion dans cel_obs,
463
					// mais est utile pour la liaison avec les images
464
					unset($last['_images']);
465
				}
1640 raphael 466
 
2447 jpm 467
				if (isset($enregistrement['_mots_cle'])) {
468
					// ne dépend pas de cel_obs, et seront insérés *après* les enregistrements
469
					// mais nous ne voulons pas nous priver de faire des INSERT multiples pour autant
470
					$tous_mots_cle[] = array(
471
						'mots_cle' => $last['_mots_cle'],
472
						'obs_pos' => $pos);
473
					unset($last['_mots_cle']);
474
				}
475
 
476
				if (isset($enregistrement['_champs_etendus'])) {
477
					$tous_champs_etendus[] = array(
478
						'champs_etendus' => $last['_champs_etendus'],
479
						'ordre' => $dernier_ordre);
480
					unset($last['_champs_etendus']);
481
				}
482
				$dernier_ordre++;
483
			}
1636 raphael 484
		}
2447 jpm 485
		return array($enregistrements, $toutes_images, $tous_mots_cle, $tous_champs_etendus);
486
	}
1640 raphael 487
 
2447 jpm 488
	static function trierColonnes(&$enregistrements) {
489
		foreach ($enregistrements as &$enregistrement) {
490
			$enregistrement = self::sortArrayByArray($enregistrement, self::$ordre_BDD);
2381 aurelien 491
		}
1642 raphael 492
	}
1640 raphael 493
 
2447 jpm 494
	static function stockerMotsCle($enregistrements, $tous_mots_cle, $lastid) {
495
		$c = 0;
496
		// debug: var_dump($tous_mots_cle);die;
497
		foreach ($tous_mots_cle as $v) {
498
			$c += count($v['mots_cle']['to_insert']);
499
		}
500
		return $c;
1677 raphael 501
	}
502
 
2447 jpm 503
	static function stockerImages($enregistrements, $toutes_images, $ordre_ids) {
2461 jpm 504
		$valuesSql = array();
2447 jpm 505
		foreach ($toutes_images as $images_pour_obs) {
506
			$obs = $enregistrements[$images_pour_obs['obs_pos']];
507
			$id_obs = $ordre_ids[$obs['ordre']]; // id réel de l'observation correspondant à l'ordre
2461 jpm 508
			$transmission = $obs['transmission'];
509
			$date_transmission = $obs['date_transmission'];
2447 jpm 510
			foreach ($images_pour_obs['images'] as $image) {
2461 jpm 511
				$id_img = $image['id_image'];
512
				$valuesSql[] = "($id_img, $id_obs, NOW(), $transmission, $date_transmission)";
2447 jpm 513
			}
514
		}
1642 raphael 515
 
2461 jpm 516
		if ($valuesSql) {
517
			$clauseValues = implode(', ', $valuesSql);
518
			// Utilisation de INSERT pour faire des UPDATE multiples en une seule requête
519
			$requete = 'INSERT INTO cel_images '.
520
				'(id_image, ce_observation, date_liaison, transmission, date_transmission) '.
2447 jpm 521
				"VALUES $clauseValues ".
2461 jpm 522
				'ON DUPLICATE KEY UPDATE '.
523
				'ce_observation = VALUES(ce_observation), '.
524
				'date_liaison = NOW(), '.
525
				'transmission = VALUES(transmission), '.
526
				'date_transmission = VALUES(date_transmission) '.
2447 jpm 527
				' -- '.__FILE__.':'.__LINE__;
2461 jpm 528
			Cel::db()->executer($requete);
2447 jpm 529
		}
2461 jpm 530
		return count($valuesSql);
1636 raphael 531
	}
532
 
2447 jpm 533
	/*
534
	 Aucune des valeurs présentes dans $enregistrement n'est quotée
535
	 cad aucune des valeurs retournée par traiter{Espece|Localisation}()
536
	 car ce tableau est passé à un PDO::preparedStatement() qui applique
537
	  proprement les règle d'échappement.
538
	*/
539
	static function chargerLigne($ligne, $dernier_ordre, $cel) {
540
		// évite des notices d'index lors des trigger_error()
541
		$ref_ligne = !empty($ligne[C_NOM_SEL]) ? trim($ligne[C_NOM_SEL]) : '';
1770 raphael 542
 
2447 jpm 543
		// en premier car le résultat est utile pour
544
		// * traiter espèce (traiterEspece())
545
		// * traiter longitude et latitude (traiterLonLat())
546
		$referentiel = self::identReferentiel(trim(strtolower(@$ligne[C_NOM_REFERENTIEL])), $ligne, $ref_ligne);
1636 raphael 547
 
2447 jpm 548
		// $espece est rempli de plusieurs informations
549
		$espece = array(
550
			C_NOM_SEL => NULL,
551
			C_NOM_SEL_NN => NULL,
552
			C_NOM_RET => NULL,
553
			C_NOM_RET_NN => NULL,
554
			C_NT => NULL,
555
			C_FAMILLE => NULL);
556
		self::traiterEspece($ligne, $espece, $referentiel, $cel->taxon_info_webservice);
1636 raphael 557
 
2447 jpm 558
		if (!$espece[C_NOM_SEL]) {
559
			$referentiel = Cel::$fallback_referentiel;
560
		}
561
		if ($espece[C_NOM_SEL] && !$espece[C_NOM_SEL_NN]) {
562
			$referentiel = Cel::$fallback_referentiel;
563
		}
1852 raphael 564
 
2447 jpm 565
		// $localisation est rempli à partir de plusieurs champs: C_ZONE_GEO et C_CE_ZONE_GEO
566
		$localisation = Array(C_ZONE_GEO => NULL, C_CE_ZONE_GEO => NULL);
567
		self::traiterLocalisation($ligne, $localisation);
2538 aurelien 568
		//TODO: le jour où c'est efficace, traiter le pays à l'import
1636 raphael 569
 
2447 jpm 570
		// $transmission est utilisé pour date_transmission
571
		// XXX: @ contre "Undefined index"
572
		@$transmission = in_array(strtolower(trim($ligne[C_TRANSMISSION])), array(1, 'oui')) ? 1 : 0;
1649 raphael 573
 
574
 
2447 jpm 575
		// Dans ce tableau, seules devraient apparaître les données variable pour chaque ligne.
576
		// Dans ce tableau, l'ordre des clefs n'importe pas (cf: self::sortArrayByArray())
2461 jpm 577
		$enregistrement = array(
2447 jpm 578
			"ordre" => $dernier_ordre,
1640 raphael 579
 
2447 jpm 580
			"nom_sel" => $espece[C_NOM_SEL],
581
			"nom_sel_nn" => $espece[C_NOM_SEL_NN],
582
			"nom_ret" => $espece[C_NOM_RET],
583
			"nom_ret_nn" => $espece[C_NOM_RET_NN],
584
			"nt" => $espece[C_NT],
585
			"famille" => $espece[C_FAMILLE],
1640 raphael 586
 
2447 jpm 587
			"nom_referentiel" => $referentiel,
1640 raphael 588
 
2538 aurelien 589
			"pays" => $ligne[C_PAYS],
2447 jpm 590
			"zone_geo" => $localisation[C_ZONE_GEO],
591
			"ce_zone_geo" => $localisation[C_CE_ZONE_GEO],
1640 raphael 592
 
2447 jpm 593
			// $ligne: uniquement pour les infos en cas de gestion d'erreurs (date incompréhensible)
2461 jpm 594
			"date_observation" => isset($ligne[C_DATE_OBSERVATION]) ? self::traiterDateObs($ligne[C_DATE_OBSERVATION], $ref_ligne) : null,
1640 raphael 595
 
2461 jpm 596
			"lieudit" => isset($ligne[C_LIEUDIT]) ? trim($ligne[C_LIEUDIT]) : null,
597
			"station" => isset($ligne[C_STATION]) ? trim($ligne[C_STATION]) : null,
598
			"milieu" => isset($ligne[C_MILIEU]) ? trim($ligne[C_MILIEU]) : null,
1642 raphael 599
 
2447 jpm 600
			"mots_cles_texte" => NULL, // TODO: foreign-key
601
			// XXX: @ contre "Undefined index"
2461 jpm 602
			"commentaire" => isset($ligne[C_COMMENTAIRE]) ? trim($ligne[C_COMMENTAIRE]) : null,
1642 raphael 603
 
2447 jpm 604
			"transmission" => $transmission,
2461 jpm 605
			"date_transmission" => $transmission ? date('Y-m-d H:i:s') : null, // pas de fonction SQL dans un PDO statement, <=> now()
1642 raphael 606
 
2447 jpm 607
			// $ligne: uniquement pour les infos en cas de gestion d'erreurs (lon/lat incompréhensible)
2461 jpm 608
			"latitude" => isset($ligne[C_LATITUDE]) ? self::traiterLonLat(null, $ligne[C_LATITUDE], $referentiel, $ref_ligne) : null,
609
			"longitude" => isset($ligne[C_LONGITUDE]) ? self::traiterLonLat($ligne[C_LONGITUDE], null, $referentiel, $ref_ligne) : null,
610
			"altitude" => isset($ligne[C_ALTITUDE]) ? intval($ligne[C_ALTITUDE]) : null, // TODO: guess alt from lon/lat
1648 raphael 611
 
2447 jpm 612
			// @ car potentiellement optionnelles ou toutes vides => pas d'index dans PHPExcel (tableau optimisé)
613
			"abondance" => @$ligne[C_ABONDANCE],
614
			"certitude" => @$ligne[C_CERTITUDE],
615
			"phenologie" => @$ligne[C_PHENOLOGIE],
1648 raphael 616
 
2447 jpm 617
			"code_insee_calcule" => substr($localisation[C_CE_ZONE_GEO], -5) // varchar(5)
618
		);
1642 raphael 619
 
2447 jpm 620
		// passage de $enregistrement par référence, ainsi ['_images'] n'est défini
621
		// que si des résultats sont trouvés
622
		// "@" car PHPExcel supprime les colonnes null sur toute la feuille (ou tout le chunk)
623
		if (@$ligne[C_IMAGES]) {
624
			self::traiterImage($ligne[C_IMAGES], $cel->id_utilisateur, $enregistrement);
625
		}
1642 raphael 626
 
2447 jpm 627
		if (@$ligne[C_MOTS_CLES_TEXTE]) {
628
			self::traiterMotsCle($ligne[C_MOTS_CLES_TEXTE], $cel->id_utilisateur, $enregistrement);
629
		}
1677 raphael 630
 
2447 jpm 631
		$champs_etendus = self::traiterChampsEtendus($ligne, self::$indexes_colonnes_etendues);
632
		if (!empty($champs_etendus)) {
633
			$enregistrement['_champs_etendus'] = $champs_etendus;
634
		}
1636 raphael 635
 
2447 jpm 636
		return $enregistrement;
2381 aurelien 637
	}
1675 raphael 638
 
2447 jpm 639
	static function traiterChampsEtendus(&$ligne, &$indexes_colonnes_etendues) {
640
		$champs_etendus_indexes = array();
641
		foreach($indexes_colonnes_etendues as $index_num => $label) {
642
			if (isset($ligne[$index_num])) {
643
				$champs_etendus_indexes[str_replace(self::$prefixe_colonnes_etendues, '', $label)] = $ligne[$index_num];
644
			}
645
		}
646
		return $champs_etendus_indexes;
647
	}
1640 raphael 648
 
2447 jpm 649
	static function traiterImage($str, $id_utilisateur, &$enregistrement) {
650
		$liste_images = array_filter(explode('/', $str));
651
		array_walk($liste_images, array(__CLASS__, '__anonyme_4'));
1640 raphael 652
 
2447 jpm 653
		$nomsOrignalConcat = implode(',', $liste_images);
654
		$requete = 'SELECT id_image, nom_original '.
655
			'FROM cel_images '.
656
			"WHERE ce_utilisateur = $id_utilisateur AND nom_original IN ($nomsOrignalConcat) ".
657
			' -- '.__FILE__.':'.__LINE__;
658
		$resultat = Cel::db()->requeter($requete);
1642 raphael 659
 
2447 jpm 660
		if ($resultat) {
661
			$enregistrement['_images'] = $resultat;
662
		}
663
	}
1678 raphael 664
 
2447 jpm 665
	static function traiterMotsCle($str, $id_utilisateur, &$enregistrement) {
666
		$liste_mots_cle = $liste_mots_cle_recherche = array_map('trim', array_unique(array_filter(explode(',', $str))));
667
		array_walk($liste_mots_cle_recherche, array(__CLASS__, '__anonyme_4'));
1642 raphael 668
 
2447 jpm 669
		if (self::$gestion_mots_cles == null) {
670
			$gestion_mots_cles = new GestionMotsCles($this->config, 'obs');
671
		}
672
		$mots_cles_ids = $gestion_mots_cles->obtenirIdsMotClesPourMotsCles($liste_mots_cle, $id_utilisateur);
673
		foreach ($mots_cles_ids as $mot_cle) {
674
			$resultat[$mot_cle['id_mot_cle']] = $mot_cle['mot_cle'];
675
		}
1677 raphael 676
 
2447 jpm 677
		$enregistrement['mots_cles_texte'] = implode(',', $liste_mots_cle);
678
		$enregistrement['_mots_cle'] = array(
679
			'existing' => $resultat,
680
			'to_insert' => array_diff($liste_mots_cle, $resultat));
2055 aurelien 681
	}
1677 raphael 682
 
1640 raphael 683
 
2447 jpm 684
	/* FONCTIONS de TRANSFORMATION de VALEUR DE CELLULE */
685
	// TODO: PHP 5.3, utiliser date_parse_from_format()
686
	// TODO: parser les heures (cf product-owner)
687
	// TODO: passer par le timestamp pour s'assurer de la validité
688
	static function traiterDateObs($date, $ref_ligne) {
689
		// TODO: see https://github.com/PHPOffice/PHPExcel/issues/208
690
		// TODO: PHPExcel_Shared_Date::ExcelToPHP()
691
		if (is_double($date)) {
692
			if ($date > 0) {
693
				return PHPExcel_Style_NumberFormat::toFormattedString($date, PHPExcel_Style_NumberFormat::FORMAT_DATE_YYYYMMDD2) . " 00:00:00";
694
			}
1770 raphael 695
 
2447 jpm 696
			$msg = "ligne «{$ref_ligne}»: Attention: date antérieure à 1970 et format de cellule «DATE» utilisés ensemble";
697
			trigger_error($msg, E_USER_NOTICE);
698
		} else {
699
			// attend l'un des formats de
700
			// http://www.php.net/manual/fr/datetime.formats.date.php
701
			// le plus simple: YYYY/MM/DD (utilisé à l'export), mais DD-MM-YYYY est aussi supporté
702
			$matches = NULL;
703
			// et on essaie d'être sympa et supporter aussi DD/MM/YYYY
704
			if (preg_match(';^([0-3]?\d)/([01]\d)/([12]\d\d\d)$;', $date, $matches)) {
705
				$date = $matches[3] . '/' . $matches[2] . '/' . $matches[1];
706
			}
707
			$timestamp = strtotime($date);
708
			if (! $timestamp || $timestamp > time() + 3600 * 24 * 1) { // une journée d'avance maxi autorisée (décallage horaire ?)
709
				if ($date) {
710
					$msg = "ligne «{$ref_ligne}»: Attention: date erronée ($date)";
711
					trigger_error($msg, E_USER_NOTICE);
712
				}
713
				return NULL;
714
			}
715
			return strftime('%Y-%m-%d 00:00:00', $timestamp);
716
		}
717
	}
1640 raphael 718
 
2447 jpm 719
	static function identReferentiel($referentiel, $ligne, $ref_ligne) {
720
		// SELECT DISTINCT nom_referentiel, COUNT(id_observation) AS count FROM cel_obs GROUP BY nom_referentiel ORDER BY count DESC;
721
		if (strpos($referentiel, 'bdtfx') !== FALSE) {
722
			return 'bdtfx'; //:v1.01';
723
		}
724
		if (strpos($referentiel, 'bdtxa') !== FALSE) {
725
			return 'bdtxa'; //:v1.00';
726
		}
727
		if (strpos($referentiel, 'bdnff') !== FALSE) {
728
			return 'bdtfx';
729
		}
730
		if (strpos($referentiel, 'isfan') !== FALSE) {
731
			return 'isfan'; //:v1.00';
732
		}
733
		if (strpos($referentiel, 'apd') !== FALSE) {
734
			return 'apd'; //:v1.00';
735
		}
736
		if (strpos($referentiel, 'autre') !== FALSE) {
737
			return 'autre';
738
		}
1640 raphael 739
 
2447 jpm 740
		if ($referentiel && isset($ligne[C_NOM_SEL]) && $ligne[C_NOM_SEL]) {
741
			$msg = "ligne «{$ref_ligne}»: Attention: référentiel «{$referentiel}» inconnu";
742
			trigger_error($msg, E_USER_NOTICE);
743
			return 'autre';
744
		}
745
		return NULL;
746
	}
1640 raphael 747
 
2447 jpm 748
	static function traiterLonLat($lon = NULL, $lat = NULL, $referentiel = 'bdtfx', $ref_ligne) {
749
		// en CSV ces valeurs sont des string, avec séparateur en français (","; cf défauts dans ExportXLS)
750
		if ($lon && is_string($lon)) {
751
			$lon = str_replace(',', '.', $lon);
752
		}
753
		if ($lat && is_string($lat)) {
754
			$lat = str_replace(',', '.', $lat);
755
		}
1636 raphael 756
 
2447 jpm 757
		// sprintf applique une précision à 5 décimale (comme le ferait MySQL)
758
		// tout en uniformisant le format de séparateur des décimales (le ".")
759
		if ($lon && is_numeric($lon) && $lon >= -180 && $lon <= 180) {
760
			return sprintf('%.5F', $lon);
761
		}
762
		if ($lat && is_numeric($lat) && $lat >= -90 && $lat <= 90) {
763
			return sprintf('%.5F', $lat);
764
		}
1642 raphael 765
 
2447 jpm 766
		if ($lon || $lat) {
767
			trigger_error("ligne \"{$ref_ligne}\": " .
768
				  "Attention: longitude ou latitude erronée",
769
				  E_USER_NOTICE);
770
		}
1640 raphael 771
		return NULL;
1636 raphael 772
	}
773
 
2447 jpm 774
	/*
775
	  TODO: s'affranchir du webservice pour la détermination du nom scientifique en s'appuyant sur cel_references,
776
	  pour des questions de performances
777
	*/
778
	static function traiterEspece($ligne, Array &$espece, &$referentiel, $taxon_info_webservice) {
779
		if (empty($ligne[C_NOM_SEL])) {
780
			return;
781
		}
1640 raphael 782
 
2447 jpm 783
		// nom_sel reste toujours celui de l'utilisateur
784
		$espece[C_NOM_SEL] = trim($ligne[C_NOM_SEL]);
1642 raphael 785
 
2447 jpm 786
		// XXX/attention, nous ne devrions pas accepter un référentiel absent !
787
		if (!$referentiel) {
788
			$referentiel = 'bdtfx';
789
		}
790
		$taxon_info_webservice->setReferentiel($referentiel);
791
		$ascii = iconv('UTF-8', 'ASCII//TRANSLIT', $ligne[C_NOM_SEL]);
1642 raphael 792
 
2447 jpm 793
		$determ = $taxon_info_webservice->rechercherInfosSurTexteCodeOuNumTax(trim($ligne[C_NOM_SEL]));
1640 raphael 794
 
2447 jpm 795
		// note: rechercherInfosSurTexteCodeOuNumTax peut ne retourner qu'une seule clef "nom_sel"
796
		if (! $determ) {
797
			// on supprime les noms retenus et renvoi tel quel
798
			// on réutilise les define pour les noms d'indexes, tant qu'à faire
799
			// XXX; tout à NULL sauf C_NOM_SEL ci-dessus ?
800
			$espece[C_NOM_SEL_NN] = @$ligne[C_NOM_SEL_NN];
801
			$espece[C_NOM_RET] = @$ligne[C_NOM_RET];
802
			$espece[C_NOM_RET_NN] = @$ligne[C_NOM_RET_NN];
803
			$espece[C_NT] = @$ligne[C_NT];
804
			$espece[C_FAMILLE] = @$ligne[C_FAMILLE];
1929 raphael 805
 
2447 jpm 806
			return;
807
		}
1636 raphael 808
 
2447 jpm 809
		// succès de la détection, mais résultat partiel
810
		if (!isset($determ->id)) {
811
			$determ = $taxon_info_webservice->effectuerRequeteInfosComplementairesSurNumNom($determ->{"nom_retenu.id"});
812
		}
1642 raphael 813
 
2447 jpm 814
		// ne devrait jamais arriver !
815
		if (!$determ) {
816
			die("erreur critique: " . __FILE__ . ':' . __LINE__);
817
		}
1651 raphael 818
 
2447 jpm 819
		// un schéma <ref>:(nt|nn):<num> (ie: bdtfx:nt:8503) a été passé
820
		// dans ce cas on met à jour le référentiel avec celui passé dans le champ espèce
821
		if (isset($determ->ref)) {
822
			$referentiel = $determ->ref;
823
		}
1651 raphael 824
 
2447 jpm 825
		// succès de la détection
826
		// nom_sel est remplacé, mais seulement si un motif spécial à été utilisé (bdtfx:nn:4567)
827
		if ($taxon_info_webservice->is_notation_spe) {
828
			$espece[C_NOM_SEL] = $determ->nom_sci;
829
		}
1640 raphael 830
 
2447 jpm 831
		// écrasement des numéros (nomenclatural, taxonomique) saisis...
832
		$espece[C_NOM_SEL_NN] = $determ->id;
833
		$espece[C_NOM_RET] = RechercheInfosTaxonBeta::supprimerBiblio($determ->nom_retenu_complet);
834
		$espece[C_NOM_RET_NN] = $determ->{"nom_retenu.id"};
835
		$espece[C_NT] = $determ->num_taxonomique;
836
		$espece[C_FAMILLE] = $determ->famille;
837
		return;
838
	}
1688 raphael 839
 
2447 jpm 840
	static function detectFromNom($nom) {
841
		$r = Cel::db()->requeter(sprintf("SELECT num_nom, num_tax_sup FROM bdtfx_v1_01 WHERE (nom_sci LIKE '%s') ".
842
			"ORDER BY nom_sci ASC LIMIT 0, 1",
843
			Cel::db()->proteger($nom)));
844
		if ($r) {
845
			return $r;
846
		}
1640 raphael 847
 
2447 jpm 848
		Cel::db()->requeter(sprintf("SELECT num_nom, num_tax_sup FROM bdtfx_v1_01 WHERE (nom_sci LIKE '%s' OR nom LIKE '%s') ".
849
			"ORDER BY nom_sci ASC LIMIT 0, 1",
850
			Cel::db()->proteger($nom),
851
			Cel::db()->proteger(str_replace(' ', '% ', $nom))));
852
		return $r;
1929 raphael 853
	}
1781 raphael 854
 
2447 jpm 855
	static function traiterLocalisation($ligne, Array &$localisation) {
856
		if (empty($ligne[C_ZONE_GEO])) {
857
			$ligne[C_ZONE_GEO] = NULL;
858
		}
859
		if (empty($ligne[C_CE_ZONE_GEO])) {
860
			$ligne[C_CE_ZONE_GEO] = NULL;
861
		}
1784 raphael 862
 
2447 jpm 863
		$identifiant_commune = trim($ligne[C_ZONE_GEO]);
864
		if (!$identifiant_commune) {
865
			$departement = trim($ligne[C_CE_ZONE_GEO]);
1784 raphael 866
 
2447 jpm 867
			if (strpos($departement, 'INSEE-C:', 0) === 0) {
868
				$localisation[C_CE_ZONE_GEO] = trim($ligne[C_CE_ZONE_GEO]);
869
				if (array_key_exists($localisation[C_CE_ZONE_GEO], self::$cache['geo'])) {
870
					$localisation[C_ZONE_GEO] = self::$cache['geo'][$localisation[C_CE_ZONE_GEO]];
871
				} else {
872
					$nom = Cel::db()->requeter(sprintf("SELECT nom FROM cel_zones_geo WHERE code = %s LIMIT 1",
873
						self::quoteNonNull(substr($localisation[C_CE_ZONE_GEO], strlen("INSEE-C:")))));
874
					if ($nom) {
875
						$localisation[C_ZONE_GEO] = $nom[0]['nom'];
876
					}
877
					self::$cache['geo'][$localisation[C_CE_ZONE_GEO]] = @$nom[0]['nom'];
878
				}
879
				return;
880
			}
1640 raphael 881
 
2447 jpm 882
			if (!is_numeric($departement)) {
883
				$localisation[C_CE_ZONE_GEO] = $ligne[C_CE_ZONE_GEO];
884
				return;
885
			}
1688 raphael 886
 
2447 jpm 887
			$cache_attempted = FALSE;
888
			if(array_key_exists($departement, self::$cache['geo'])) {
889
				$cache_attempted = TRUE;
890
				if (self::$cache['geo'][$departement][0] && self::$cache['geo'][$departement][1]) {
891
					$localisation[C_ZONE_GEO] = self::$cache['geo'][$departement][0];
892
					$localisation[C_CE_ZONE_GEO] = self::$cache['geo'][$departement][1];
893
					return;
894
				}
895
			}
1697 raphael 896
 
2447 jpm 897
			$requete = "SELECT DISTINCT nom, CONCAT('INSEE-C:', code) AS code ".
898
				'FROM cel_zones_geo '.
899
				'WHERE code = %s '.
900
				'LIMIT 1 '.
901
				' -- '.__FILE__.':'.__LINE__;
902
			$resultat_commune = Cel::db()->requeter(sprintf($requete, self::quoteNonNull($departement)));
903
			if (! $cache_attempted && $resultat_commune) {
904
				$localisation[C_ZONE_GEO] = $resultat_commune[0]['nom'];
905
				$localisation[C_CE_ZONE_GEO] = $resultat_commune[0]['code'];
906
				self::$cache['geo'][$departement] = array($resultat_commune[0]['nom'], $resultat_commune[0]['code']);
907
				return;
908
			}
909
			$localisation[C_CE_ZONE_GEO] = $ligne[C_CE_ZONE_GEO];
910
			return;
911
		}
1770 raphael 912
 
2447 jpm 913
		$select = "SELECT DISTINCT nom, code FROM cel_zones_geo";
1697 raphael 914
 
2447 jpm 915
		if (preg_match('/(.+) \((\d{1,5})\)/', $identifiant_commune, $elements)) {
916
			// commune + departement : montpellier (34)
917
			$nom_commune=$elements[1];
918
			$code_commune=$elements[2];
919
			if (strlen($code_commune) <= 2) {
920
				$requete = sprintf("%s WHERE nom = %s AND code LIKE %s",
921
					$select, self::quoteNonNull($nom_commune),
922
					self::quoteNonNull($code_commune.'%'));
923
			} else {
924
				$requete = sprintf("%s WHERE nom = %s AND code = %d",
925
					$select, self::quoteNonNull($nom_commune),
926
					$code_commune);
927
			}
928
		} elseif (preg_match('/^(\d+|(2[ab]\d+))$/i', $identifiant_commune, $elements)) {
929
			// Code insee seul
930
			$code_insee_commune=$elements[1];
931
			$requete = sprintf("%s WHERE code = %s", $select, self::quoteNonNull($code_insee_commune));
932
		} else {
933
			// Commune seule (le departement sera recupere dans la colonne departement si elle est presente)
934
			// on prend le risque ici de retourner une mauvaise Commune
935
			$nom_commune = str_replace(" ", "%", iconv('UTF-8', 'ASCII//TRANSLIT', $identifiant_commune));
936
			$requete = sprintf("%s WHERE nom LIKE %s", $select, self::quoteNonNull($nom_commune.'%'));
937
		}
1697 raphael 938
 
2447 jpm 939
		if (array_key_exists($identifiant_commune, self::$cache['geo'])) {
940
			$resultat_commune = self::$cache['geo'][$identifiant_commune];
941
		} else {
942
			$resultat_commune = Cel::db()->requeter($requete);
943
			self::$cache['geo'][$identifiant_commune] = $resultat_commune;
944
		}
1697 raphael 945
 
2447 jpm 946
		// cas de la commune introuvable dans le référentiel
947
		// réinitialisation aux valeurs du fichier XLS
948
		if (! $resultat_commune) {
949
			$localisation[C_ZONE_GEO] = trim($ligne[C_ZONE_GEO]);
950
			$localisation[C_CE_ZONE_GEO] = trim($ligne[C_CE_ZONE_GEO]);
951
		} else {
952
			$localisation[C_ZONE_GEO] = $resultat_commune[0]['nom'];
953
			$localisation[C_CE_ZONE_GEO] = "INSEE-C:" . $resultat_commune[0]['code'];
954
			return;
955
		}
1697 raphael 956
 
2447 jpm 957
		$departement =& $localisation[C_CE_ZONE_GEO];
1697 raphael 958
 
2447 jpm 959
		if (strpos($departement, "INSEE-C:", 0) === 0) {
960
			$localisation[C_ZONE_GEO] = $localisation[C_ZONE_GEO];
961
			$localisation[C_CE_ZONE_GEO] = $localisation[C_CE_ZONE_GEO];
962
		}
1697 raphael 963
 
2447 jpm 964
		if (!is_numeric($departement)) {
965
			$localisation[C_ZONE_GEO] = $localisation[C_ZONE_GEO];
966
			$localisation[C_CE_ZONE_GEO] = $localisation[C_CE_ZONE_GEO];
967
		}
1929 raphael 968
 
2447 jpm 969
		if (strlen($departement) == 4) {
970
			$departement = "INSEE-C:0$departement";
1697 raphael 971
		}
2447 jpm 972
		if (strlen($departement) == 5) {
973
			$departement = "INSEE-C:$departement";
1697 raphael 974
		}
2447 jpm 975
		$departement = trim($departement);
1697 raphael 976
 
2447 jpm 977
		$localisation[C_ZONE_GEO] = $localisation[C_ZONE_GEO];
978
		$localisation[C_CE_ZONE_GEO] = $localisation[C_CE_ZONE_GEO];
979
	}
1929 raphael 980
 
2447 jpm 981
	public static function stockerChampsEtendus($champs_etendus, $ordre_ids, $config) {
982
		// singleton du pauvre mais l'export est suffisamment inefficace pour s'en priver
983
		self::$gestion_champs_etendus = self::$gestion_champs_etendus == null ?
984
			new GestionChampsEtendus($config, 'obs') :
985
			self::$gestion_champs_etendus;
1697 raphael 986
 
2447 jpm 987
		$champs_etendus_obs = array();
988
		foreach ($champs_etendus as $champ_etendu_a_obs) {
989
			$id_obs = $ordre_ids[$champ_etendu_a_obs['ordre']]; // id réel de l'observation correspondant à l'ordre
990
			foreach ($champ_etendu_a_obs['champs_etendus'] as $label => $champ) {
991
				// XXX: insère t'on des valeurs vides ?
992
				$valeur = $champ;
993
				$cle = $label;
1697 raphael 994
 
2447 jpm 995
				if (!empty($cle) && !empty($valeur)) {
996
					$champ_etendu_a_inserer = new ChampEtendu();
997
					$champ_etendu_a_inserer->id = $id_obs;
998
					$champ_etendu_a_inserer->cle = $cle;
999
					$champ_etendu_a_inserer->valeur = $valeur;
1697 raphael 1000
 
2447 jpm 1001
					$champs_etendus_obs[] = $champ_etendu_a_inserer;
1002
				}
1003
			}
1004
		}
1697 raphael 1005
 
2447 jpm 1006
		self::$gestion_champs_etendus->ajouterParLots($champs_etendus_obs);
1007
		//TODO: que faire si l'insertion des champs étendus échoue ?
1008
		return count($champs_etendus_obs);
1929 raphael 1009
	}
1697 raphael 1010
 
2447 jpm 1011
	/* HELPERS */
1697 raphael 1012
 
2447 jpm 1013
	// http://stackoverflow.com/questions/348410/sort-an-array-based-on-another-array
1014
	// XXX; utilisé aussi (temporairement ?) par FormateurGroupeColonne.
1015
	static function sortArrayByArray($array, $orderArray) {
1016
		$ordered = array();
1017
		foreach($orderArray as $key) {
1018
			if (array_key_exists($key, $array)) {
1019
				$ordered[$key] = $array[$key];
1020
				unset($array[$key]);
1021
			}
1022
		}
1023
		return $ordered + $array;
1697 raphael 1024
	}
1025
 
2447 jpm 1026
	// retourne une BBox [N,S,E,O) pour un référentiel donné
1027
	static function getReferentielBBox($referentiel) {
1028
		if ($referentiel == 'bdtfx') {
1029
			return Array(
1030
				'NORD' => 51.2, // Dunkerque
1031
				'SUD' => 41.3, // Bonifacio
1032
				'EST' => 9.7, // Corse
1033
				'OUEST' => -5.2); // Ouessan
1034
		}
1035
		return FALSE;
1929 raphael 1036
	}
1642 raphael 1037
 
2447 jpm 1038
	// ces valeurs ne sont pas inséré via les placeholders du PDO::preparedStatement
1039
	// et doivent donc être échappées correctement.
1040
	public function initialiser_colonnes_statiques() {
1041
		$this->colonnes_statiques = array_merge($this->colonnes_statiques,
1042
			array(
1043
				'ce_utilisateur' => self::quoteNonNull($this->id_utilisateur), // peut-être un hash ou un id
1044
				'prenom_utilisateur' => self::quoteNonNull($this->utilisateur['prenom']),
1045
				'nom_utilisateur' => self::quoteNonNull($this->utilisateur['nom']),
1046
				'courriel_utilisateur' => self::quoteNonNull($this->utilisateur['courriel']),
1047
			));
1929 raphael 1048
	}
1049
 
2447 jpm 1050
	static function initialiser_pdo_ordered_statements($colonnes_statiques) {
1051
		return Array(
1052
			// insert_ligne_pattern_ordre
1053
			sprintf('INSERT INTO cel_obs (%s, %s) VALUES',
1054
				implode(', ', array_keys($colonnes_statiques)),
1055
				implode(', ', array_diff(self::$ordre_BDD, array_keys($colonnes_statiques)))),
1929 raphael 1056
 
2447 jpm 1057
			// insert_ligne_pattern_ordre
1058
			sprintf('(%s, %s ?)',
1059
				implode(', ', $colonnes_statiques),
1060
				str_repeat('?, ', count(self::$ordre_BDD) - count($colonnes_statiques) - 1))
1061
		);
1929 raphael 1062
	}
1063
 
2447 jpm 1064
	static function initialiser_pdo_statements($colonnes_statiques) {
1065
		return Array(
1066
			// insert_prefix
1067
			sprintf('INSERT INTO cel_obs (%s) VALUES ',
1068
				implode(', ', self::$ordre_BDD)),
1929 raphael 1069
 
1070
 
2447 jpm 1071
			// insert_ligne_pattern, cf: self::$insert_ligne_pattern
1072
			'(' .
1073
			// 3) créé une chaîne de liste de champ à inséré en DB
1074
			implode(', ', array_values(
1075
			// 2) garde les valeurs fixes (de $colonnes_statiques),
1076
			// mais remplace les NULL par des "?"
1077
			array_map('__anonyme_5',
1078
				  // 1) créé un tableau genre (nom_sel_nn => NULL) depuis self::$ordre_BDD
1079
				  // et écrase certaines valeurs avec $colonnes_statiques (initilisé avec les données utilisateur)
1080
				  array_merge(array_map('__anonyme_6', array_flip(self::$ordre_BDD)), $colonnes_statiques
1081
				  )))) .
1082
			')'
1083
		);
1084
	}
1929 raphael 1085
 
2461 jpm 1086
	// équivalent à Bdd->proteger() (qui wrap PDO::quote),
2447 jpm 1087
	// sans transformer NULL en ""
1088
	static function quoteNonNull($chaine) {
1089
		if (is_null($chaine)) {
1090
			return 'NULL';
1091
		}
1092
		if (!is_string($chaine) && !is_integer($chaine)) {
1093
			die('erreur: ' . __FILE__ . ':' . __LINE__);
1094
		}
1095
		return Cel::db()->quote($chaine);
1636 raphael 1096
	}
1640 raphael 1097
 
2447 jpm 1098
	public function erreurs_stock($errno, $errstr) {
1099
		$this->bilan[] = $errstr;
1642 raphael 1100
	}
2657 aurelien 1101
}
1102
?>