import { parse } from "csv-parse/sync"; import { readFileSync, writeFileSync } from "fs"; import { Feature, FeatureCollection, GeoJsonProperties } from "geojson"; import Constants from "./constants"; /** * Function to determine the tags for a category * * @param category The category of the item * @returns List of tags for the category */ function categoryTags(category: string): GeoJsonProperties { const tags = { tags: Object.keys(Constants.categories[category]).map((tag) => { return `${tag}=${Constants.categories[category][tag]}`; }), }; if (!tags) { throw `Unknown category: ${category}`; } return tags; } /** * Rename tags to match the OSM standard * * @param item The item to convert * @returns GeoJsonProperties for the item */ function renameTags(item): GeoJsonProperties { const properties: GeoJsonProperties = {}; properties.tags = []; // Loop through the original item tags for (const key in item) { // Check if we need it and it's not a null value if (Constants.names[key] && item[key]) { // Name and id tags need to be in the properties if (Constants.names[key] == "name" || Constants.names[key] == "id") { properties[Constants.names[key]] = item[key]; } // Other tags need to be in the tags variable if (Constants.names[key] !== "id") { // Website needs to have at least any = encoded if(Constants.names[key] == "website") { let website = item[key]; // Encode URL website = website.replace("=", "%3D"); item[key] = website; } properties.tags.push(Constants.names[key] + "=" + item[key]); } } } return properties; } /** * Convert types to match the OSM standard * * @param properties The properties to convert * @returns The converted properties */ function convertTypes(properties: GeoJsonProperties): GeoJsonProperties { // Split the tags into a list let tags = properties.tags.split(";"); for (const tag in tags) { // Split the tag into key and value const key = tags[tag].split("=")[0]; const value = tags[tag].split("=")[1]; const originalKey = Object.keys(Constants.names).find( (tag) => Constants.names[tag] === key ); if (Constants.types[originalKey]) { // We need to convert the value to the correct type let newValue; switch (Constants.types[originalKey]) { case "boolean": newValue = value === "1" ? "yes" : "no"; break; default: newValue = value; break; } tags[tag] = `${key}=${newValue}`; } } // Rejoin the tags properties.tags = tags.join(";"); // Return the properties return properties; } /** * Function to add units to the properties if necessary * * @param properties The properties to add units to * @returns The properties with units added */ function addUnits(properties: GeoJsonProperties): GeoJsonProperties { // Split the tags into a list let tags = properties.tags.split(";"); for (const tag in tags) { const key = tags[tag].split("=")[0]; const value = tags[tag].split("=")[1]; const originalKey = Object.keys(Constants.names).find( (tag) => Constants.names[tag] === key ); // Check if the property needs units, and doesn't already have them if (Constants.units[originalKey] && value.match(/.*([A-z]).*/gi) === null) { tags[tag] = `${key}=${value} ${Constants.units[originalKey]}`; } } // Rejoin the tags properties.tags = tags.join(";"); // Return the properties return properties; } /** * Function that adds Maproulette instructions and blurb to each item * * @param properties The properties to add Maproulette tags to * @param item The original CSV item */ function addMaprouletteTags(properties: GeoJsonProperties, item: any): GeoJsonProperties { properties[ "blurb" ] = `This is feature out of the ${item["Categorie"]} category. It may match another OSM item, if so, you can add any missing tags to it. If it doesn't match any other OSM item, you can create a new one. Here is a list of tags that can be added: ${properties["tags"].split(";").join("\n")} You can also easily import this item using MapComplete: https://mapcomplete.osm.be/onwheels.html#${properties["id"]}`; return properties; } /** * Main function to convert original CSV into GeoJSON * * @param args List of arguments [input.csv] */ function main(args: string[]): void { const csvOptions = { columns: true, skip_empty_lines: true, trim: true, }; const file = args[0]; const output = args[1]; // Create an empty list to store the converted features var items: Feature[] = []; // Read CSV file const csv: Record[] = parse(readFileSync(file), csvOptions); // Loop through all the entries for (var i = 0; i < csv.length; i++) { const item = csv[i]; // Determine coordinates const lat = Number(item["Latitude"]); const lon = Number(item["Longitude"]); // Check if coordinates are valid if (isNaN(lat) || isNaN(lon)) { throw `Not a valid lat or lon for entry ${i}: ${JSON.stringify(item)}`; } // Create a new collection to store the converted properties var properties: GeoJsonProperties = {}; // Add standard tags for category const category = item["Categorie"]; const tagsCategory = categoryTags(category); // Add the rest of the needed tags properties = { ...properties, ...renameTags(item) }; // Merge them together properties.tags = [...tagsCategory.tags, ...properties.tags]; properties.tags = properties.tags.join(";"); // Convert types properties = convertTypes(properties); // Add units if necessary properties = addUnits(properties); // Add Maproulette tags properties = addMaprouletteTags(properties, item); // Create the new feature const feature: Feature = { type: "Feature", id: item["ID"], geometry: { type: "Point", coordinates: [lon, lat], }, properties, }; // Push it to the list we created earlier items.push(feature); } // Make a FeatureCollection out of it const featureCollection: FeatureCollection = { type: "FeatureCollection", features: items, }; // Write the data to a file or output to the console if (output) { writeFileSync( `${output}.geojson`, JSON.stringify(featureCollection, null, 2) ); } else { console.log(JSON.stringify(featureCollection)); } } // Execute the main function, with the stripped arguments main(process.argv.slice(2));