mirror of
https://forge.chapril.org/tykayn/wololo
synced 2025-06-20 01:34:42 +02:00
382 lines
12 KiB
TypeScript
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);
|