Subversion Repositories eFlore/Applications.cel

Rev

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