Subversion Repositories eFlore/Applications.cel

Rev

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