Files
Arnaud 5633969d98 Version initiale — carte des établissements scolaires
Carte Leaflet (annuaire Éducation nationale) + API PHP/MySQL :
fiche établissement (effectifs, IPS, population INSEE, DPE ADEME),
GED documents/photos, commentaires par groupe, gestion utilisateurs
et politique de mots de passe. Imports opendata (effectifs, IPS,
population, DPE, IRIS).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:35:41 +02:00

210 lines
8.1 KiB
PHP

<?php
/**
* Bibliothèque partagée DPE (ADEME) : création des tables, import du dataset
* tertiaire et rattachement géographique aux établissements.
*
* Utilisée par les scripts CLI (import_dpe.php, match_dpe.php) et par
* l'endpoint web super-admin (api/import_dpe.php), pour éviter toute
* divergence de logique.
*/
declare(strict_types=1);
require_once __DIR__ . '/db.php';
const DPE_DATASET = 'https://data.ademe.fr/data-fair/api/v1/datasets/dpe01tertiaire/lines';
const DPE_FIELDS = 'numero_dpe,etiquette_dpe,etiquette_ges,conso_kwhep_m2_an,'
. 'emission_ges_kg_co2_m2_an,surface_utile,periode_construction,categorie_erp,'
. 'date_etablissement_dpe,date_fin_validite_dpe,code_insee_ban,code_postal_ban,'
. 'adresse_ban,nom_commune_ban,_geopoint';
/** GET JSON via cURL (suit les redirections). */
function dpe_fetch_json(string $url): array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 120,
CURLOPT_USERAGENT => 'ecoles-import/1.0',
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$body = curl_exec($ch);
if ($body === false) {
$err = curl_error($ch);
curl_close($ch);
throw new RuntimeException('Requête échouée : ' . $err);
}
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($code >= 400) {
throw new RuntimeException("HTTP $code sur l'API ADEME.");
}
$data = json_decode($body, true);
if (!is_array($data)) {
throw new RuntimeException('Réponse JSON ADEME invalide.');
}
return $data;
}
/** Crée la table « dpe » si absente. */
function dpe_create_table(PDO $pdo): void
{
$pdo->exec(
'CREATE TABLE IF NOT EXISTS dpe (
numero_dpe VARCHAR(20) NOT NULL PRIMARY KEY,
etiquette_dpe CHAR(1) NULL,
etiquette_ges CHAR(1) NULL,
conso DECIMAL(10,1) NULL, -- conso_kwhep_m2_an (énergie primaire kWh/m²/an)
ges DECIMAL(10,2) NULL, -- emission_ges_kg_co2_m2_an
surface INT NULL, -- surface_utile (m²)
periode VARCHAR(40) NULL, -- periode_construction
cat_erp VARCHAR(60) NULL, -- categorie_erp
date_dpe DATE NULL, -- date_etablissement_dpe
date_fin DATE NULL, -- date_fin_validite_dpe
code_insee VARCHAR(5) NULL, -- code_insee_ban (clé de rattachement commune)
cp VARCHAR(5) NULL,
adresse VARCHAR(255) NULL,
commune VARCHAR(120) NULL,
lat DECIMAL(9,6) NULL,
lon DECIMAL(9,6) NULL,
INDEX idx_insee (code_insee)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
);
}
/**
* Importe les DPE tertiaires de l'ADEME dans la table « dpe ».
* @param string|null $dep filtre département (code) ou null pour tout
* @param callable $log fn(string) pour la progression
* @return int nombre de DPE importés
*/
function dpe_import(PDO $pdo, ?string $dep, callable $log): int
{
dpe_create_table($pdo);
$url = DPE_DATASET . '?size=10000&sort=numero_dpe&select=' . rawurlencode(DPE_FIELDS);
if ($dep !== null && $dep !== '') {
$url .= '&qs=' . rawurlencode('code_departement_ban:"' . $dep . '"');
$log("Filtre département = $dep.");
}
$ins = $pdo->prepare(
'REPLACE INTO dpe
(numero_dpe, etiquette_dpe, etiquette_ges, conso, ges, surface, periode,
cat_erp, date_dpe, date_fin, code_insee, cp, adresse, commune, lat, lon)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'
);
$letter = static fn($v) => ($v !== null && $v !== '') ? strtoupper(substr((string) $v, 0, 1)) : null;
$date = static fn($v) => (is_string($v) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $v)) ? $v : null;
$numOrNull = static fn($v) => ($v === null || $v === '') ? null : (float) $v;
$page = 0;
$n = 0;
$total = null;
$pdo->beginTransaction();
while ($url) {
$res = dpe_fetch_json($url);
$total ??= $res['total'] ?? null;
$rows = $res['results'] ?? [];
if (!$rows) {
break;
}
foreach ($rows as $r) {
$id = $r['numero_dpe'] ?? '';
if ($id === '') {
continue;
}
$lat = $lon = null;
if (!empty($r['_geopoint']) && str_contains($r['_geopoint'], ',')) {
[$lat, $lon] = array_map('floatval', explode(',', $r['_geopoint'], 2));
}
$ins->execute([
substr($id, 0, 20),
$letter($r['etiquette_dpe'] ?? null),
$letter($r['etiquette_ges'] ?? null),
$numOrNull($r['conso_kwhep_m2_an'] ?? null),
$numOrNull($r['emission_ges_kg_co2_m2_an'] ?? null),
isset($r['surface_utile']) && $r['surface_utile'] !== '' ? (int) round((float) $r['surface_utile']) : null,
$r['periode_construction'] ?? null,
$r['categorie_erp'] ?? null,
$date($r['date_etablissement_dpe'] ?? null),
$date($r['date_fin_validite_dpe'] ?? null),
$r['code_insee_ban'] ?? null,
$r['code_postal_ban'] ?? null,
$r['adresse_ban'] ?? null,
$r['nom_commune_ban'] ?? null,
$lat, $lon,
]);
$n++;
}
$pdo->commit();
$pdo->beginTransaction();
$page++;
$log(sprintf('Page %d — %d DPE importés%s', $page, $n, $total ? " / $total" : ''));
// Page suivante (curseur). data-fair renvoie l'URL complète dans « next ».
$url = $res['next'] ?? null;
if ($url) {
usleep(800000); // ménage le quota anonyme de l'API (20 s de traitement / 60 s)
}
}
$pdo->commit();
return $n;
}
/** Crée la table de rattachement « etab_dpe » si absente. */
function dpe_ensure_link_table(PDO $pdo): void
{
$pdo->exec(
'CREATE TABLE IF NOT EXISTS etab_dpe (
uai VARCHAR(12) NOT NULL,
numero_dpe VARCHAR(20) NOT NULL,
distance_m SMALLINT NOT NULL, -- distance école↔bâtiment (m)
PRIMARY KEY (uai, numero_dpe),
INDEX idx_uai (uai)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
);
}
/**
* Rattache les DPE aux établissements par proximité (même commune, ≤ $radius m).
* @return array{pairs:int, ecoles:int}
*/
function dpe_match(PDO $pdo, int $radius, callable $log): array
{
foreach (['dpe', 'etablissements'] as $t) {
if (!$pdo->query('SHOW TABLES LIKE ' . $pdo->quote($t))->fetchColumn()) {
throw new RuntimeException("Table « $t » absente — lancez d'abord l'import.");
}
}
if (!$pdo->query("SHOW COLUMNS FROM etablissements LIKE 'comc'")->fetchColumn()) {
throw new RuntimeException("Colonne « comc » absente — lancez d'abord import_comc.php.");
}
dpe_ensure_link_table($pdo);
$log("Rattachement (rayon = {$radius} m)…");
$pdo->exec('TRUNCATE TABLE etab_dpe');
// Jointure restreinte à la commune (index idx_insee / idx_comc) puis filtre
// par distance sphérique (ST_Distance_Sphere ⇒ MySQL 5.7.6+ / MariaDB 10.2.4+).
$sql =
'INSERT INTO etab_dpe (uai, numero_dpe, distance_m)
SELECT e.id, d.numero_dpe,
ROUND(ST_Distance_Sphere(POINT(e.lon, e.lat), POINT(d.lon, d.lat)))
FROM etablissements e
JOIN dpe d ON d.code_insee = e.comc
WHERE e.comc IS NOT NULL AND e.lat IS NOT NULL AND e.lon IS NOT NULL
AND d.lat IS NOT NULL AND d.lon IS NOT NULL
AND ST_Distance_Sphere(POINT(e.lon, e.lat), POINT(d.lon, d.lat)) <= ?';
$st = $pdo->prepare($sql);
$st->execute([$radius]);
return [
'pairs' => (int) $pdo->query('SELECT COUNT(*) FROM etab_dpe')->fetchColumn(),
'ecoles' => (int) $pdo->query('SELECT COUNT(DISTINCT uai) FROM etab_dpe')->fetchColumn(),
];
}