add config maxspeed and Echirolles, up script get datasets, start conflation of geojsons

This commit is contained in:
Tykayn 2025-03-31 13:47:28 +02:00 committed by tykayn
parent 9b5baab032
commit aa35803a0b
5 changed files with 518 additions and 82 deletions

View file

@ -0,0 +1,382 @@
/**
* 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);

View file

@ -22,6 +22,9 @@ interface Options {
hasHeaders: boolean;
}
let counter = 0;
let counter_features = 0;
let counter_missing_lat = 0;
let counter_missing_lon = 0;
function csvToGeoJSON(options: Options): FeatureCollection<Point> {
const { dir, file, latColumn, lonColumn, hasHeaders } = options;
@ -38,13 +41,12 @@ function csvToGeoJSON(options: Options): FeatureCollection<Point> {
let headers: string[] = [];
let headersFound = false;
let limitOffset = 100;
let limitOffset = 30000000;
console.log(`hasHeaders: ${hasHeaders}`);
fs.createReadStream(filePath)
.pipe(csvParser({ headers: hasHeaders }))
.on('data', (row) => {
counter++;
counter += 1;
if (!headersFound && hasHeaders) {
let keys = Object.keys(row);
keys.forEach((key) => {
@ -56,11 +58,7 @@ function csvToGeoJSON(options: Options): FeatureCollection<Point> {
if (counter > limitOffset) {
return;
}
if (headers.indexOf(latColumn)) {
console.log(`latColumn: ${latColumn}`);
console.log(`headers latColumn: ${headers.indexOf(latColumn)}`);
console.log(`headers.indexOf(latColumn): `, headers.indexOf(latColumn));
console.log('row', row);
if (headers.indexOf(latColumn) && headers.indexOf(lonColumn)) {
const lat = parseFloat(row['_' + headers.indexOf(latColumn)]);
const lon = parseFloat(row['_' + headers.indexOf(lonColumn)]);
@ -83,21 +81,28 @@ function csvToGeoJSON(options: Options): FeatureCollection<Point> {
}
// filtrer la ligne du header si présente
if (hasHeaders && counter > 1 || !hasHeaders || counter > limitOffset) {
features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [lon, lat],
},
properties: row,
});
if (lon && lat) {
if (hasHeaders && counter > 1 || !hasHeaders || counter > limitOffset) {
features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [lon, lat],
},
properties: row,
});
counter_features += 1;
}
}
if (headers.indexOf(latColumn) === -1) {
console.log('!!! no latColumn', row);
counter_missing_lat += 1;
}
if (headers.indexOf(lonColumn) === -1) {
console.log('!!! no lonColumn', row);
counter_missing_lon += 1;
}
}
else {
console.log('!!! no latColumn', row);
}
})
.on('end', () => {
geoJSON = {
@ -106,9 +111,12 @@ function csvToGeoJSON(options: Options): FeatureCollection<Point> {
};
fs.writeFileSync(`${dir}/${file}.geojson`, JSON.stringify(geoJSON, null, 2));
console.log(`GeoJSON créé avec succès : ${file}.geojson`);
console.log(`geoJSON lines: ${counter}`);
console.log(`geoJSON lines missing lat: ${counter_missing_lat}`);
console.log(`geoJSON lines missing lon: ${counter_missing_lon}`);
console.log(`geoJSON lines features: ${counter_features}`);
});
console.log(`geoJSON lines: ${counter}`);
return geoJSON;
}
@ -146,7 +154,26 @@ function checkFile(args: Options) {
}
function countGeoJSONFeatures(args: Options) {
const filePath = path.join(args.dir, `${args.file}.geojson`);
// Vérifier si le fichier GeoJSON existe
if (!fs.existsSync(filePath)) {
console.log(`Le fichier GeoJSON ${filePath} n'existe pas`);
return;
}
// Lire et parser le fichier GeoJSON
const geoJSON = JSON.parse(fs.readFileSync(filePath, 'utf8'));
// Compter le nombre de features
const featureCount = geoJSON.features?.length || 0;
console.log(`Nombre de features dans le GeoJSON : ${featureCount}`);
return featureCount;
}
console.log(`args: `, args);
checkFile(args);
csvToGeoJSON(args);
csvToGeoJSON(args);
// Appeler la fonction après la création du GeoJSON
countGeoJSONFeatures(args);

View file

@ -17,7 +17,10 @@ const MappingArbresEchirolles: MappingConfigType = {
// source: undefined,
config_name: 'Mapping des arbres d\'Echirolles',
config_author: 'tykayn <contact+geojson2osm@cipherbliss.com>',
default_properties_of_point: {natural: 'tree', source : 'Échirolles Métropole'},
default_properties_of_point: {
natural: 'tree',
source: 'Échirolles Métropole'
},
tags: {
// ******* booléens
// ******* nombres
@ -27,27 +30,27 @@ const MappingArbresEchirolles: MappingConfigType = {
"nom_latin": {
key_converted: "species",
conditional_values: {
"Platanus acerifolia": {'tags_to_add': {"species:wikidata": "Q24853030"}},
"Tilia cordata": {'tags_to_add': {"wikidata": "species:Q158746"}},
"Liriodendron tulipifera": {'tags_to_add': {"species:wikidata": "Q158783"}},
"Platanus acerifolia": { 'tags_to_add': { "species:wikidata": "Q24853030" } },
"Tilia cordata": { 'tags_to_add': { "wikidata": "species:Q158746" } },
"Liriodendron tulipifera": { 'tags_to_add': { "species:wikidata": "Q158783" } },
},
},
// - CADUC_PERS : leaf_cycle=evergreen pour persistant , deciduous pour caduque
// "caduc_pers": {
// conditional_values: {
// "Persistant ": {
// 'tags_to_add':{
// "leaf_cycle": "evergreen",
// }
// },
// "Caduc ": {
// 'tags_to_add':{
// "leaf_cycle": "deciduous",
// }
// },
// }
// },
// - CADUC_PERS : leaf_cycle=evergreen pour persistant , deciduous pour caduque
// "caduc_pers": {
// conditional_values: {
// "Persistant ": {
// 'tags_to_add':{
// "leaf_cycle": "evergreen",
// }
// },
// "Caduc ": {
// 'tags_to_add':{
// "leaf_cycle": "deciduous",
// }
// },
// }
// },
// - FEUIL_CONI : feuillu leaf_type=broadleaved / connifère leaf_type=needleleaved
// "feuil_coni": {
// conditional_values: {
@ -63,7 +66,7 @@ const MappingArbresEchirolles: MappingConfigType = {
// },
// }
// },
// - PARTICULAR : Majeur, Remarquable : historic=monument
// - PARTICULAR : Majeur, Remarquable : historic=monument
// "particular": {
// conditional_values: {
@ -80,27 +83,27 @@ const MappingArbresEchirolles: MappingConfigType = {
// },
// }
// },
// - FORME: tree_shape = curtain / free / half_free <= Architecturé, rideau / Libre / Semi-libre
// "forme": {
// key_converted: "tree_shape",
// conditional_values: {
// "Architecturé, rideau ": {
// 'tags_to_add':{
// "tree_shape": "curtain",
// },
// },
// "Semi-libre ": {
// 'tags_to_add':{
// "tree_shape": "half_free",
// },
// },
// "Libre ": {
// 'tags_to_add':{
// "tree_shape": "free",
// },
// },
// }
// },
// - FORME: tree_shape = curtain / free / half_free <= Architecturé, rideau / Libre / Semi-libre
// "forme": {
// key_converted: "tree_shape",
// conditional_values: {
// "Architecturé, rideau ": {
// 'tags_to_add':{
// "tree_shape": "curtain",
// },
// },
// "Semi-libre ": {
// 'tags_to_add':{
// "tree_shape": "half_free",
// },
// },
// "Libre ": {
// 'tags_to_add':{
// "tree_shape": "free",
// },
// },
// }
// },
},

View file

@ -0,0 +1,40 @@
/**
* panneaux de vitesse détectés sur panoramax
* utilisation:
*/
import { constrainedMemory } from "process";
import MappingConfigType from "../mapping-config.type";
const MappingPanneauxMaxSpeed: MappingConfigType = {
config_name: "MappingPanneauxMaxSpeed",
config_author: "tk",
default_properties_of_point: {
"traffic_sign": "FR:B14",
"maxspeed:source": "panoramax"
},
source: {
geojson_path: '',
url: ''
},
filters: {
// exclude_point_if_tag_not_empty: ['id_osm'], // exclure les points ayant déjà un id_osm pour éviter les doublons
// offset: 1
},
add_not_mapped_tags_too: false,
boolean_keys: [],
tags_to_ignore_if_value_is: ['Non renseigne'],
tags: {
"SourceFile": {
key_converted: 'maxspeed',
transform_function: (value: string) => {
let explode = value.split('-');
if (explode.length > 1) {
return explode[1];
}
return value;
}
},
}
}
export default MappingPanneauxMaxSpeed;

View file

@ -16,23 +16,12 @@ wget https://www.data.gouv.fr/fr/datasets/r/8d9398ae-3037-48b2-be19-412c24561fbb
echo "- OK IRVE"
echo "- récupérer les données présentes dans OpenStreetMap"
curl --header "Content-Type: plain/text" --data @content_irve_geojson.txt --trace-ascii website-data.log "https://overpass-api.de/api/interpreter" > "irve_osm_latest.geojson"
curl --header "Content-Type: plain/text" --data @content_irve_geojson.txt --trace-ascii website-data.log "https://overpass-api.de/api/interpreter" >"irve_osm_latest.geojson"
echo "- récupérer les données présentes dans Osmose"
wget "https://osmose.openstreetmap.fr/api/0.3/issues.geojson?full=true&status=open&item=8410&limit=20000" -O "osmose-item-irve-8411-intégrables.json"
echo "- OK Osmose"
##################
# moving datasets to the source folder etalab_data
##################
echo " - déplacement des datasets des IRVE dans le dossier etalab_data/irve_bornes_recharge"
mv latest.json ../etalab_data/irve_bornes_recharge/
#mv finess_idf.json ../etalab_data/finess/
mv irve_osm_latest.geojson ../etalab_data/irve_bornes_recharge/
mv clean_french_irve.csv ../etalab_data/irve_bornes_recharge/
mv osmose-item-irve-8411-intégrables.json ../etalab_data/irve_bornes_recharge/
##################
# other sources of data should be placed in data_other folder
##################
@ -44,11 +33,6 @@ wget "https://data.issy.com/api/explore/v2.1/catalog/datasets/arbres-remarquable
echo "- récupérer les données de cyclabilité de Rouen"
wget "https://data.metropole-rouen-normandie.fr/api/explore/v2.1/catalog/datasets/liste-des-stationnements-cyclables-metropole-rouen-normandie/exports/geojson?lang=fr&timezone=Europe%2FBerlin" -O "rouen_parking_velos.json"
mv "issy_les_mx_arbres.json" ../data_other/arbres/issy_les_mx_arbres.json
mv "geojson?lang=fr" ../data_other/cyclabilité/issy_les_mx_cyclabilité.json
mv "rouen_parking_velos.json" ../data_other/cyclabilité/rouen_parking_velos.json
# clean logs and finish
rm website-data.log
cd ..