diff --git a/conflate_geojson_with_osm.ts b/conflate_geojson_with_osm.ts new file mode 100644 index 0000000..2af1a59 --- /dev/null +++ b/conflate_geojson_with_osm.ts @@ -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 + * + * 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); diff --git a/csv_to_geojson.ts b/csv_to_geojson.ts index 0976d36..6a6bd0a 100644 --- a/csv_to_geojson.ts +++ b/csv_to_geojson.ts @@ -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 { const { dir, file, latColumn, lonColumn, hasHeaders } = options; @@ -38,13 +41,12 @@ function csvToGeoJSON(options: Options): FeatureCollection { 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 { 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 { } // 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 { }; 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); \ No newline at end of file +csvToGeoJSON(args); +// Appeler la fonction après la création du GeoJSON +countGeoJSONFeatures(args); diff --git a/mappings/converters/configArbresEchirolles.ts b/mappings/converters/configArbresEchirolles.ts index d9e3655..2d9bc7c 100644 --- a/mappings/converters/configArbresEchirolles.ts +++ b/mappings/converters/configArbresEchirolles.ts @@ -17,7 +17,10 @@ const MappingArbresEchirolles: MappingConfigType = { // source: undefined, config_name: 'Mapping des arbres d\'Echirolles', config_author: 'tykayn ', - 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", + // }, + // }, + // } + // }, }, diff --git a/mappings/converters/configPanneauxMaxSpeed.ts b/mappings/converters/configPanneauxMaxSpeed.ts new file mode 100644 index 0000000..f3433b4 --- /dev/null +++ b/mappings/converters/configPanneauxMaxSpeed.ts @@ -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; diff --git a/update_scripts/get_datasets.sh b/update_scripts/get_datasets.sh index ff459e7..1167995 100755 --- a/update_scripts/get_datasets.sh +++ b/update_scripts/get_datasets.sh @@ -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 ..