Subversion Repositories eFlore/Applications.cel

Rev

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