Subversion Repositories eFlore/Applications.cel

Rev

Rev 3030 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

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