24,12 → 24,35 |
require_once('lib/PHPExcel/Classes/PHPExcel.php'); |
require_once('ExportXLS.php'); |
|
|
date_default_timezone_set("Europe/Paris"); |
|
// nombre d'INSERT à cumuler par requête SQL |
// (= nombre de lignes XLS à bufferiser) |
define('NB_LIRE_LIGNE_SIMUL', 30); |
|
// Numbers of days between January 1, 1900 and 1970 (including 19 leap years) |
// see traiterDateObs() |
define("MIN_DATES_DIFF", 25569); |
|
|
class MyReadFilter implements PHPExcel_Reader_IReadFilter { |
// exclusion de colonnes |
public $exclues = array(); |
|
// lecture par morceaux |
public $ligne_debut = 0; |
public $ligne_fin = 0; |
|
public function __construct() {} |
public function def_interval($debut, $nb) { |
$this->ligne_debut = $debut; |
$this->ligne_fin = $debut + $nb; |
} |
public function readCell($colonne, $ligne, $worksheetName = '') { |
if(@$this->exclues[$colonne]) return false; |
if($ligne == 1) return false; |
// si des n° de morceaux ont été initialisés, on filtre... |
if($this->ligne_debut && ($ligne < $this->ligne_debut || $ligne >= $this->ligne_fin)) return false; |
return true; |
} |
} |
62,6 → 85,25 |
"latitude", |
"longitude"); |
|
/* |
Ces colonnes: |
- sont propre à tous les enregistrements uploadés |
- sont indépendantes du numéro de lignes |
- n'ont pas de valeur par défaut dans la structure de la table |
- nécessitent une initialisation dans le cadre de l'upload |
*/ |
public $colonnes_statiques = Array( |
"ce_utilisateur" => NULL, |
"prenom_utilisateur" => NULL, |
"nom_utilisateur" => NULL, |
"courriel_utilisateur" => NULL, |
|
// XXX: fixes (mais pourraient varier dans le futur si la mise-à-jour |
// d'observation est implémentée |
"date_creation" => NULL, // ne peut initialiser d'une date ici |
"date_modification" => NULL, // idem, cf initialiser_colonnes_statiques() |
); |
|
function ExportXLS($config) { |
parent::__construct($config); |
} |
70,11 → 112,15 |
if(!isset($pairs['utilisateur']) || trim($pairs['utilisateur']) == '') { |
echo '0'; exit; |
} |
$id_utilisateur = intval($pairs['utilisateur']); |
|
if(!isset($_SESSION)) session_start(); |
$this->controleUtilisateur($pairs['utilisateur']); |
$this->controleUtilisateur($id_utilisateur); |
|
$this->utilisateur = $this->getInfosComplementairesUtilisateur($pairs['utilisateur']); |
$this->utilisateur = $this->getInfosComplementairesUtilisateur($id_utilisateur); |
$this->initialiser_colonnes_statiques($id_utilisateur); |
|
|
$infos_fichier = array_pop($_FILES); |
|
/*$objPHPExcel = PHPExcel_IOFactory::load($infos_fichier['tmp_name']); |
88,19 → 134,47 |
|
$objReader = PHPExcel_IOFactory::createReader("Excel5"); |
$objReader->setReadDataOnly(true); |
|
// on ne conserve que l'en-tête |
$filtre = new MyReadFilter(); |
$filtre->def_interval(1, 2); |
$objReader->setReadFilter($filtre); |
|
$objPHPExcel = $objReader->load($infos_fichier['tmp_name']); |
$obj_infos = $objReader->listWorksheetInfo($infos_fichier['tmp_name']); |
// XXX: indépendant du readFilter ? |
$nb_lignes = $obj_infos[0]['totalRows']; |
|
$donnees = $objPHPExcel->getActiveSheet()->toArray(NULL,FALSE,FALSE,TRUE); |
$filtre->exclues = self::detectionEntete($donnees[1]); |
|
$filtre_colonnes = new MyReadFilter(); |
$filtre_colonnes->exclues = self::detectionEntete($donnees[1]); |
$objReader->setReadFilter($filtre_colonnes); |
$obs_ajouts = 0; |
$obs_maj = 0; |
$dernier_ordre = $this->requeter("SELECT MAX(ordre) AS ordre FROM cel_obs WHERE ce_utilisateur = $id_utilisateur"); |
$dernier_ordre = intval($dernier_ordre[0]['ordre']) + 1; |
if(! $dernier_ordre) $dernier_ordre = 0; |
|
// recharge avec le filtre sur colonnes actif |
// (exclue les colonnes inutiles/inutilisables) |
// lecture par morceaux (chunks), NB_LIRE_LIGNE_SIMUL lignes à fois |
// pour aboutir des requêtes SQL d'insert groupés. |
for($ligne = 2; $ligne < $nb_lignes + NB_LIRE_LIGNE_SIMUL; $ligne += NB_LIRE_LIGNE_SIMUL) { |
$filtre->def_interval($ligne, NB_LIRE_LIGNE_SIMUL); |
$objReader->setReadFilter($filtre); |
|
/* recharge avec $filtre actif (filtre sur lignes colonnes): |
- exclue les colonnes inutiles/inutilisables) |
- ne selectionne que les lignes dans le range [$ligne - $ligne + NB_LIRE_LIGNE_SIMUL] */ |
$objPHPExcel = $objReader->load($infos_fichier['tmp_name']); |
$donnees = $objPHPExcel->getActiveSheet()->toArray(NULL,FALSE,FALSE,TRUE); |
self::charger($donnees); |
|
// ici on appel la fonction qui fera effectivement l'insertion multiple |
// à partir des (au plus) NB_LIRE_LIGNE_SIMUL lignes |
|
// TODO: passer $this, ne sert que pour appeler des méthodes public qui pourraient être statiques |
// notamment dans RechercheInfosTaxonBeta.php |
self::chargerLignes($donnees, $this->colonnes_statiques, $dernier_ordre, $this, $obs_ajouts, $obs_maj); |
} |
die('end'); |
} |
|
static function detectionEntete($entete) { |
$colonnes_reconnues = Array(); |
111,6 → 185,7 |
$entete_officiel_simple = iconv('UTF-8', 'ASCII//TRANSLIT', strtolower(trim($col['nom']))); |
$entete_officiel_abbrev = $col['abbrev']; |
if($entete_simple == $entete_officiel_simple || $entete_simple == $entete_officiel_abbrev) { |
// debug echo "define C_" . strtoupper($entete_officiel_abbrev) . ", $k\n"; |
define("C_" . strtoupper($entete_officiel_abbrev), $k); |
$colonnes_reconnues[$k] = 1; |
break; |
118,84 → 193,229 |
} |
} |
|
// eg: 'I' => "rien" |
// prépare le filtre de PHPExcel qui évitera le traitement de toutes les colonnes superflues |
|
// eg: diff ( Array( H => Commune, I => rien ) , Array( H => 1, K => 1 ) |
// ==> Array( I => rien ) |
$colonnesID_non_reconnues = array_diff_key($entete, $colonnes_reconnues); |
|
// des colonnes de ExportXLS::nom_d_ensemble_vers_liste_de_colonnes() |
// ne retient que celles marquées "importables" |
$colonnes_automatiques = array_filter($cols, function($v) { return !$v['importable']; }); |
|
// ne conserve que le nom long pour matcher avec la ligne XLS d'entête |
array_walk($colonnes_automatiques, function(&$v) { $v = $v['nom']; }); |
|
// intersect ( Array ( N => Milieu, S => Ordre ), Array ( ordre => Ordre, phenologie => Phénologie ) ) |
// ==> Array ( S => Ordre, AA => Phénologie ) |
$colonnesID_a_exclure = array_intersect($entete, $colonnes_automatiques); |
// TODO: pourquoi ne pas comparer avec les abbrevs aussi |
|
// TODO: pourquoi ne pas comparer avec les abbrevs aussi ? |
// merge ( Array( I => rien ) , Array ( S => Ordre, AA => Phénologie ) ) |
// ==> Array ( I => rien, AA => Phénologie ) |
return array_merge($colonnesID_non_reconnues, $colonnesID_a_exclure); |
} |
|
static function charger($donnees) { |
// TODO: chunkfilter |
$morceaux = 30; |
$i = 2; |
$fin = $i + $morceaux; |
while($i < $fin && isset($donnees[$i])) { |
$enregistrement = self::chargerLigne($donnees[$i]); |
$req = implode(', ', self::sortArrayByArray($enregistrement, self::$ordre_BDD)); |
$i++; |
/* |
* charge un groupe de lignes |
*/ |
static function chargerLignes($lignes, $colonnes_statiques, &$dernier_ordre, $cel, &$inserted, &$updated) { |
$enregistrement = NULL; |
$enregistrements = Array(); |
$images = Array(); |
|
foreach($lignes as $ligne) { |
$ligne = array_filter($ligne, function($cell) { return !is_null($cell); }); |
if(!$ligne) continue; |
|
if( ($enregistrement = self::chargerLigne($ligne, $colonnes_statiques, $dernier_ordre, $cel)) ) { |
$enregistrements[] = $enregistrement; |
|
if(isset($enregistrement['_images'])) { |
$pos = count($enregistrements) - 1; |
// ne dépend pas de cel_obs, et seront insérées *après* les enregistrements |
// mais nous ne voulons pas nous priver de faire des INSERT multiples pour autant |
$images[] = Array("text" => $enregistrements[$pos]['_images'], |
"obs_pos" => $pos); |
// ce champ n'a pas a faire partie de l'insertion dans cel_obs, |
// mais est utile pour cel_obs_images |
unset($enregistrements[$pos]['_images']); |
} |
|
$dernier_ordre++; |
} |
} |
|
// TODO: bouger les valeurs fixes comme valeurs de self::$ordre_BDD |
// et compter sur un array_merge() |
if(!$enregistrements) die('AIE // XXX'); |
|
// TODO, théoriquement, l'ordre des index peut-être changé si nécessaire car |
// nous utilisons self::sortArrayByArray() |
static function chargerLigne($ligne) { |
$req = ''; |
|
|
foreach($enregistrements as $enregistrement) |
$req .= implode(', ', self::sortArrayByArray($enregistrement, self::$ordre_BDD)); |
print_r($req); |
|
// $cel->executer($req); |
// transactionnel + auto-inc |
$lastid = $cel->bdd->lastInsertId(); |
foreach($images as $image) { |
$obs = $enregistrements[$image["obs_pos"]]; |
$id_obs = $lastid // dernier autoinc inséré |
- count($enregistrements) - 1 // correspondrait au premier autoinc |
+ $image["obs_pos"]; // ordre d'insertion = ordre dans le tableau $enregistrements |
// TODO: INSERT |
} |
} |
|
|
static function chargerLigne($ligne, $colonnes_statiques, $dernier_ordre, $cel) { |
// en premier car le résultat est utile pour |
// traiter longitude et latitude (traiterLonLat()) |
$referentiel = self::identReferentiel($ligne[C_REFERENTIEL]); |
$referentiel = self::identReferentiel($ligne[C_NOM_REFERENTIEL]); |
|
return Array( |
"ce_utilisateur" => XXX, |
"prenom_utilisateur" => XXX, |
"nom_utilisateur" => XXX, |
"courriel_utilisateur" => XXX, |
"ordre" => YYY, |
"nom_sel" => TODO, |
"nom_sel_nn" => TODO, |
"nom_ret" => TODO, |
"nom_ret_nn" => TODO, |
"nt" => TODO, |
"famille" => TODO, |
// $espece est rempli de plusieurs informations |
$espece = Array(); |
self::traiterEspece($ligne, $espece, $cel); |
|
return array_merge($colonnes_statiques, |
// Dans ce tableau, seules devraient apparaître les données variable pour chaque ligne. |
// Dans ce tableau, l'ordre des clefs n'importe pas (cf: self::sortArrayByArray()) |
Array( |
"ordre" => $dernier_ordre, |
|
"nom_sel" => $espece[C_NOM_SEL], |
"nom_sel_nn" => $espece[C_NOM_SEL_NN], |
"nom_ret" => $espece[C_NOM_RET], |
"nom_ret_nn" => $espece[C_NOM_RET_NN], |
"nt" => $espece[C_NT], |
"famille" => $espece[C_FAMILLE], |
|
"nom_referentiel" => $referentiel, |
|
"zone_geo" => TODO, |
"ce_zone_geo" => TODO, |
"ce_zone_geo" => self::traiterDepartement(trim($ligne[C_CE_ZONE_GEO])), |
|
"date_observation" => self::traiterDateObs($ligne[C_DATE_OBSERVATION]), |
"lieudit" => trim($ligne[C_LIEUDIT]), |
"station" => trim($ligne[C_STATION]), |
"milieu" => trim($ligne[C_MILIEU]), |
"commentaire" => trim($ligne[C_COMMENTAIRE]), |
"commentaire" => trim($ligne[C_COMMENTAIRE]), // TODO: foreign-key |
|
// XXX: fixe |
"transmission" => 0, // cf commentaire dans ExportXLS::nom_d_ensemble_vers_liste_de_colonnes() |
"date_creation" => date("Y-m-d H:i:s"), |
"date_modification" => date("Y-m-d H:i:s"), |
"transmission" => in_array(strtolower(trim($ligne[C_TRANSMISSION])), array(1, 'oui')) ? 1 : 0, |
|
"latitude" => self::traiterLonLat(NULL, $ligne[C_LATITUDE], $referentiel), |
"longitude" => self::traiterLonLat($ligne[C_LONGITUDE], NULL, $referentiel) |
); |
"longitude" => self::traiterLonLat($ligne[C_LONGITUDE], NULL, $referentiel), |
)); |
} |
|
|
|
/* FONCTIONS de TRANSFORMATION de VALEUR DE CELLULE */ |
|
// TODO: PHP 5.3, utiliser date_parse_from_format() |
// TODO: parser les heures (cf product-owner) |
// TODO: passer par le timestamp pour s'assurer de la validité |
static function traiterDateObs($date) { |
return NULL; // TODO |
// TODO: see https://github.com/PHPOffice/PHPExcel/issues/208 |
if(is_double($date)) { |
if($date > 0) |
return PHPExcel_Style_NumberFormat::toFormattedString($date, PHPExcel_Style_NumberFormat::FORMAT_DATE_YYYYMMDD2) . " 00:00:00"; |
throw new Exception("erreur: date antérieure à 1970 et format de cellule \"DATE\" utilisés ensemble"); |
|
// attention, UNIX timestamp, car Excel les décompte depuis 1900 |
// cf http://fczaja.blogspot.fr/2011/06/convert-excel-date-into-timestamp.html |
// $timestamp = ($date - MIN_DATES_DIFF) * 60 * 60 * 24 - time(); // NON |
|
// $timestamp = PHPExcel_Calculation::getInstance()->calculateFormula("=" . $date . "-DATE(1970,1,1)*60*60*24"); // NON |
|
// echo strftime("%Y/%m/%d 00:00:00", $timestamp); // NON |
} |
else { |
$timestamp = strtotime($date); |
if(!$timestamp) return NULL; // TODO: throw error |
return strftime("%Y-%m-%d 00:00:00", strtotime($date)); |
} |
} |
|
static function identReferentiel($referentiel) { |
return NULL; // TODO |
// SELECT DISTINCT nom_referentiel, COUNT(id_observation) AS count FROM cel_obs GROUP BY nom_referentiel ORDER BY count DESC; |
if(strpos(strtolower($referentiel), 'bdtfx') !== FALSE) return 'bdtfx:v1.01'; |
if(strpos(strtolower($referentiel), 'bdtxa') !== FALSE) return 'bdtxa:v1.00'; |
if(strpos(strtolower($referentiel), 'bdnff') !== FALSE) return 'bdnff:4.02'; |
if(strpos(strtolower($referentiel), 'isfan') !== FALSE) return 'isfan:v1.00'; |
return NULL; |
/* TODO: cf story, |
En cas de NULL faire une seconde passe de détection à partir du nom saisie */ |
} |
|
/* NON! |
Un taxon d'un référentiel donné peut être théoriquement observé n'importe où sur le globe. |
Il n'y a pas lieu d'effectuer des restriction ici. |
Cependant des erreurs fréquentes (0,0 ou lon/lat inversées) peuvent être détectés ici. |
TODO */ |
static function traiterLonLat($lon = NULL, $lat = NULL, $referentiel = 'bdtfx:v1.01') { |
// verifier format decimal + |
// + limite france si bdtfx ou bdtxa |
|
$bbox = self::getReferentielBBox($referentiel); |
if(!$bbox) return NULL; |
|
if($lon) { |
if($lon < $bbox['EST'] && $lon > $bbox['OUEST']) return is_numeric($lon) ? $lon : NULL; |
else return NULL; |
} |
if($lat) { |
if($lat < $bbox['NORD'] && $lat > $bbox['SUD']) return is_numeric($lat) ? $lat : NULL; |
return NULL; |
} |
} |
|
|
static function traiterEspece($ligne, Array &$espece, $cel) { |
$taxon_info_webservice = new RechercheInfosTaxonBeta($cel->config); |
|
$ascii = iconv('UTF-8', 'ASCII//TRANSLIT', $ligne[C_NOM_SEL]); |
$resultat_recherche_espece = $taxon_info_webservice->rechercherInfosSurTexteCodeOuNumTax($ligne[C_NOM_SEL]); |
|
// on supprime les noms retenus et renvoi tel quel |
// on réutilise les define pour les noms d'indexes, tant qu'à faire |
if (empty($resultat_recherche_espece['en_id_nom'])) { |
$espece[C_NOM_SEL] = $ligne[C_NOM_SEL]; |
$espece[C_NOM_SEL_NN] = $ligne[C_NOM_SEL_NN]; |
|
// TODO: si empty(C_NOM_SEL) et !empty(C_NOM_SEL_NN) : recherche info à partir de C_NOM_SEL_NN |
$espece[C_NOM_RET] = $ligne[C_NOM_RET]; |
$espece[C_NOM_RET_NN] = $ligne[C_NOM_RET_NN]; |
$espece[C_NT] = $ligne[C_NT]; |
$espece[C_FAMILLE] = $ligne[C_FAMILLE]; |
|
return; |
} |
|
// succès de la détection, récupération des infos |
$espece[C_NOM_SEL] = $resultat_recherche_espece['nom_sel']; |
$espece[C_NOM_SEL_NN] = $resultat_recherche_espece['en_id_nom']; |
|
$complement = $taxon_info_webservice->rechercherInformationsComplementairesSurNumNom($resultat_recherche_espece['en_id_nom']); |
$espece[C_NOM_RET] = $complement['Nom_Retenu']; |
$espece[C_NOM_RET_NN] = $complement['Num_Nom_Retenu']; |
$espece[C_NT] = $complement['Num_Taxon']; |
$espece[C_FAMILLE] = $complement['Famille']; |
} |
|
|
static function traiterDepartement($departement) { |
if(strpos($departement, "INSEE-C:", 0) === 0) return $departement; |
if(!is_numeric($departement)) return NULL; // TODO ? |
if(strlen($departement) == 4) return "INSEE-C:0" . $departement; |
if(strlen($departement) == 5) return "INSEE-C:" . $departement; |
// if(strlen($departement) <= 9) return "INSEE-C:0" . $departement; // ? ... TODO |
return trim($departement); // TODO |
} |
|
|
/* HELPERS */ |
|
// http://stackoverflow.com/questions/348410/sort-an-array-based-on-another-array |
static function sortArrayByArray($array,$orderArray) { |
$ordered = array(); |
207,4 → 427,29 |
} |
return $ordered + $array; |
} |
|
// retourne une BBox [N,S,E,O) pour un référentiel donné |
static function getReferentielBBox($referentiel) { |
if($referentiel == 'bdtfx:v1.01') return Array( |
'NORD' => 51.2, // Dunkerque |
'SUD' => 41.3, // Bonifacio |
'EST' => 9.7, // Corse |
'OUEST' => -5.2); // Ouessan |
return FALSE; |
} |
|
|
public function initialiser_colonnes_statiques($id_utisateur) { |
$this->colonnes_statiques = array_merge($this->colonnes_statiques, |
Array( |
"ce_utilisateur" => $id_utisateur, |
"prenom_utilisateur" => $this->utilisateur['prenom'], |
"nom_utilisateur" => $this->utilisateur['nom'], |
"courriel_utilisateur" => $this->utilisateur['courriel'], |
|
"date_creation" => date("Y-m-d H:i:s"), |
"date_modification" => date("Y-m-d H:i:s"), |
)); |
|
} |
} |