Subversion Repositories eFlore/Applications.cel

Rev

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