Merge pull request #1003 from pietervdvn/feature/maproulette

Maproulette tasks
This commit is contained in:
Pieter Vander Vennet 2022-07-27 16:48:58 +02:00 committed by GitHub
commit c81d36fb8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1253 additions and 15 deletions

View file

@ -349,11 +349,12 @@ snap_onto_layers | _undefined_ | If a way of the given layer(s) is closeby, will
max_snap_distance | 5 | The maximum distance that the imported point will be moved to snap onto a way in an already existing layer (in meters). This is previewed to the contributor, similar to the 'add new point'-action of MapComplete
note_id | _undefined_ | If given, this key will be read. The corresponding note on OSM will be closed, stating 'imported'
location_picker | photo | Chooses the background for the precise location picker, options are 'map', 'photo' or 'osmbasedmap' or 'none' if the precise input picker should be disabled
maproulette_id | _undefined_ | If given, the maproulette challenge will be marked as fixed
#### Example usage of import_button
`{import_button(,,Import this data into OpenStreetMap,./assets/svg/addSmall.svg,,5,,photo)}`
`{import_button(,,Import this data into OpenStreetMap,./assets/svg/addSmall.svg,,5,,photo,)}`

View file

@ -82,6 +82,28 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
Utils.downloadJsonCached(url, 60 * 60)
.then(json => {
self.state.setData("loaded")
// TODO: move somewhere else, just for testing
// Check for maproulette data
if (url.startsWith("https://maproulette.org/api/v2/tasks/box/")) {
console.log("MapRoulette data detected")
const data = json;
let maprouletteFeatures: any[] = [];
data.forEach(element => {
maprouletteFeatures.push({
type: "Feature",
geometry: {
type: "Point",
coordinates: [element.point.lng, element.point.lat]
},
properties: {
// Map all properties to the feature
...element,
}
});
});
json.features = maprouletteFeatures;
}
if (json.features === undefined || json.features === null) {
return;
}

39
Logic/Maproulette.ts Normal file
View file

@ -0,0 +1,39 @@
import Constants from "../Models/Constants";
export default class Maproulette {
/**
* The API endpoint to use
*/
endpoint: string;
/**
* The API key to use for all requests
*/
private apiKey: string;
/**
* Creates a new Maproulette instance
* @param endpoint The API endpoint to use
*/
constructor(endpoint: string = "https://maproulette.org/api/v2") {
this.endpoint = endpoint;
this.apiKey = Constants.MaprouletteApiKey;
}
/**
* Close a task
* @param taskId The task to close
*/
async closeTask(taskId: number): Promise<void> {
const response = await fetch(`${this.endpoint}/task/${taskId}/1`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"apiKey": this.apiKey,
},
});
if (response.status !== 304) {
console.log(`Failed to close task: ${response.status}`);
}
}
}

View file

@ -13,6 +13,7 @@ import ChangeToElementsActor from "../Actors/ChangeToElementsActor";
import PendingChangesUploader from "../Actors/PendingChangesUploader";
import * as translators from "../../assets/translators.json"
import {post} from "jquery";
import Maproulette from "../Maproulette";
/**
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
@ -34,6 +35,11 @@ export default class UserRelatedState extends ElementsState {
*/
public mangroveIdentity: MangroveIdentity;
/**
* Maproulette connection
*/
public maprouletteConnection: Maproulette;
public readonly isTranslator : Store<boolean>;
public readonly installedUserThemes: Store<string[]>
@ -80,6 +86,8 @@ export default class UserRelatedState extends ElementsState {
this.osmConnection.GetLongPreference("identity", "mangrove")
);
this.maprouletteConnection = new Maproulette();
if (layoutToUse?.hideFromOverview) {
this.osmConnection.isLoggedIn.addCallbackAndRunD(loggedIn => {
if (loggedIn) {

View file

@ -7,6 +7,15 @@ export default class Constants {
public static ImgurApiKey = '7070e7167f0a25a'
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
/**
* API key for Maproulette
*
* Currently there is no user-friendly way to get the user's API key.
* See https://github.com/maproulette/maproulette2/issues/476 for more information.
* Using an empty string however does work for most actions, but will attribute all actions to the Superuser.
*/
public static readonly MaprouletteApiKey = "";
public static defaultOverpassUrls = [
// The official instance, 10000 queries per day per project allowed
"https://overpass-api.de/api/interpreter",

View file

@ -550,15 +550,21 @@ export class ImportPointButton extends AbstractImportButton {
name: "note_id",
doc: "If given, this key will be read. The corresponding note on OSM will be closed, stating 'imported'"
},
{name:"location_picker",
{
name:"location_picker",
defaultValue: "photo",
doc: "Chooses the background for the precise location picker, options are 'map', 'photo' or 'osmbasedmap' or 'none' if the precise input picker should be disabled"}],
doc: "Chooses the background for the precise location picker, options are 'map', 'photo' or 'osmbasedmap' or 'none' if the precise input picker should be disabled"
},
{
name: "maproulette_id",
doc: "If given, the maproulette challenge will be marked as fixed"
}],
{ showRemovedTags: false}
)
}
private static createConfirmPanelForPoint(
args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, newTags: UIEventSource<any>, targetLayer: string, note_id: string },
args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, newTags: UIEventSource<any>, targetLayer: string, note_id: string, maproulette_id: string },
state: FeaturePipelineState,
guiState: DefaultGuiState,
originalFeatureTags: UIEventSource<any>,
@ -600,6 +606,19 @@ export class ImportPointButton extends AbstractImportButton {
originalFeatureTags.data["closed_at"] = new Date().toISOString()
originalFeatureTags.ping()
}
let maproulette_id = originalFeatureTags.data[args.maproulette_id];
console.log("Checking if we need to mark a maproulette task as fixed (" + maproulette_id + ")")
if (maproulette_id !== undefined) {
if (state.featureSwitchIsTesting.data){
console.log("Not marking maproulette task " + maproulette_id + " as fixed, because we are in testing mode")
} else {
console.log("Marking maproulette task as fixed")
state.maprouletteConnection.closeTask(Number(maproulette_id));
originalFeatureTags.data["mr_taskStatus"] = "Fixed";
originalFeatureTags.ping();
}
}
}
let preciseInputOption = args["location_picker"]

View file

@ -42,6 +42,13 @@ export default class TagApplyButton implements AutoAction {
public static generateTagsToApply(spec: string, tagSource: Store<any>): Store<Tag[]> {
// Check whether we need to look up a single value
if (!spec.includes(";") && !spec.includes("=") && spec.includes("$")){
// We seem to be dealing with a single value, fetch it
spec = tagSource.data[spec.replace("$","")]
}
const tgsSpec = spec.split(";").map(spec => {
const kv = spec.split("=").map(s => s.trim());
if (kv.length != 2) {

View file

@ -1,4 +1,4 @@
import {Store, UIEventSource} from "../Logic/UIEventSource";
import {Store, Stores, UIEventSource} from "../Logic/UIEventSource";
import {VariableUiElement} from "./Base/VariableUIElement";
import LiveQueryHandler from "../Logic/Web/LiveQueryHandler";
import {ImageCarousel} from "./Image/ImageCarousel";
@ -57,8 +57,9 @@ import {SaveButton} from "./Popup/SaveButton";
import {MapillaryLink} from "./BigComponents/MapillaryLink";
import {CheckBox} from "./Input/Checkboxes";
import Slider from "./Input/Slider";
import {OsmFeature} from "../Models/OsmFeature";
import List from "./Base/List";
import StatisticsPanel from "./BigComponents/StatisticsPanel";
import { OsmFeature } from "../Models/OsmFeature";
export interface SpecialVisualization {
funcName: string,
@ -1100,6 +1101,39 @@ export default class SpecialVisualizations {
},
new NearbyImageVis(),
new MapillaryLinkVis(),
{
funcName: "maproulette_task",
args: [],
constr(state, tagSource, argument, guistate) {
let parentId = tagSource.data.mr_challengeId;
let challenge = Stores.FromPromise(Utils.downloadJsonCached(`https://maproulette.org/api/v2/challenge/${parentId}`,24*60*60*1000));
let details = new VariableUiElement( challenge.map(challenge => {
let listItems: BaseUIElement[] = [];
let title: BaseUIElement;
if (challenge?.name) {
title = new Title(challenge.name);
}
if (challenge?.description) {
listItems.push(new FixedUiElement(challenge.description));
}
if (challenge?.instruction) {
listItems.push(new FixedUiElement(challenge.instruction));
}
if(listItems.length === 0) {
return undefined;
} else {
return [title, new List(listItems)];
}
}))
return details;
},
docs: "Show details of a MapRoulette task"
},
{
funcName: "statistics",
docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",

View file

@ -0,0 +1,12 @@
[
{
"path": "logomark.svg",
"license": "MIT",
"authors": [
"MapRoulette"
],
"sources": [
"https://github.com/maproulette/docs/blob/master/src/assets/svg/logo.svg"
]
}
]

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View file

@ -0,0 +1,225 @@
{
"id": "maproulette",
"source": {
"geoJson": "https://maproulette.org/api/v2/tasks/box/{x_min}/{y_min}/{x_max}/{y_max}",
"geoJsonZoomLevel": 16,
"osmTags": "id~*"
},
"mapRendering": [
{
"location": [
"point",
"centroid"
],
"icon": {
"render": "./assets/layers/maproulette/logomark.svg",
"mappings": [
{
"if": "status=0",
"then": "pin:#959DFF"
},
{
"if": "status=1",
"then": "pin:#65D2DA"
},
{
"if": "status=2",
"then": "pin:#F7BB59"
},
{
"if": "status=3",
"then": "pin:#F7BB59"
},
{
"if": "status=4",
"then": "pin:#737373"
},
{
"if": "status=5",
"then": "pin:#CCB186"
},
{
"if": "status=6",
"then": "pin:#FF5E63"
},
{
"if": "status=9",
"then": "pin:#FF349C"
}
]
},
"iconSize": "40,40,center"
}
],
"tagRenderings": [
{
"id": "status",
"render": "Current status: {status}",
"mappings": [
{
"if": "status=0",
"then": {
"en": "Task is created"
}
},
{
"if": "status=1",
"then": {
"en": "Task is fixed"
}
},
{
"if": "status=2",
"then": {
"en": "Task is a false positive"
}
},
{
"if": "status=3",
"then": {
"en": "Task is skipped"
}
},
{
"if": "status=4",
"then": {
"en": "Task is deleted"
}
},
{
"if": "status=5",
"then": {
"en": "Task is already fixed"
}
},
{
"if": "status=6",
"then": {
"en": "Task is marked as too hard"
}
},
{
"if": "status=9",
"then": {
"en": "Task is disabled"
}
}
]
},
{
"id": "blurb",
"condition": "blurb~*",
"render": "{blurb}"
}
],
"description": {
"en": "Layer showing all tasks in MapRoulette"
},
"minzoom": 15,
"name": {
"en": "MapRoulette Tasks"
},
"title": {
"render": {
"en": "MapRoulette Item: {parentName}"
}
},
"titleIcons": [
{
"id": "maproulette",
"render": "<a href='https://maproulette.org/challenge/{parentId}/task/{id}' target='_blank'><img src='./assets/layers/maproulette/logomark.svg'/></a>"
}
],
"filter": [
{
"id": "status",
"options": [
{
"question": {
"en": "Show tasks with all statuses"
}
},
{
"question": {
"en": "Show tasks that are created"
},
"osmTags": "status=0"
},
{
"question": {
"en": "Show tasks that are fixed"
},
"osmTags": "status=1"
},
{
"question": {
"en": "Show tasks that are false positives"
},
"osmTags": "status=2"
},
{
"question": {
"en": "Show tasks that are skipped"
},
"osmTags": "status=3"
},
{
"question": {
"en": "Show tasks that are deleted"
},
"osmTags": "status=4"
},
{
"question": {
"en": "Show tasks that are already fixed"
},
"osmTags": "status=5"
},
{
"question": {
"en": "Show tasks that are marked as too hard"
},
"osmTags": "status=6"
},
{
"question": {
"en": "Show tasks that are disabled"
},
"osmTags": "status=9"
}
]
},
{
"id": "parent-name",
"options": [
{
"osmTags": "parentName~i~.*{search}.*",
"fields": [
{
"name": "search"
}
],
"question": {
"en": "Challenge name contains {search}"
}
}
]
},
{
"id": "parent-id",
"options": [
{
"osmTags": "parentId={search}",
"fields": [
{
"name": "search"
}
],
"question": {
"en": "Challenge ID matches {search}"
}
}
]
}
]
}

View file

@ -0,0 +1,194 @@
{
"id": "maproulette_challenge",
"name": null,
"description": {
"en": "Layer showing tasks of a MapRoulette challenge"
},
"source": {
"osmTags": "id~*",
"geoJson": "https://maproulette.org/api/v2/challenge/view/27971",
"isOsmCache": false
},
"title": {
"render": {
"en": "Item in MapRoulette"
}
},
"titleIcons": [
{
"id": "maproulette",
"render": "<a href='https://maproulette.org/challenge/{mr_challengeId}/task/{mr_taskId}' target='_blank'><img src='./assets/layers/maproulette/logomark.svg'/></a>"
}
],
"mapRendering": [
{
"location": [
"point",
"centroid"
],
"icon": {
"render": "./assets/layers/maproulette/logomark.svg",
"mappings": [
{
"if": "mr_taskStatus=Created",
"then": "pin:#959DFF"
},
{
"if": "mr_taskStatus=Fixed",
"then": "pin:#65D2DA"
},
{
"if": "mr_taskStatus=False positive",
"then": "pin:#F7BB59"
},
{
"if": "mr_taskStatus=Skipped",
"then": "pin:#F7BB59"
},
{
"if": "mr_taskStatus=Deleted",
"then": "pin:#737373"
},
{
"if": "mr_taskStatus=Already fixed",
"then": "pin:#CCB186"
},
{
"if": "mr_taskStatus=Too hard",
"then": "pin:#FF5E63"
},
{
"if": "mr_taskStatus=Disabled",
"then": "pin:#FF349C"
}
]
},
"iconSize": "40,40,bottom"
}
],
"tagRenderings": [
{
"id": "details",
"render": "{maproulette_task()}"
},
{
"id": "status",
"render": "Current status: {status}",
"mappings": [
{
"if": "mr_taskStatus=Created",
"then": {
"en": "Task is created"
}
},
{
"if": "mr_taskStatus=Fixed",
"then": {
"en": "Task is fixed"
}
},
{
"if": "mr_taskStatus=False positive",
"then": {
"en": "Task is a false positive"
}
},
{
"if": "mr_taskStatus=Skipped",
"then": {
"en": "Task is skipped"
}
},
{
"if": "mr_taskStatus=Deleted",
"then": {
"en": "Task is deleted"
}
},
{
"if": "mr_taskStatus=Already fixed",
"then": {
"en": "Task is already fixed"
}
},
{
"if": "mr_taskStatus=Too hard",
"then": {
"en": "Task is marked as too hard"
}
},
{
"if": "mr_taskStatus=Disabled",
"then": {
"en": "Task is disabled"
}
}
]
},
{
"id": "blurb",
"condition": "blurb~*",
"render": "{blurb}"
}
],
"filter": [
{
"id": "status",
"options": [
{
"question": {
"en": "Show tasks with all statuses"
}
},
{
"question": {
"en": "Show tasks that are created"
},
"osmTags": "mr_taskStatus=Created"
},
{
"question": {
"en": "Show tasks that are fixed"
},
"osmTags": "mr_taskStatus=Fixed"
},
{
"question": {
"en": "Show tasks that are false positives"
},
"osmTags": "mr_taskStatus=False positive"
},
{
"question": {
"en": "Show tasks that are skipped"
},
"osmTags": "mr_taskStatus=Skipped"
},
{
"question": {
"en": "Show tasks that are deleted"
},
"osmTags": "mr_taskStatus=Deleted"
},
{
"question": {
"en": "Show tasks that are already fixed"
},
"osmTags": "mr_taskStatus=Already fixed"
},
{
"question": {
"en": "Show tasks that are marked as too hard"
},
"osmTags": "mr_taskStatus=Too hard"
},
{
"question": {
"en": "Show tasks that are disabled"
},
"osmTags": "mr_taskStatus=Disabled"
}
]
}
]
}

View file

@ -303,7 +303,7 @@
},
"allowMove": {
"enableRelocation": false,
"enableImproveAccuraccy": true
"enableImproveAccuracy": true
},
"mapRendering": [
{

View file

@ -246,6 +246,10 @@
"if": "theme=mapcomplete-changes",
"then": "./assets/svg/logo.svg"
},
{
"if": "theme=maproulette",
"then": "./assets/layers/maproulette/logomark.svg"
},
{
"if": "theme=maps",
"then": "./assets/themes/maps/logo.svg"

View file

@ -0,0 +1,19 @@
{
"id": "maproulette",
"title": {
"en": "MapRoulette Tasks"
},
"description": {
"en": "Theme showing MapRoulette tasks, allowing you to search, filter and fix them."
},
"version": "1.0.0",
"hideFromOverview": true,
"icon": "./assets/layers/maproulette/logomark.svg",
"maintainer": "",
"startLat": 0,
"startLon": 0,
"startZoom": 4,
"layers": [
"maproulette"
]
}

View file

@ -20,7 +20,7 @@
"widenFactor": 2,
"hideFromOverview": false,
"layers": [
{
{
"builtin": "indoors",
"override": {
"minzoom": 19,
@ -366,6 +366,53 @@
}
]
}
},
{
"builtin": "maproulette_challenge",
"override": {
"source": {
"geoJson": "https://maproulette.org/api/v2/challenge/view/28012"
},
"calculatedTags": [
"_closest_osm_hotel=feat.closest('hotel')?.properties?.id",
"_closest_osm_hotel_distance=feat.distanceTo(feat.properties._closest_osm_hotel)",
"_has_closeby_feature=Number(feat.properties._closest_osm_hotel_distance) < 50 ? 'yes' : 'no'"
],
"+tagRenderings": [
{
"id": "import-button",
"condition": "_has_closeby_feature=no",
"render": {
"special": {
"type": "import_button",
"targetLayer": "hotel",
"tags": "tags",
"text": {
"en": "Import"
},
"icon": "./assets/svg/addSmall.svg",
"location_picker": "photo",
"maproulette_id": "mr_taskId"
}
}
},
{
"id": "tag-apply-button",
"condition": "_has_closeby_feature=yes",
"render": {
"special": {
"type": "tag_apply",
"tags_to_apply": "$tags",
"message": {
"en": "Add all the suggested tags"
},
"image": "./assets/svg/addSmall.svg",
"id_of_object_to_apply_this_one": "_closest_osm_hotel"
}
}
}
]
}
}
],
"overrideAll": {

View file

@ -51,6 +51,22 @@
"tagRenderings": [
"all_tags"
]
},
{
"builtin": "maproulette_challenge",
"override": {
"calculatedTags": [
"_closest_osm_street_lamp=feat.closest('street_lamps')?.properties?.id",
"_closest_osm_street_lamp_distance=feat.distanceTo(feat.properties._closest_osm_street_lamp)",
"_has_closeby_feature=Number(feat.properties._closest_osm_street_lamp_distance) < 5 ? 'yes' : 'no'"
],
"tagRenderings+": [
{
"id": "import",
"render": "{import_button(street_lamps,tags,Import,./assets/svg/addSmall.svg,,,,photo,mr_taskId)}"
}
]
}
}
],
"hideFromOverview": true

View file

@ -4464,6 +4464,159 @@
"render": "Map"
}
},
"maproulette": {
"description": "Layer showing all tasks in MapRoulette",
"filter": {
"0": {
"options": {
"0": {
"question": "Show tasks with all statuses"
},
"1": {
"question": "Show tasks that are created"
},
"2": {
"question": "Show tasks that are fixed"
},
"3": {
"question": "Show tasks that are false positives"
},
"4": {
"question": "Show tasks that are skipped"
},
"5": {
"question": "Show tasks that are deleted"
},
"6": {
"question": "Show tasks that are already fixed"
},
"7": {
"question": "Show tasks that are marked as too hard"
},
"8": {
"question": "Show tasks that are disabled"
}
}
},
"1": {
"options": {
"0": {
"question": "Challenge name contains {search}"
}
}
},
"2": {
"options": {
"0": {
"question": "Challenge ID matches {search}"
}
}
}
},
"name": "MapRoulette Tasks",
"tagRenderings": {
"status": {
"mappings": {
"0": {
"then": "Task is created"
},
"1": {
"then": "Task is fixed"
},
"2": {
"then": "Task is a false positive"
},
"3": {
"then": "Task is skipped"
},
"4": {
"then": "Task is deleted"
},
"5": {
"then": "Task is already fixed"
},
"6": {
"then": "Task is marked as too hard"
},
"7": {
"then": "Task is disabled"
}
}
}
},
"title": {
"render": "MapRoulette Item: {parentName}"
}
},
"maproulette_challenge": {
"description": "Layer showing tasks of a MapRoulette challenge",
"filter": {
"0": {
"options": {
"0": {
"question": "Show tasks with all statuses"
},
"1": {
"question": "Show tasks that are created"
},
"2": {
"question": "Show tasks that are fixed"
},
"3": {
"question": "Show tasks that are false positives"
},
"4": {
"question": "Show tasks that are skipped"
},
"5": {
"question": "Show tasks that are deleted"
},
"6": {
"question": "Show tasks that are already fixed"
},
"7": {
"question": "Show tasks that are marked as too hard"
},
"8": {
"question": "Show tasks that are disabled"
}
}
}
},
"tagRenderings": {
"status": {
"mappings": {
"0": {
"then": "Task is created"
},
"1": {
"then": "Task is fixed"
},
"2": {
"then": "Task is a false positive"
},
"3": {
"then": "Task is skipped"
},
"4": {
"then": "Task is deleted"
},
"5": {
"then": "Task is already fixed"
},
"6": {
"then": "Task is marked as too hard"
},
"7": {
"then": "Task is disabled"
}
}
}
},
"title": {
"render": "Item in MapRoulette"
}
},
"maxspeed": {
"description": "Shows the allowed speed for every road",
"name": "Maxspeed",

View file

@ -725,6 +725,10 @@
"shortDescription": "Shows changes made by MapComplete",
"title": "Changes made with MapComplete"
},
"maproulette": {
"description": "Theme showing MapRoulette tasks, allowing you to search, filter and fix them.",
"title": "MapRoulette Tasks"
},
"maps": {
"description": "On this map you can find all maps OpenStreetMap knows - typically a big map on an information board showing the area, city or region, e.g. a tourist map on the back of a billboard, a map of a nature reserve, a map of cycling networks in the region, ...) <br/><br/>If a map is missing, you can easily map this map on OpenStreetMap.",
"shortDescription": "This theme shows all (touristic) maps that OpenStreetMap knows of",

23
package-lock.json generated
View file

@ -18,6 +18,7 @@
"@turf/length": "^6.5.0",
"@turf/turf": "^6.5.0",
"@types/chai": "^4.3.0",
"@types/geojson": "^7946.0.10",
"@types/jquery": "^3.5.5",
"@types/leaflet-markercluster": "^1.0.3",
"@types/leaflet-providers": "^1.2.0",
@ -3224,9 +3225,9 @@
"integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw=="
},
"node_modules/@types/geojson": {
"version": "7946.0.8",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz",
"integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA=="
"version": "7946.0.10",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
},
"node_modules/@types/jquery": {
"version": "3.5.5",
@ -7176,6 +7177,11 @@
"rbush": "^3.0.1"
}
},
"node_modules/geojson-rbush/node_modules/@types/geojson": {
"version": "7946.0.8",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz",
"integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA=="
},
"node_modules/geojson-rbush/node_modules/quickselect": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
@ -19255,9 +19261,9 @@
"integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw=="
},
"@types/geojson": {
"version": "7946.0.8",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz",
"integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA=="
"version": "7946.0.10",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
},
"@types/jquery": {
"version": "3.5.5",
@ -22428,6 +22434,11 @@
"rbush": "^3.0.1"
},
"dependencies": {
"@types/geojson": {
"version": "7946.0.8",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz",
"integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA=="
},
"quickselect": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",

View file

@ -77,6 +77,7 @@
"@turf/length": "^6.5.0",
"@turf/turf": "^6.5.0",
"@types/chai": "^4.3.0",
"@types/geojson": "^7946.0.10",
"@types/jquery": "^3.5.5",
"@types/leaflet-markercluster": "^1.0.3",
"@types/leaflet-providers": "^1.2.0",

View file

@ -0,0 +1,102 @@
/**
* Class containing all constants and tables used in the script
*
* @class Constants
*/
export default class Constants {
/**
* Table used to determine tags for the category
*
* Keys are the original category names,
* values are an object containing the tags
*/
public static categories = {
restaurant: {
amenity: "restaurant",
},
parking: {
amenity: "parking",
},
hotel: {
tourism: "hotel",
},
wc: {
amenity: "toilets",
},
winkel: {
shop: "yes",
},
apotheek: {
amenity: "pharmacy",
healthcare: "pharmacy",
},
ziekenhuis: {
amenity: "hospital",
healthcare: "hospital",
},
bezienswaardigheid: {
tourism: "attraction",
},
ontspanning: {
fixme: "Needs proper tags",
},
cafe: {
amenity: "cafe",
},
dienst: {
fixme: "Needs proper tags",
},
bank: {
amenity: "bank",
},
gas: {
amenity: "fuel",
},
medical: {
fixme: "Needs proper tags",
},
obstacle: {
fixme: "Needs proper tags",
},
};
/**
* Table used to rename original Onwheels properties to their corresponding OSM properties
*
* Keys are the original Onwheels properties, values are the corresponding OSM properties
*/
public static names = {
ID: "id",
Naam: "name",
Straat: "addr:street",
Nummer: "addr:housenumber",
Postcode: "addr:postcode",
Plaats: "addr:city",
Website: "website",
Email: "email",
"Aantal aangepaste parkeerplaatsen": "capacity:disabled",
"Aantal treden": "step_count",
"Hellend vlak aanwezig": "ramp",
"Baby verzorging aanwezig": "changing_table",
"Totale hoogte van de treden": "kerb:height",
"Deurbreedte": "door:width",
};
/**
* In some cases types might need to be converted as well
*
* Keys are the OSM properties, values are the wanted type
*/
public static types = {
"Hellend vlak aanwezig": "boolean",
"Baby verzorging aanwezig": "boolean",
};
/**
* Some tags also need to have units added
*/
public static units = {
"Totale hoogte van de treden": "cm",
"Deurbreedte": "cm",
};
}

View file

@ -0,0 +1,234 @@
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<any, string>[] = 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));

View file

@ -123,7 +123,7 @@ describe('RewriteSpecial', function () {
const r = new RewriteSpecial().convert(tr, "test").result
expect(r).to.deep.eq({
"id": "uk_addresses_import_button",
"render": {'*': "{import_button(address,urpn_count=$urpn_count;ref:GB:uprn=$ref:GB:uprn$,Add this address,./assets/themes/uk_addresses/housenumber_add.svg,,,,none)}"}
"render": {'*': "{import_button(address,urpn_count=$urpn_count;ref:GB:uprn=$ref:GB:uprn$,Add this address,./assets/themes/uk_addresses/housenumber_add.svg,,,,none,)}"}
})
})
});