Subversion Repositories eFlore/Applications.cel

Rev

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