Subversion Repositories eFlore/Applications.cel

Rev

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

Rev Author Line No. Line
1636 raphael 1
<?php
2
 
3
/**
4
* @category  PHP
5
* @package   jrest
6
* @author    Raphaël Droz <raphael@tela-botania.org>
7
* @copyright 2013 Tela-Botanica
8
* @license   http://www.cecill.info/licences/Licence_CeCILL_V2-fr.txt Licence CECILL
9
* @license GPL v3 <http://www.gnu.org/licenses/gpl.txt>
10
*/
11
 
12
/**
13
 * Service d'import de données d'observation du CEL au format XLS
14
 */
15
 
1638 raphael 16
// sont define()'d commme n° de colonne tous les abbrevs retournés par ExportXLS::nom_d_ensemble_vers_liste_de_colonnes()
1636 raphael 17
// préfixés par C_  cf: detectionEntete()
18
 
19
set_include_path(get_include_path() . PATH_SEPARATOR . dirname(dirname(realpath(__FILE__))) . '/lib');
20
// TERM
21
error_reporting(-1);
22
ini_set('html_errors', 0);
23
ini_set('xdebug.cli_color', 2);
24
require_once('lib/PHPExcel/Classes/PHPExcel.php');
25
require_once('ExportXLS.php');
26
 
1640 raphael 27
 
28
date_default_timezone_set("Europe/Paris");
29
 
30
// nombre d'INSERT à cumuler par requête SQL
31
// (= nombre de lignes XLS à bufferiser)
32
define('NB_LIRE_LIGNE_SIMUL', 30);
33
 
34
// Numbers of days between January 1, 1900 and 1970 (including 19 leap years)
35
// see traiterDateObs()
36
define("MIN_DATES_DIFF", 25569);
37
 
38
 
1636 raphael 39
class MyReadFilter implements PHPExcel_Reader_IReadFilter {
1640 raphael 40
	// exclusion de colonnes
1638 raphael 41
	public $exclues = array();
1640 raphael 42
 
43
	// lecture par morceaux
44
    public $ligne_debut = 0;
45
    public $ligne_fin = 0;
46
 
1636 raphael 47
	public function __construct() {}
1640 raphael 48
	public function def_interval($debut, $nb) {
49
		$this->ligne_debut = $debut;
50
		$this->ligne_fin = $debut + $nb;
51
	}
1638 raphael 52
    public function readCell($colonne, $ligne, $worksheetName = '') {
53
		if(@$this->exclues[$colonne]) return false;
1640 raphael 54
		// si des n° de morceaux ont été initialisés, on filtre...
55
		if($this->ligne_debut && ($ligne < $this->ligne_debut || $ligne >= $this->ligne_fin)) return false;
1636 raphael 56
		return true;
57
    }
58
}
59
 
60
class ImportXLS extends Cel  {
61
 
62
	static $ordre_BDD = Array(
63
		"ce_utilisateur",
64
		"prenom_utilisateur",
65
		"nom_utilisateur",
66
		"courriel_utilisateur",
67
		"ordre",
68
		"nom_sel",
69
		"nom_sel_nn",
70
		"nom_ret",
71
		"nom_ret_nn",
72
		"nt",
73
		"famille",
74
		"nom_referentiel",
75
		"zone_geo",
76
		"ce_zone_geo",
77
		"date_observation",
78
		"lieudit",
79
		"station",
80
		"milieu",
81
		"commentaire",
82
		"transmission",
83
		"date_creation",
84
		"date_modification",
85
		"latitude",
86
		"longitude");
87
 
1640 raphael 88
	/*
89
	  Ces colonnes:
90
	  - sont propre à tous les enregistrements uploadés
91
	  - sont indépendantes du numéro de lignes
92
	  - n'ont pas de valeur par défaut dans la structure de la table
93
	  - nécessitent une initialisation dans le cadre de l'upload
94
	*/
95
	public $colonnes_statiques = Array(
96
		"ce_utilisateur" => NULL,
97
		"prenom_utilisateur" => NULL,
98
		"nom_utilisateur" => NULL,
99
		"courriel_utilisateur" => NULL,
100
 
101
		// XXX: fixes (mais pourraient varier dans le futur si la mise-à-jour
102
		// d'observation est implémentée
103
		"date_creation" => NULL, // ne peut initialiser d'une date ici
104
		"date_modification" => NULL, // idem, cf initialiser_colonnes_statiques()
105
	);
106
 
1636 raphael 107
	function ExportXLS($config) {
108
		parent::__construct($config);
109
	}
110
 
111
	function createElement($pairs) {
112
		if(!isset($pairs['utilisateur']) || trim($pairs['utilisateur']) == '') {
113
			echo '0'; exit;
114
		}
1640 raphael 115
		$id_utilisateur = intval($pairs['utilisateur']);
116
 
1636 raphael 117
		if(!isset($_SESSION)) session_start();
1640 raphael 118
        $this->controleUtilisateur($id_utilisateur);
1636 raphael 119
 
1640 raphael 120
        $this->utilisateur = $this->getInfosComplementairesUtilisateur($id_utilisateur);
121
		$this->initialiser_colonnes_statiques($id_utilisateur);
1636 raphael 122
 
1640 raphael 123
 
1636 raphael 124
		$infos_fichier = array_pop($_FILES);
125
 
126
		/*$objPHPExcel = PHPExcel_IOFactory::load($infos_fichier['tmp_name']);
1638 raphael 127
		  $donnees = $objPHPExcel->getActiveSheet()->toArray(NULL,FALSE,FALSE,TRUE);*/
1636 raphael 128
 
129
		/*$objReader = PHPExcel_IOFactory::createReader("Excel5");
130
		$objReader->setReadDataOnly(true);
131
		$objPHPExcel = $objReader->load($infos_fichier['tmp_name']);*/
132
 
1638 raphael 133
		//var_dump($donnees);
1636 raphael 134
 
135
		$objReader = PHPExcel_IOFactory::createReader("Excel5");
136
		$objReader->setReadDataOnly(true);
1640 raphael 137
 
138
		// on ne conserve que l'en-tête
139
		$filtre = new MyReadFilter();
140
		$filtre->def_interval(1, 2);
141
		$objReader->setReadFilter($filtre);
142
 
1636 raphael 143
		$objPHPExcel = $objReader->load($infos_fichier['tmp_name']);
1640 raphael 144
		$obj_infos = $objReader->listWorksheetInfo($infos_fichier['tmp_name']);
145
		// XXX: indépendant du readFilter ?
146
		$nb_lignes = $obj_infos[0]['totalRows'];
1636 raphael 147
 
1640 raphael 148
		$donnees = $objPHPExcel->getActiveSheet()->toArray(NULL, FALSE, FALSE, TRUE);
149
		$filtre->exclues = self::detectionEntete($donnees[1]);
1636 raphael 150
 
1640 raphael 151
		$obs_ajouts = 0;
152
		$obs_maj = 0;
153
		$dernier_ordre = $this->requeter("SELECT MAX(ordre) AS ordre FROM cel_obs WHERE ce_utilisateur = $id_utilisateur");
154
		$dernier_ordre = intval($dernier_ordre[0]['ordre']) + 1;
155
		if(! $dernier_ordre) $dernier_ordre = 0;
156
 
157
		// lecture par morceaux (chunks), NB_LIRE_LIGNE_SIMUL lignes à fois
158
		// pour aboutir des requêtes SQL d'insert groupés.
159
		for($ligne = 2; $ligne < $nb_lignes + NB_LIRE_LIGNE_SIMUL; $ligne += NB_LIRE_LIGNE_SIMUL) {
160
			$filtre->def_interval($ligne, NB_LIRE_LIGNE_SIMUL);
161
			$objReader->setReadFilter($filtre);
162
 
163
			/* recharge avec $filtre actif (filtre sur lignes colonnes):
164
			   - exclue les colonnes inutiles/inutilisables)
165
			   - ne selectionne que les lignes dans le range [$ligne - $ligne + NB_LIRE_LIGNE_SIMUL] */
166
			$objPHPExcel = $objReader->load($infos_fichier['tmp_name']);
167
			$donnees = $objPHPExcel->getActiveSheet()->toArray(NULL, FALSE, FALSE, TRUE);
168
 
169
			// ici on appel la fonction qui fera effectivement l'insertion multiple
170
			// à partir des (au plus) NB_LIRE_LIGNE_SIMUL lignes
171
 
172
			// TODO: passer $this, ne sert que pour appeler des méthodes public qui pourraient être statiques
173
			// notamment dans RechercheInfosTaxonBeta.php
174
			self::chargerLignes($donnees, $this->colonnes_statiques, $dernier_ordre, $this, $obs_ajouts, $obs_maj);
175
		}
176
		die('end');
1636 raphael 177
	}
178
 
179
	static function detectionEntete($entete) {
180
		$colonnes_reconnues = Array();
1638 raphael 181
		$cols = ExportXLS::nom_d_ensemble_vers_liste_de_colonnes('standard');
1636 raphael 182
		foreach($entete as $k => $v) {
183
			$entete_simple = iconv('UTF-8', 'ASCII//TRANSLIT', strtolower(trim($v)));
184
			foreach($cols as $col) {
185
				$entete_officiel_simple = iconv('UTF-8', 'ASCII//TRANSLIT', strtolower(trim($col['nom'])));
1638 raphael 186
				$entete_officiel_abbrev = $col['abbrev'];
187
				if($entete_simple == $entete_officiel_simple || $entete_simple == $entete_officiel_abbrev) {
1640 raphael 188
					// debug echo "define C_" . strtoupper($entete_officiel_abbrev) . ", $k\n";
1638 raphael 189
					define("C_" . strtoupper($entete_officiel_abbrev), $k);
1636 raphael 190
					$colonnes_reconnues[$k] = 1;
191
					break;
192
				}
193
			}
194
		}
195
 
1640 raphael 196
		// prépare le filtre de PHPExcel qui évitera le traitement de toutes les colonnes superflues
197
 
198
		// eg: diff ( Array( H => Commune, I => rien ) , Array( H => 1, K => 1 )
199
		// ==> Array( I => rien )
1636 raphael 200
		$colonnesID_non_reconnues = array_diff_key($entete, $colonnes_reconnues);
201
 
1640 raphael 202
		// des colonnes de ExportXLS::nom_d_ensemble_vers_liste_de_colonnes()
203
		// ne retient que celles marquées "importables"
1636 raphael 204
		$colonnes_automatiques = array_filter($cols, function($v) {	return !$v['importable']; });
1640 raphael 205
 
1636 raphael 206
		// ne conserve que le nom long pour matcher avec la ligne XLS d'entête
207
		array_walk($colonnes_automatiques, function(&$v) {	$v = $v['nom']; });
1640 raphael 208
 
209
		// intersect ( Array ( N => Milieu, S => Ordre ), Array ( ordre => Ordre, phenologie => Phénologie ) )
210
		// ==> Array ( S => Ordre, AA => Phénologie )
1636 raphael 211
		$colonnesID_a_exclure = array_intersect($entete, $colonnes_automatiques);
212
 
1640 raphael 213
		// TODO: pourquoi ne pas comparer avec les abbrevs aussi ?
214
		// merge ( Array( I => rien ) , Array ( S => Ordre, AA => Phénologie ) )
215
		// ==> Array ( I => rien, AA => Phénologie )
1636 raphael 216
		return array_merge($colonnesID_non_reconnues, $colonnesID_a_exclure);
217
	}
218
 
1640 raphael 219
	/*
220
	 * charge un groupe de lignes
221
	 */
222
	static function chargerLignes($lignes, $colonnes_statiques, &$dernier_ordre, $cel, &$inserted, &$updated) {
223
		$enregistrement = NULL;
224
		$enregistrements = Array();
225
		$images = Array();
226
 
227
		foreach($lignes as $ligne) {
228
			$ligne = array_filter($ligne, function($cell) { return !is_null($cell); });
229
			if(!$ligne) continue;
230
 
231
			if( ($enregistrement = self::chargerLigne($ligne, $colonnes_statiques, $dernier_ordre, $cel)) ) {
232
				$enregistrements[] = $enregistrement;
233
 
234
				if(isset($enregistrement['_images'])) {
235
					$pos = count($enregistrements) - 1;
236
					// ne dépend pas de cel_obs, et seront insérées *après* les enregistrements
237
					// mais nous ne voulons pas nous priver de faire des INSERT multiples pour autant
238
					$images[] = Array("text" => $enregistrements[$pos]['_images'],
239
									  "obs_pos" => $pos);
240
					// ce champ n'a pas a faire partie de l'insertion dans cel_obs,
241
					// mais est utile pour cel_obs_images
242
					unset($enregistrements[$pos]['_images']);
243
				}
244
 
245
				$dernier_ordre++;
246
			}
1636 raphael 247
		}
1640 raphael 248
 
249
		if(!$enregistrements) die('AIE // XXX');
250
 
251
		$req = '';
252
 
253
 
254
		foreach($enregistrements as $enregistrement)
255
			$req .= implode(', ', self::sortArrayByArray($enregistrement, self::$ordre_BDD));
256
		print_r($req);
257
 
258
		// $cel->executer($req);
259
		// transactionnel + auto-inc
260
		$lastid = $cel->bdd->lastInsertId();
261
		foreach($images as $image) {
262
			$obs = $enregistrements[$image["obs_pos"]];
263
			$id_obs = $lastid // dernier autoinc inséré
264
				- count($enregistrements) - 1 // correspondrait au premier autoinc
265
				+ $image["obs_pos"]; // ordre d'insertion = ordre dans le tableau $enregistrements
266
			// TODO: INSERT
267
		}
1636 raphael 268
	}
269
 
270
 
1640 raphael 271
	static function chargerLigne($ligne, $colonnes_statiques, $dernier_ordre, $cel) {
1636 raphael 272
		// en premier car le résultat est utile pour
273
		// traiter longitude et latitude (traiterLonLat())
1640 raphael 274
		$referentiel = self::identReferentiel($ligne[C_NOM_REFERENTIEL]);
1636 raphael 275
 
1640 raphael 276
		// $espece est rempli de plusieurs informations
277
		$espece = Array();
278
		self::traiterEspece($ligne, $espece, $cel);
1636 raphael 279
 
1640 raphael 280
		return array_merge($colonnes_statiques,
281
						   // Dans ce tableau, seules devraient apparaître les données variable pour chaque ligne.
282
						   // Dans ce tableau, l'ordre des clefs n'importe pas (cf: self::sortArrayByArray())
283
						   Array(
284
							   "ordre" => $dernier_ordre,
1636 raphael 285
 
1640 raphael 286
							   "nom_sel" => $espece[C_NOM_SEL],
287
							   "nom_sel_nn" => $espece[C_NOM_SEL_NN],
288
							   "nom_ret" => $espece[C_NOM_RET],
289
							   "nom_ret_nn" => $espece[C_NOM_RET_NN],
290
							   "nt" => $espece[C_NT],
291
							   "famille" => $espece[C_FAMILLE],
292
 
293
							   "nom_referentiel" => $referentiel,
294
 
295
							   "zone_geo" => TODO,
296
							   "ce_zone_geo" => self::traiterDepartement(trim($ligne[C_CE_ZONE_GEO])),
297
 
298
							   "date_observation" => self::traiterDateObs($ligne[C_DATE_OBSERVATION]),
299
							   "lieudit" => trim($ligne[C_LIEUDIT]),
300
							   "station" => trim($ligne[C_STATION]),
301
							   "milieu" => trim($ligne[C_MILIEU]),
302
							   "commentaire" => trim($ligne[C_COMMENTAIRE]), // TODO: foreign-key
303
 
304
							   "transmission" => in_array(strtolower(trim($ligne[C_TRANSMISSION])), array(1, 'oui')) ? 1 : 0,
305
 
306
							   "latitude" => self::traiterLonLat(NULL, $ligne[C_LATITUDE], $referentiel),
307
							   "longitude" => self::traiterLonLat($ligne[C_LONGITUDE], NULL, $referentiel),
308
						   ));
1636 raphael 309
	}
310
 
1640 raphael 311
 
312
 
313
	/* FONCTIONS de TRANSFORMATION de VALEUR DE CELLULE */
314
 
315
	// TODO: PHP 5.3, utiliser date_parse_from_format()
316
	// TODO: parser les heures (cf product-owner)
317
	// TODO: passer par le timestamp pour s'assurer de la validité
1636 raphael 318
	static function traiterDateObs($date) {
1640 raphael 319
		// TODO: see https://github.com/PHPOffice/PHPExcel/issues/208
320
		if(is_double($date)) {
321
			if($date > 0)
322
				return PHPExcel_Style_NumberFormat::toFormattedString($date, PHPExcel_Style_NumberFormat::FORMAT_DATE_YYYYMMDD2) . " 00:00:00";
323
			throw new Exception("erreur: date antérieure à 1970 et format de cellule \"DATE\" utilisés ensemble");
324
 
325
			// attention, UNIX timestamp, car Excel les décompte depuis 1900
326
			// cf http://fczaja.blogspot.fr/2011/06/convert-excel-date-into-timestamp.html
327
			// $timestamp = ($date - MIN_DATES_DIFF) * 60 * 60 * 24 - time(); // NON
328
 
329
			// $timestamp = PHPExcel_Calculation::getInstance()->calculateFormula("=" . $date . "-DATE(1970,1,1)*60*60*24"); // NON
330
 
331
			// echo strftime("%Y/%m/%d 00:00:00", $timestamp); // NON
332
		}
333
		else {
334
			$timestamp = strtotime($date);
335
			if(!$timestamp) return NULL; // TODO: throw error
336
			return strftime("%Y-%m-%d 00:00:00", strtotime($date));
337
		}
1636 raphael 338
	}
339
 
340
	static function identReferentiel($referentiel) {
1640 raphael 341
		// SELECT DISTINCT nom_referentiel, COUNT(id_observation) AS count FROM cel_obs GROUP BY nom_referentiel ORDER BY count DESC;
342
		if(strpos(strtolower($referentiel), 'bdtfx') !== FALSE) return 'bdtfx:v1.01';
343
		if(strpos(strtolower($referentiel), 'bdtxa') !== FALSE) return 'bdtxa:v1.00';
344
		if(strpos(strtolower($referentiel), 'bdnff') !== FALSE) return 'bdnff:4.02';
345
		if(strpos(strtolower($referentiel), 'isfan') !== FALSE) return 'isfan:v1.00';
346
		return NULL;
347
		/* TODO: cf story,
348
		   En cas de NULL faire une seconde passe de détection à partir du nom saisie */
1636 raphael 349
	}
350
 
1640 raphael 351
	/* NON!
352
	   Un taxon d'un référentiel donné peut être théoriquement observé n'importe où sur le globe.
353
	   Il n'y a pas lieu d'effectuer des restriction ici.
354
	   Cependant des erreurs fréquentes (0,0 ou lon/lat inversées) peuvent être détectés ici.
355
	   TODO */
1636 raphael 356
	static function traiterLonLat($lon = NULL, $lat = NULL, $referentiel = 'bdtfx:v1.01') {
357
		// verifier format decimal +
358
		// + limite france si bdtfx ou bdtxa
1640 raphael 359
 
360
		$bbox = self::getReferentielBBox($referentiel);
361
		if(!$bbox) return NULL;
362
 
363
		if($lon) {
364
			if($lon < $bbox['EST'] && $lon > $bbox['OUEST']) return is_numeric($lon) ? $lon : NULL;
365
			else return NULL;
366
		}
367
		if($lat) {
368
			if($lat < $bbox['NORD'] && $lat > $bbox['SUD']) return is_numeric($lat) ? $lat : NULL;
369
			return NULL;
370
		}
1636 raphael 371
	}
372
 
1640 raphael 373
 
374
	static function traiterEspece($ligne, Array &$espece, $cel) {
375
		$taxon_info_webservice = new RechercheInfosTaxonBeta($cel->config);
376
 
377
		$ascii = iconv('UTF-8', 'ASCII//TRANSLIT', $ligne[C_NOM_SEL]);
378
		$resultat_recherche_espece = $taxon_info_webservice->rechercherInfosSurTexteCodeOuNumTax($ligne[C_NOM_SEL]);
379
 
380
		// on supprime les noms retenus et renvoi tel quel
381
		// on réutilise les define pour les noms d'indexes, tant qu'à faire
382
		if (empty($resultat_recherche_espece['en_id_nom'])) {
383
			$espece[C_NOM_SEL] = $ligne[C_NOM_SEL];
384
			$espece[C_NOM_SEL_NN] = $ligne[C_NOM_SEL_NN];
385
 
386
			// TODO: si empty(C_NOM_SEL) et !empty(C_NOM_SEL_NN) : recherche info à partir de C_NOM_SEL_NN
387
			$espece[C_NOM_RET] = $ligne[C_NOM_RET];
388
			$espece[C_NOM_RET_NN] = $ligne[C_NOM_RET_NN];
389
			$espece[C_NT] = $ligne[C_NT];
390
			$espece[C_FAMILLE] = $ligne[C_FAMILLE];
391
 
392
			return;
393
		}
394
 
395
		// succès de la détection, récupération des infos
396
		$espece[C_NOM_SEL] = $resultat_recherche_espece['nom_sel'];
397
		$espece[C_NOM_SEL_NN] = $resultat_recherche_espece['en_id_nom'];
398
 
399
		$complement = $taxon_info_webservice->rechercherInformationsComplementairesSurNumNom($resultat_recherche_espece['en_id_nom']);
400
		$espece[C_NOM_RET] = $complement['Nom_Retenu'];
401
		$espece[C_NOM_RET_NN] = $complement['Num_Nom_Retenu'];
402
		$espece[C_NT] = $complement['Num_Taxon'];
403
		$espece[C_FAMILLE] = $complement['Famille'];
404
	}
405
 
406
 
407
	static function traiterDepartement($departement) {
408
		if(strpos($departement, "INSEE-C:", 0) === 0) return $departement;
409
		if(!is_numeric($departement)) return NULL; // TODO ?
410
		if(strlen($departement) == 4) return "INSEE-C:0" . $departement;
411
		if(strlen($departement) == 5) return "INSEE-C:" . $departement;
412
		// if(strlen($departement) <= 9) return "INSEE-C:0" . $departement; // ? ... TODO
413
		return trim($departement); // TODO
414
	}
415
 
416
 
417
	/* HELPERS */
418
 
1636 raphael 419
	// http://stackoverflow.com/questions/348410/sort-an-array-based-on-another-array
1640 raphael 420
	static function sortArrayByArray($array, $orderArray) {
1636 raphael 421
		$ordered = array();
422
		foreach($orderArray as $key) {
423
			if(array_key_exists($key, $array)) {
424
				$ordered[$key] = $array[$key];
425
				unset($array[$key]);
426
			}
427
		}
428
		return $ordered + $array;
429
	}
1640 raphael 430
 
431
	// retourne une BBox [N,S,E,O) pour un référentiel donné
432
	static function getReferentielBBox($referentiel) {
433
		if($referentiel == 'bdtfx:v1.01') return Array(
434
			'NORD' => 51.2, // Dunkerque
435
			'SUD' => 41.3, // Bonifacio
436
			'EST' => 9.7, // Corse
437
			'OUEST' => -5.2); // Ouessan
438
		return FALSE;
439
	}
440
 
441
 
442
	public function initialiser_colonnes_statiques($id_utisateur) {
443
		$this->colonnes_statiques = array_merge($this->colonnes_statiques,
444
												Array(
445
													"ce_utilisateur" => $id_utisateur,
446
													"prenom_utilisateur" => $this->utilisateur['prenom'],
447
													"nom_utilisateur" => $this->utilisateur['nom'],
448
													"courriel_utilisateur" => $this->utilisateur['courriel'],
449
 
450
													"date_creation" => date("Y-m-d H:i:s"),
451
													"date_modification" => date("Y-m-d H:i:s"),
452
												));
453
 
454
	}
1636 raphael 455
}