Files
JWE/app/scripts/curate-street.mjs
T

203 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Curation du corpus street (« An lari a ») — mode Mapillary.
* « Fetch, pas recall » : pour chaque commune cible, interroge l'API Mapillary
* (bbox autour du centre communal, préférence 360° + qualité + récence),
* geo.api.gouv.fr (centre, superficie), api-adresse (nom de rue) et Wikidata
* (QID de la commune via le sitelink frwiki).
*
* Usage : MAPILLARY_TOKEN=… node scripts/curate-street.mjs
* (le token est aussi lu depuis .env : MAPILLARY_TOKEN ou PUBLIC_MAPILLARY_TOKEN)
* Écrit src/lib/server/data/street.json.
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
function chargeEnv() {
try {
for (const ligne of readFileSync(join(ROOT, '.env'), 'utf8').split('\n')) {
const m = ligne.match(/^\s*([A-Z_]+)\s*=\s*(.+?)\s*$/);
if (m && !process.env[m[1]]) process.env[m[1]] = m[2];
}
} catch {
/* pas de .env */
}
}
chargeEnv();
const TOKEN = process.env.MAPILLARY_TOKEN || process.env.PUBLIC_MAPILLARY_TOKEN;
if (!TOKEN) {
console.error('Token Mapillary manquant (MAPILLARY_TOKEN ou PUBLIC_MAPILLARY_TOKEN).');
process.exit(1);
}
const UA = { 'User-Agent': 'JWE-OKI/2.0 (curation corpus street; contact: o-k-i.net)' };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const getJSON = async (url, headers = UA) => {
const res = await fetch(url, { headers });
if (!res.ok) throw new Error(`HTTP ${res.status} sur ${url}`);
return res.json();
};
// Communes cibles : 5 par région, difficulté = précision exigée (pas de FACILE :
// rayon 50 km = trivial en mode street). titreWiki = titre frwiki exact (désambiguïsé).
const SPOTS = [
// — Guadeloupe (971) —
{ region: 'GUADELOUPE', commune: 'Pointe-à-Pitre', titreWiki: 'Pointe-à-Pitre', difficulte: 'MOYEN', indice1: 'Île papillon, aile droite', indice2: 'Port de commerce principal de larchipel' },
{ region: 'GUADELOUPE', commune: 'Les Abymes', titreWiki: 'Les Abymes', difficulte: 'MOYEN', indice1: 'Île papillon, aile droite', indice2: 'Commune la plus peuplée de larchipel' },
{ region: 'GUADELOUPE', commune: 'Le Gosier', titreWiki: 'Le Gosier', difficulte: 'DIFFICILE', indice1: 'Île papillon, aile droite', indice2: 'Station balnéaire au sud de la Grande-Terre' },
{ region: 'GUADELOUPE', commune: 'Sainte-Anne', titreWiki: 'Sainte-Anne (Guadeloupe)', difficulte: 'DIFFICILE', indice1: 'Île papillon, aile droite', indice2: 'Plages au sud de la Grande-Terre' },
{ region: 'GUADELOUPE', commune: 'Basse-Terre', titreWiki: 'Basse-Terre', difficulte: 'EXPERT', indice1: 'Île papillon, aile gauche', indice2: 'Chef-lieu au pied du volcan' },
// — Martinique (972) —
{ region: 'MARTINIQUE', commune: 'Fort-de-France', titreWiki: 'Fort-de-France', difficulte: 'MOYEN', indice1: 'Île aux fleurs', indice2: 'Chef-lieu, sur une grande baie' },
{ region: 'MARTINIQUE', commune: 'Le Lamentin', titreWiki: 'Le Lamentin', difficulte: 'MOYEN', indice1: 'Île aux fleurs', indice2: 'Banlieue du chef-lieu, zone aéroportuaire' },
{ region: 'MARTINIQUE', commune: 'Saint-Pierre', titreWiki: 'Saint-Pierre (Martinique)', difficulte: 'DIFFICILE', indice1: 'Île aux fleurs, côte caraïbe nord', indice2: 'Ville détruite par une éruption en 1902' },
{ region: 'MARTINIQUE', commune: 'Le François', titreWiki: 'Le François', difficulte: 'DIFFICILE', indice1: 'Île aux fleurs, côte atlantique', indice2: 'Bourg face aux îlets du sud' },
{ region: 'MARTINIQUE', commune: 'Sainte-Marie', titreWiki: 'Sainte-Marie (Martinique)', difficulte: 'EXPERT', indice1: 'Île aux fleurs, côte atlantique nord', indice2: 'Musée de la banane et rhum' },
// — Guyane (973) —
{ region: 'GUYANE', commune: 'Cayenne', titreWiki: 'Cayenne', difficulte: 'MOYEN', indice1: 'Terre de forêt amazonienne', indice2: 'Chef-lieu, sur l’île du même nom' },
{ region: 'GUYANE', commune: 'Kourou', titreWiki: 'Kourou', difficulte: 'DIFFICILE', indice1: 'Terre de forêt amazonienne', indice2: 'Ville des fusées' },
{ region: 'GUYANE', commune: 'Saint-Laurent-du-Maroni', titreWiki: 'Saint-Laurent-du-Maroni', difficulte: 'DIFFICILE', indice1: 'Terre de forêt amazonienne, à louest', indice2: 'Ville du fleuve Maroni, ancien bagne' },
{ region: 'GUYANE', commune: 'Matoury', titreWiki: 'Matoury', difficulte: 'EXPERT', indice1: 'Terre de forêt amazonienne', indice2: 'Banlieue sud du chef-lieu' },
{ region: 'GUYANE', commune: 'Remire-Montjoly', titreWiki: 'Remire-Montjoly', difficulte: 'EXPERT', indice1: 'Terre de forêt amazonienne', indice2: 'Plages à lest du chef-lieu' },
// — La Réunion (974) —
{ region: 'REUNION', commune: 'Saint-Denis', titreWiki: 'Saint-Denis (La Réunion)', difficulte: 'MOYEN', indice1: 'Île intense de locéan Indien', indice2: 'Chef-lieu, au nord' },
{ region: 'REUNION', commune: 'Saint-Pierre', titreWiki: 'Saint-Pierre (La Réunion)', difficulte: 'MOYEN', indice1: 'Île intense de locéan Indien', indice2: 'Capitale du sud, front de mer animé' },
{ region: 'REUNION', commune: 'Le Tampon', titreWiki: 'Le Tampon', difficulte: 'DIFFICILE', indice1: 'Île intense de locéan Indien', indice2: 'Commune des hauts, dans le sud' },
{ region: 'REUNION', commune: 'Saint-Paul', titreWiki: 'Saint-Paul (La Réunion)', difficulte: 'DIFFICILE', indice1: 'Île intense de locéan Indien', indice2: 'Côte ouest, premier site de peuplement' },
{ region: 'REUNION', commune: 'Saint-André', titreWiki: 'Saint-André (La Réunion)', difficulte: 'EXPERT', indice1: 'Île intense de locéan Indien', indice2: 'Côte est, pays de la canne' }
];
const PREFIXE_REGION = { GUADELOUPE: '971', MARTINIQUE: '972', GUYANE: '973', REUNION: '974' };
async function centreCommune(spot) {
const data = await getJSON(
`https://geo.api.gouv.fr/communes?nom=${encodeURIComponent(spot.commune)}&codeDepartement=${PREFIXE_REGION[spot.region]}&fields=centre,surface,code,mairie&limit=8`
);
const c = data[0];
if (!c) throw new Error(`commune introuvable sur geo.api.gouv.fr`);
// La mairie est un meilleur ancrage que le centroïde (les communes DOM
// englobent des reliefs : le centroïde tombe souvent hors du centre-ville).
const p = c.mairie?.coordinates ? c.mairie.coordinates : c.centre.coordinates;
return { lat: p[1], lon: p[0], surfaceHa: c.surface };
}
async function chercherImage(lat, lon) {
const champs = 'id,is_pano,captured_at,computed_geometry,geometry,creator,quality_score';
const note = (img) => (img.quality_score ?? 0.5) * 1e13 + (img.captured_at ?? 0);
// 1) bbox autour de l'ancre (< 0,01° de côté, contrainte API), pano d'abord
const demi = 0.0045;
const bbox = `${lon - demi},${lat - demi},${lon + demi},${lat + demi}`;
for (const pano of ['&is_pano=true', '']) {
const data = await getJSON(
`https://graph.mapillary.com/images?access_token=${encodeURIComponent(TOKEN)}&fields=${champs}&bbox=${bbox}&limit=200${pano}`
);
if (data.data?.length) {
const triees = [...data.data].sort((a, b) => note(b) - note(a));
return { image: triees[0], total: data.data.length };
}
}
// 2) grille de recherches par rayon (50 m) autour de l'ancre
const parId = new Map();
for (const dLat of [-0.004, 0, 0.004]) {
for (const dLon of [-0.004, 0, 0.004]) {
try {
const data = await getJSON(
`https://graph.mapillary.com/images?access_token=${encodeURIComponent(TOKEN)}&fields=${champs}&lat=${lat + dLat}&lng=${lon + dLon}&radius=50&limit=10`
);
for (const img of data.data ?? []) parId.set(img.id, img);
} catch {
/* point sans couverture */
}
await sleep(120);
}
}
if (!parId.size) return null;
const triees = [...parId.values()].sort((a, b) => note(b) - note(a));
return { image: triees[0], total: parId.size };
}
async function nomDeRue(lat, lon) {
try {
const data = await getJSON(`https://api-adresse.data.gouv.fr/reverse/?lon=${lon}&lat=${lat}&type=street`);
const f = data.features?.[0]?.properties;
return f?.name ?? f?.street ?? null;
} catch {
return null;
}
}
// QIDs des communes via sitelinks frwiki (un seul appel batch)
async function qidsCommunes() {
const titres = SPOTS.map((s) => s.titreWiki);
const data = await getJSON(
`https://www.wikidata.org/w/api.php?action=wbgetentities&sites=frwiki&titles=${encodeURIComponent(titres.join('|'))}&props=sitelinks&sitefilter=frwiki&format=json&redirects=yes`
);
const parTitre = {};
for (const [qid, e] of Object.entries(data.entities ?? {})) {
if (e.sitelinks?.frwiki?.title) parTitre[e.sitelinks.frwiki.title] = qid;
}
const redirige = {};
for (const r of data.redirects ?? []) redirige[r.from] = r.to;
const res = {};
for (const s of SPOTS) {
const t = redirige[s.titreWiki] ?? s.titreWiki;
res[s.titreWiki] = parTitre[t] ?? null;
}
return res;
}
const qids = await qidsCommunes();
const lieux = [];
let n = 0;
for (const spot of SPOTS) {
n++;
try {
const centre = await centreCommune(spot);
await sleep(150);
const trouve = await chercherImage(centre.lat, centre.lon);
if (!trouve) {
console.log(`VIDE ${spot.commune} (${spot.region}) — aucune image dans la bbox`);
continue;
}
const img = trouve.image;
const geo = img.computed_geometry ?? img.geometry;
const coords = { lat: geo.coordinates[1], lon: geo.coordinates[0] };
await sleep(150);
const rue = await nomDeRue(coords.lat, coords.lon);
const qid = qids[spot.titreWiki];
lieux.push({
id: `st-${String(n).padStart(2, '0')}`,
nom: rue ? `${rue}, ${spot.commune}` : `Centre de ${spot.commune}`,
nom_creole: `Lari ${spot.commune}`,
region: spot.region,
commune: spot.commune,
coordonnees: coords,
difficulte: spot.difficulte,
mapillary_id: img.id,
credit: {
username: img.creator?.username ?? 'contributeur Mapillary',
captured_at: new Date(img.captured_at).toISOString().slice(0, 10)
},
is_pano: !!img.is_pano,
wikidata_id: qid,
indices: [
{ niveau: 1, texte: spot.indice1 },
{ niveau: 2, texte: spot.indice2 }
],
aire_commune_km2: Math.round(centre.surfaceHa) / 100 // geo.api : hectares → km²
});
console.log(
`OK ${spot.commune.padEnd(24)} img ${img.id} pano=${img.is_pano ? 'oui' : 'NON'} q=${img.quality_score?.toFixed(2) ?? '—'} pool=${trouve.total} qid=${qid ?? 'ABSENT'} rue=${rue ?? '—'}`
);
} catch (e) {
console.log(`ERREUR ${spot.commune}${e.message}`);
}
await sleep(150);
}
writeFileSync(join(ROOT, 'src/lib/server/data/street.json'), JSON.stringify(lieux, null, 2) + '\n');
console.log(`\n${lieux.length} lieux écrits dans src/lib/server/data/street.json`);