Subversion Repositories eFlore/Applications.cel

Rev

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