wololo/conflate_geojson_with_osm.ts

382 lines
12 KiB
TypeScript

/**
* 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 <geojson_file> <distance_max_en_metres> <overpass_request>
*
* 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 <geojson_file> <distance_max_en_metres> <overpass_request>");
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<GeoJSONCollection> {
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<GeoJSONFeature>();
const osmFeaturesMatched = new Set<GeoJSONFeature>();
// 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<void> {
// 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);