/** * conflate_geojson_with_osm.ts * * Réalise une conflation entre un fichier GeoJSON local et des données OSM via Overpass * en fusionnant les points proches (distance configurable). * * Usage: * npx ts-node conflate_geojson_with_osm.ts * * Exemple: * npx ts-node conflate_geojson_with_osm.ts data/bornes.geojson 100 "area[name='Limours']->.a;nwr[amenity=charging_station](area.a);" */ import * as fs from 'fs'; import axios from 'axios'; import * as turf from '@turf/turf'; import * as path from 'path'; interface GeoJSONGeometry { type: string; coordinates: number[] | number[][] | number[][][] | number[][][][]; } interface GeoJSONProperties { [key: string]: any; } interface GeoJSONFeature { type: string; id?: string | number; geometry: GeoJSONGeometry; properties: GeoJSONProperties; } interface GeoJSONCollection { type: string; features: GeoJSONFeature[]; } interface OverpassElement { type: string; id: number; lat?: number; lon?: number; tags?: { [key: string]: string; }; nodes?: number[]; geometry?: { lat: number; lon: number; }[]; } interface OverpassResponse { version: number; generator: string; osm3s: { timestamp_osm_base: string; copyright: string; }; elements: OverpassElement[]; } interface MatchedFeaturePair { localFeature: GeoJSONFeature; osmFeature: GeoJSONFeature; distance: number; } /** * Vérifie les arguments et retourne les valeurs */ function checkArguments(): { filePath: string, distance: number, overpassQuery: string } { if (process.argv.length < 5) { console.error("Usage: npx ts-node conflate_geojson_with_osm.ts "); console.error("Exemple: npx ts-node conflate_geojson_with_osm.ts data/bornes.geojson 100 \"area[name='Limours']->.a;nwr[amenity=charging_station](area.a);\""); process.exit(1); } const filePath = process.argv[2]; const distance = parseFloat(process.argv[3]); const overpassQuery = process.argv[4]; if (isNaN(distance)) { console.error("La distance doit être un nombre (en mètres)"); process.exit(1); } return { filePath, distance, overpassQuery }; } /** * Charge les données GeoJSON à partir d'un fichier */ function loadLocalGeoJSON(filePath: string): GeoJSONCollection { try { const fileContent = fs.readFileSync(filePath, 'utf8'); const data = JSON.parse(fileContent); if (data.type !== 'FeatureCollection' || !Array.isArray(data.features)) { console.error("Le fichier ne semble pas être un GeoJSON valide avec des features"); process.exit(1); } return data; } catch (error) { console.error(`Erreur lors du chargement du fichier: ${error}`); process.exit(1); throw error; // Pour TypeScript } } /** * Récupère les données depuis Overpass API */ async function fetchOverpassData(query: string): Promise { try { console.log("Récupération des données depuis Overpass API..."); console.log(`Requête: ${query}`); const encodedQuery = encodeURIComponent(query); const url = `https://overpass-api.de/api/interpreter?data=[out:json][timeout:25];${encodedQuery}out body geom;`; const response = await axios.get(url); const overpassData: OverpassResponse = response.data; console.log(`${overpassData.elements.length} éléments récupérés depuis Overpass`); // Convertir les données Overpass en GeoJSON return convertOverpassToGeoJSON(overpassData); } catch (error) { console.error("Erreur lors de la récupération des données Overpass:", error); process.exit(1); throw error; // Pour TypeScript } } /** * Convertit les données Overpass en GeoJSON */ function convertOverpassToGeoJSON(overpassData: OverpassResponse): GeoJSONCollection { const features: GeoJSONFeature[] = []; overpassData.elements.forEach(element => { let geometry: GeoJSONGeometry; let properties: GeoJSONProperties = { ...element.tags, id_osm: element.id, type_osm: element.type }; // Nœud (point) if (element.type === 'node' && element.lat !== undefined && element.lon !== undefined) { geometry = { type: 'Point', coordinates: [element.lon, element.lat] }; } // Chemin (ligne ou polygone) else if (element.type === 'way' && element.geometry) { const coordinates = element.geometry.map(point => [point.lon, point.lat]); // Si le premier et le dernier point sont identiques, c'est un polygone if (element.nodes && element.nodes[0] === element.nodes[element.nodes.length - 1]) { geometry = { type: 'Polygon', coordinates: [coordinates] }; } else { geometry = { type: 'LineString', coordinates: coordinates }; } } // Relation (multipolygon, etc.) else if (element.type === 'relation') { // Pour les relations, on crée un point à partir du centre de la géométrie si disponible // Une gestion complète des relations nécessiterait plus de logique if (element.lat !== undefined && element.lon !== undefined) { geometry = { type: 'Point', coordinates: [element.lon, element.lat] }; } else { return; // Skip this element } } else { return; // Skip this element } features.push({ type: 'Feature', id: element.id, geometry, properties }); }); return { type: 'FeatureCollection', features }; } /** * Trouve les paires de points correspondants entre local et OSM */ function findMatchingFeatures(localGeoJSON: GeoJSONCollection, osmGeoJSON: GeoJSONCollection, maxDistance: number): MatchedFeaturePair[] { const matches: MatchedFeaturePair[] = []; // Pour chaque feature locale, trouver la feature OSM la plus proche localGeoJSON.features.forEach(localFeature => { if (localFeature.geometry.type !== 'Point') return; let closestOsmFeature: GeoJSONFeature | null = null; let minDistance = Infinity; osmGeoJSON.features.forEach(osmFeature => { // Si ce n'est pas un point, on passe if (osmFeature.geometry.type !== 'Point') return; // Calculer la distance const from = turf.point(localFeature.geometry.coordinates as [number, number]); const to = turf.point(osmFeature.geometry.coordinates as [number, number]); const distance = turf.distance(from, to, { units: 'meters' }); // Mettre à jour si c'est la distance minimale trouvée if (distance < minDistance) { minDistance = distance; closestOsmFeature = osmFeature; } }); // Si on a trouvé une feature proche, l'ajouter aux correspondances if (closestOsmFeature && minDistance <= maxDistance) { matches.push({ localFeature, osmFeature: closestOsmFeature, distance: minDistance }); } }); return matches; } /** * Fusionne les propriétés de deux features */ function mergeProperties(localProps: GeoJSONProperties, osmProps: GeoJSONProperties): GeoJSONProperties { const result: GeoJSONProperties = { ...localProps }; // Ajouter les propriétés OSM qui n'existent pas dans les données locales // ou dont la valeur est vide Object.entries(osmProps).forEach(([key, value]) => { // Si la propriété n'existe pas ou est vide localement, on prend celle d'OSM if (result[key] === undefined || result[key] === '' || result[key] === null) { result[key] = value; } // Si les deux ont une valeur, on peut préférer celle d'OSM pour certaines clés else if (['name', 'operator', 'opening_hours', 'website', 'phone', 'brand'].includes(key)) { // On garde l'info qu'il y a une différence result[`local_${key}`] = result[key]; result[key] = value; } }); // Marquer comme ayant été fusionné result.conflated = true; return result; } /** * Génère le GeoJSON final après conflation */ function generateConflatedGeoJSON( localGeoJSON: GeoJSONCollection, osmGeoJSON: GeoJSONCollection, matches: MatchedFeaturePair[] ): GeoJSONCollection { const conflatedFeatures: GeoJSONFeature[] = []; const localFeaturesMatched = new Set(); const osmFeaturesMatched = new Set(); // 1. Ajouter les features fusionnées matches.forEach(match => { const mergedProps = mergeProperties(match.localFeature.properties, match.osmFeature.properties); mergedProps.conflated_distance = match.distance; // On utilise la géométrie d'OSM (plus précise) const conflatedFeature: GeoJSONFeature = { type: 'Feature', geometry: match.osmFeature.geometry, properties: mergedProps }; conflatedFeatures.push(conflatedFeature); localFeaturesMatched.add(match.localFeature); osmFeaturesMatched.add(match.osmFeature); }); // 2. Ajouter les features locales non matchées localGeoJSON.features .filter(feature => !localFeaturesMatched.has(feature)) .forEach(feature => { const props = { ...feature.properties, source: 'local_only' }; conflatedFeatures.push({ type: 'Feature', geometry: feature.geometry, properties: props }); }); // 3. Ajouter les features OSM non matchées osmGeoJSON.features .filter(feature => !osmFeaturesMatched.has(feature)) .forEach(feature => { const props = { ...feature.properties, source: 'osm_only' }; conflatedFeatures.push({ type: 'Feature', geometry: feature.geometry, properties: props }); }); return { type: 'FeatureCollection', features: conflatedFeatures }; } /** * Écrit le GeoJSON dans un fichier */ function writeGeoJSONToFile(geojson: GeoJSONCollection, filename: string): void { fs.writeFileSync(filename, JSON.stringify(geojson, null, 2)); console.log(`Fichier écrit: ${filename}`); } /** * Fonction principale */ async function main(): Promise { // Récupérer les arguments const { filePath, distance, overpassQuery } = checkArguments(); // Charger les données locales console.log(`Chargement des données depuis ${filePath}...`); const localGeoJSON = loadLocalGeoJSON(filePath); console.log(`${localGeoJSON.features.length} features chargées depuis le fichier local`); // Récupérer les données OSM const osmGeoJSON = await fetchOverpassData(overpassQuery); // Trouver les correspondances console.log(`Recherche des correspondances (distance max: ${distance}m)...`); const matches = findMatchingFeatures(localGeoJSON, osmGeoJSON, distance); console.log(`${matches.length} correspondances trouvées`); // Générer le GeoJSON conflated console.log("Génération du GeoJSON après conflation..."); const conflatedGeoJSON = generateConflatedGeoJSON(localGeoJSON, osmGeoJSON, matches); // Statistiques const localOnly = conflatedGeoJSON.features.filter(f => f.properties.source === 'local_only').length; const osmOnly = conflatedGeoJSON.features.filter(f => f.properties.source === 'osm_only').length; const conflated = conflatedGeoJSON.features.filter(f => f.properties.conflated).length; console.log("\n=== Résultats de la conflation ==="); console.log(`Total des features: ${conflatedGeoJSON.features.length}`); console.log(`- Features fusionnées: ${conflated}`); console.log(`- Features locales uniquement: ${localOnly}`); console.log(`- Features OSM uniquement: ${osmOnly}`); // Écrire les résultats dans des fichiers const basename = path.basename(filePath, path.extname(filePath)); writeGeoJSONToFile(localGeoJSON, `${basename}_local.geojson`); writeGeoJSONToFile(osmGeoJSON, `${basename}_osm.geojson`); writeGeoJSONToFile(conflatedGeoJSON, `${basename}_conflated.geojson`); } // Exécuter le programme main().catch(console.error);