forked from MapComplete/MapComplete
		
	
							parent
							
								
									c8971a1cbe
								
							
						
					
					
						commit
						8f51dd8d64
					
				
					 4 changed files with 123 additions and 70 deletions
				
			
		|  | @ -16,6 +16,7 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|     private readonly _splitPointsCoordinates: [number, number][] // lon, lat
 |     private readonly _splitPointsCoordinates: [number, number][] // lon, lat
 | ||||||
|     private _meta: { theme: string; changeType: "split" } |     private _meta: { theme: string; changeType: "split" } | ||||||
|     private _toleranceInMeters: number |     private _toleranceInMeters: number | ||||||
|  |     private _withNewCoordinates: (coordinates: [number, number][]) => void | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Create a changedescription for splitting a point. |      * Create a changedescription for splitting a point. | ||||||
|  | @ -24,17 +25,20 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|      * @param splitPointCoordinates: lon, lat |      * @param splitPointCoordinates: lon, lat | ||||||
|      * @param meta |      * @param meta | ||||||
|      * @param toleranceInMeters: if a splitpoint closer then this amount of meters to an existing point, the existing point will be used to split the line instead of a new point |      * @param toleranceInMeters: if a splitpoint closer then this amount of meters to an existing point, the existing point will be used to split the line instead of a new point | ||||||
|  |      * @param withNewCoordinates: an optional callback which will leak the new coordinates of the original way | ||||||
|      */ |      */ | ||||||
|     constructor( |     constructor( | ||||||
|         wayId: string, |         wayId: string, | ||||||
|         splitPointCoordinates: [number, number][], |         splitPointCoordinates: [number, number][], | ||||||
|         meta: { theme: string }, |         meta: { theme: string }, | ||||||
|         toleranceInMeters = 5 |         toleranceInMeters = 5, | ||||||
|  |         withNewCoordinates?: (coordinates: [number, number][]) => void | ||||||
|     ) { |     ) { | ||||||
|         super(wayId, true) |         super(wayId, true) | ||||||
|         this.wayId = wayId |         this.wayId = wayId | ||||||
|         this._splitPointsCoordinates = splitPointCoordinates |         this._splitPointsCoordinates = splitPointCoordinates | ||||||
|         this._toleranceInMeters = toleranceInMeters |         this._toleranceInMeters = toleranceInMeters | ||||||
|  |         this._withNewCoordinates = withNewCoordinates | ||||||
|         this._meta = { ...meta, changeType: "split" } |         this._meta = { ...meta, changeType: "split" } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -59,7 +63,7 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|         const originalElement = <OsmWay>await OsmObject.DownloadObjectAsync(this.wayId) |         const originalElement = <OsmWay>await OsmObject.DownloadObjectAsync(this.wayId) | ||||||
|         const originalNodes = originalElement.nodes |         const originalNodes = originalElement.nodes | ||||||
| 
 | 
 | ||||||
|         // First, calculate splitpoints and remove points close to one another
 |         // First, calculate the splitpoints and remove points close to one another
 | ||||||
|         const splitInfo = this.CalculateSplitCoordinates(originalElement, this._toleranceInMeters) |         const splitInfo = this.CalculateSplitCoordinates(originalElement, this._toleranceInMeters) | ||||||
|         // Now we have a list with e.g.
 |         // Now we have a list with e.g.
 | ||||||
|         // [ { originalIndex: 0}, {originalIndex: 1, doSplit: true}, {originalIndex: 2}, {originalIndex: undefined, doSplit: true}, {originalIndex: 3}]
 |         // [ { originalIndex: 0}, {originalIndex: 1, doSplit: true}, {originalIndex: 2}, {originalIndex: undefined, doSplit: true}, {originalIndex: 3}]
 | ||||||
|  | @ -90,7 +94,7 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const changeDescription: ChangeDescription[] = [] |         const changeDescription: ChangeDescription[] = [] | ||||||
|         // Let's create the new points as needed
 |         // Let's create the new nodes as needed
 | ||||||
|         for (const element of splitInfo) { |         for (const element of splitInfo) { | ||||||
|             if (element.originalIndex >= 0) { |             if (element.originalIndex >= 0) { | ||||||
|                 continue |                 continue | ||||||
|  | @ -114,17 +118,21 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|         for (const wayPart of wayParts) { |         for (const wayPart of wayParts) { | ||||||
|             let isOriginal = wayPart === longest |             let isOriginal = wayPart === longest | ||||||
|             if (isOriginal) { |             if (isOriginal) { | ||||||
|                 // We change the actual element!
 |                 // We change the existing way
 | ||||||
|                 const nodeIds = wayPart.map((p) => p.originalIndex) |                 const nodeIds = wayPart.map((p) => p.originalIndex) | ||||||
|  |                 const newCoordinates = wayPart.map((p) => p.lngLat) | ||||||
|                 changeDescription.push({ |                 changeDescription.push({ | ||||||
|                     type: "way", |                     type: "way", | ||||||
|                     id: originalElement.id, |                     id: originalElement.id, | ||||||
|                     changes: { |                     changes: { | ||||||
|                         coordinates: wayPart.map((p) => p.lngLat), |                         coordinates: newCoordinates, | ||||||
|                         nodes: nodeIds, |                         nodes: nodeIds, | ||||||
|                     }, |                     }, | ||||||
|                     meta: this._meta, |                     meta: this._meta, | ||||||
|                 }) |                 }) | ||||||
|  |                 if (this._withNewCoordinates) { | ||||||
|  |                     this._withNewCoordinates(newCoordinates) | ||||||
|  |                 } | ||||||
|                 allWayIdsInOrder.push(originalElement.id) |                 allWayIdsInOrder.push(originalElement.id) | ||||||
|                 allWaysNodesInOrder.push(nodeIds) |                 allWaysNodesInOrder.push(nodeIds) | ||||||
|             } else { |             } else { | ||||||
|  | @ -141,6 +149,10 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|                     kv.push({ k: k, v: originalElement.tags[k] }) |                     kv.push({ k: k, v: originalElement.tags[k] }) | ||||||
|                 } |                 } | ||||||
|                 const nodeIds = wayPart.map((p) => p.originalIndex) |                 const nodeIds = wayPart.map((p) => p.originalIndex) | ||||||
|  |                 if (nodeIds.length <= 1) { | ||||||
|  |                     console.error("Got a segment with only one node - skipping") | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|                 changeDescription.push({ |                 changeDescription.push({ | ||||||
|                     type: "way", |                     type: "way", | ||||||
|                     id: id, |                     id: id, | ||||||
|  |  | ||||||
|  | @ -22,8 +22,10 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import { ElementStorage } from "../../Logic/ElementStorage" | import { ElementStorage } from "../../Logic/ElementStorage" | ||||||
| import BaseLayer from "../../Models/BaseLayer" | import BaseLayer from "../../Models/BaseLayer" | ||||||
| import FilteredLayer from "../../Models/FilteredLayer" | import FilteredLayer from "../../Models/FilteredLayer" | ||||||
|  | import BaseUIElement from "../BaseUIElement" | ||||||
|  | import { VariableUiElement } from "../Base/VariableUIElement" | ||||||
| 
 | 
 | ||||||
| export default class SplitRoadWizard extends Toggle { | export default class SplitRoadWizard extends Combine { | ||||||
|     // @ts-ignore
 |     // @ts-ignore
 | ||||||
|     private static splitLayerStyling = new LayerConfig( |     private static splitLayerStyling = new LayerConfig( | ||||||
|         split_point, |         split_point, | ||||||
|  | @ -63,6 +65,106 @@ export default class SplitRoadWizard extends Toggle { | ||||||
| 
 | 
 | ||||||
|         // Toggle variable between show split button and map
 |         // Toggle variable between show split button and map
 | ||||||
|         const splitClicked = new UIEventSource<boolean>(false) |         const splitClicked = new UIEventSource<boolean>(false) | ||||||
|  | 
 | ||||||
|  |         const leafletMap = new UIEventSource<BaseUIElement>( | ||||||
|  |             SplitRoadWizard.setupMapComponent(id, splitPoints, state) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         // Toggle between splitmap
 | ||||||
|  |         const splitButton = new SubtleButton( | ||||||
|  |             Svg.scissors_ui().SetStyle("height: 1.5rem; width: auto"), | ||||||
|  |             new Toggle( | ||||||
|  |                 t.splitAgain.Clone().SetClass("text-lg font-bold"), | ||||||
|  |                 t.inviteToSplit.Clone().SetClass("text-lg font-bold"), | ||||||
|  |                 hasBeenSplit | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         splitButton.onClick(() => { | ||||||
|  |             splitClicked.setData(true) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         // Only show the splitButton if logged in, else show login prompt
 | ||||||
|  |         const loginBtn = t.loginToSplit | ||||||
|  |             .Clone() | ||||||
|  |             .onClick(() => state.osmConnection.AttemptLogin()) | ||||||
|  |             .SetClass("login-button-friendly") | ||||||
|  |         const splitToggle = new Toggle(splitButton, loginBtn, state.osmConnection.isLoggedIn) | ||||||
|  | 
 | ||||||
|  |         // Save button
 | ||||||
|  |         const saveButton = new Button(t.split.Clone(), async () => { | ||||||
|  |             hasBeenSplit.setData(true) | ||||||
|  |             splitClicked.setData(false) | ||||||
|  |             const splitAction = new SplitAction( | ||||||
|  |                 id, | ||||||
|  |                 splitPoints.data.map((ff) => ff.feature.geometry.coordinates), | ||||||
|  |                 { | ||||||
|  |                     theme: state?.layoutToUse?.id, | ||||||
|  |                 }, | ||||||
|  |                 5, | ||||||
|  |                 (coordinates) => { | ||||||
|  |                     state.allElements.ContainingFeatures.get(id).geometry["coordinates"] = | ||||||
|  |                         coordinates | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |             await state.changes.applyAction(splitAction) | ||||||
|  |             // We throw away the old map and splitpoints, and create a new map from scratch
 | ||||||
|  |             splitPoints.setData([]) | ||||||
|  |             leafletMap.setData(SplitRoadWizard.setupMapComponent(id, splitPoints, state)) | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         saveButton.SetClass("btn btn-primary mr-3") | ||||||
|  |         const disabledSaveButton = new Button("Split", undefined) | ||||||
|  |         disabledSaveButton.SetClass("btn btn-disabled mr-3") | ||||||
|  |         // Only show the save button if there are split points defined
 | ||||||
|  |         const saveToggle = new Toggle( | ||||||
|  |             disabledSaveButton, | ||||||
|  |             saveButton, | ||||||
|  |             splitPoints.map((data) => data.length === 0) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         const cancelButton = Translations.t.general.cancel | ||||||
|  |             .Clone() // Not using Button() element to prevent full width button
 | ||||||
|  |             .SetClass("btn btn-secondary mr-3") | ||||||
|  |             .onClick(() => { | ||||||
|  |                 splitPoints.setData([]) | ||||||
|  |                 splitClicked.setData(false) | ||||||
|  |             }) | ||||||
|  | 
 | ||||||
|  |         cancelButton.SetClass("btn btn-secondary block") | ||||||
|  | 
 | ||||||
|  |         const splitTitle = new Title(t.splitTitle) | ||||||
|  | 
 | ||||||
|  |         const mapView = new Combine([ | ||||||
|  |             splitTitle, | ||||||
|  |             new VariableUiElement(leafletMap), | ||||||
|  |             new Combine([cancelButton, saveToggle]).SetClass("flex flex-row"), | ||||||
|  |         ]) | ||||||
|  |         mapView.SetClass("question") | ||||||
|  |         super([ | ||||||
|  |             Toggle.If(hasBeenSplit, () => | ||||||
|  |                 t.hasBeenSplit.Clone().SetClass("font-bold thanks block w-full") | ||||||
|  |             ), | ||||||
|  |             new Toggle(mapView, splitToggle, splitClicked), | ||||||
|  |         ]) | ||||||
|  |         this.dialogIsOpened = splitClicked | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static setupMapComponent( | ||||||
|  |         id: string, | ||||||
|  |         splitPoints: UIEventSource<{ feature: any; freshness: Date }[]>, | ||||||
|  |         state: { | ||||||
|  |             filteredLayers: UIEventSource<FilteredLayer[]> | ||||||
|  |             backgroundLayer: UIEventSource<BaseLayer> | ||||||
|  |             featureSwitchIsTesting: UIEventSource<boolean> | ||||||
|  |             featureSwitchIsDebugging: UIEventSource<boolean> | ||||||
|  |             featureSwitchShowAllQuestions: UIEventSource<boolean> | ||||||
|  |             osmConnection: OsmConnection | ||||||
|  |             featureSwitchUserbadge: UIEventSource<boolean> | ||||||
|  |             changes: Changes | ||||||
|  |             layoutToUse: LayoutConfig | ||||||
|  |             allElements: ElementStorage | ||||||
|  |         } | ||||||
|  |     ): BaseUIElement { | ||||||
|         // Load the road with given id on the minimap
 |         // Load the road with given id on the minimap
 | ||||||
|         const roadElement = state.allElements.ContainingFeatures.get(id) |         const roadElement = state.allElements.ContainingFeatures.get(id) | ||||||
| 
 | 
 | ||||||
|  | @ -96,7 +198,6 @@ export default class SplitRoadWizard extends Toggle { | ||||||
|             layerToShow: SplitRoadWizard.splitLayerStyling, |             layerToShow: SplitRoadWizard.splitLayerStyling, | ||||||
|             state, |             state, | ||||||
|         }) |         }) | ||||||
| 
 |  | ||||||
|         /** |         /** | ||||||
|          * Handles a click on the overleaf map. |          * Handles a click on the overleaf map. | ||||||
|          * Finds the closest intersection with the road and adds a point there, ready to confirm the cut. |          * Finds the closest intersection with the road and adds a point there, ready to confirm the cut. | ||||||
|  | @ -137,67 +238,6 @@ export default class SplitRoadWizard extends Toggle { | ||||||
|                 onMapClick([mouseEvent.latlng.lng, mouseEvent.latlng.lat]) |                 onMapClick([mouseEvent.latlng.lng, mouseEvent.latlng.lat]) | ||||||
|             }) |             }) | ||||||
|         ) |         ) | ||||||
| 
 |         return miniMap | ||||||
|         // Toggle between splitmap
 |  | ||||||
|         const splitButton = new SubtleButton( |  | ||||||
|             Svg.scissors_ui().SetStyle("height: 1.5rem; width: auto"), |  | ||||||
|             t.inviteToSplit.Clone().SetClass("text-lg font-bold") |  | ||||||
|         ) |  | ||||||
|         splitButton.onClick(() => { |  | ||||||
|             splitClicked.setData(true) |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         // Only show the splitButton if logged in, else show login prompt
 |  | ||||||
|         const loginBtn = t.loginToSplit |  | ||||||
|             .Clone() |  | ||||||
|             .onClick(() => state.osmConnection.AttemptLogin()) |  | ||||||
|             .SetClass("login-button-friendly") |  | ||||||
|         const splitToggle = new Toggle(splitButton, loginBtn, state.osmConnection.isLoggedIn) |  | ||||||
| 
 |  | ||||||
|         // Save button
 |  | ||||||
|         const saveButton = new Button(t.split.Clone(), () => { |  | ||||||
|             hasBeenSplit.setData(true) |  | ||||||
|             state.changes.applyAction( |  | ||||||
|                 new SplitAction( |  | ||||||
|                     id, |  | ||||||
|                     splitPoints.data.map((ff) => ff.feature.geometry.coordinates), |  | ||||||
|                     { |  | ||||||
|                         theme: state?.layoutToUse?.id, |  | ||||||
|                     } |  | ||||||
|                 ) |  | ||||||
|             ) |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         saveButton.SetClass("btn btn-primary mr-3") |  | ||||||
|         const disabledSaveButton = new Button("Split", undefined) |  | ||||||
|         disabledSaveButton.SetClass("btn btn-disabled mr-3") |  | ||||||
|         // Only show the save button if there are split points defined
 |  | ||||||
|         const saveToggle = new Toggle( |  | ||||||
|             disabledSaveButton, |  | ||||||
|             saveButton, |  | ||||||
|             splitPoints.map((data) => data.length === 0) |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         const cancelButton = Translations.t.general.cancel |  | ||||||
|             .Clone() // Not using Button() element to prevent full width button
 |  | ||||||
|             .SetClass("btn btn-secondary mr-3") |  | ||||||
|             .onClick(() => { |  | ||||||
|                 splitPoints.setData([]) |  | ||||||
|                 splitClicked.setData(false) |  | ||||||
|             }) |  | ||||||
| 
 |  | ||||||
|         cancelButton.SetClass("btn btn-secondary block") |  | ||||||
| 
 |  | ||||||
|         const splitTitle = new Title(t.splitTitle) |  | ||||||
| 
 |  | ||||||
|         const mapView = new Combine([ |  | ||||||
|             splitTitle, |  | ||||||
|             miniMap, |  | ||||||
|             new Combine([cancelButton, saveToggle]).SetClass("flex flex-row"), |  | ||||||
|         ]) |  | ||||||
|         mapView.SetClass("question") |  | ||||||
|         const confirm = new Toggle(mapView, splitToggle, splitClicked) |  | ||||||
|         super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit) |  | ||||||
|         this.dialogIsOpened = splitClicked |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -913,6 +913,7 @@ | ||||||
|         "inviteToSplit": "Split this road in smaller segments. This allows to give different properties to parts of the road.", |         "inviteToSplit": "Split this road in smaller segments. This allows to give different properties to parts of the road.", | ||||||
|         "loginToSplit": "You must be logged in to split a road", |         "loginToSplit": "You must be logged in to split a road", | ||||||
|         "split": "Split", |         "split": "Split", | ||||||
|  |         "splitAgain": "Split this road again", | ||||||
|         "splitTitle": "Choose on the map where the properties of this road change" |         "splitTitle": "Choose on the map where the properties of this road change" | ||||||
|     }, |     }, | ||||||
|     "translations": { |     "translations": { | ||||||
|  |  | ||||||
|  | @ -7594,7 +7594,7 @@ describe("GenerateCache", () => { | ||||||
|         } |         } | ||||||
|         mkdirSync(dir + "np-cache") |         mkdirSync(dir + "np-cache") | ||||||
|         initDownloads( |         initDownloads( | ||||||
|             "(nwr%5B%22amenity%22%3D%22toilets%22%5D%3Bnwr%5B%22amenity%22%3D%22parking%22%5D%3Bnwr%5B%22amenity%22%3D%22bench%22%5D%3Bnwr%5B%22id%22%3D%22location_track%22%5D%3Bnwr%5B%22id%22%3D%22gps%22%5D%3Bnwr%5B%22information%22%3D%22board%22%5D%3Bnwr%5B%22leisure%22%3D%22picnic_table%22%5D%3Bnwr%5B%22man_made%22%3D%22watermill%22%5D%3Bnwr%5B%22selected%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Ahome%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Alocation%22%3D%22yes%22%5D%3Bnwr%5B%22leisure%22%3D%22nature_reserve%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22boundary%22%3D%22protected_area%22%5D%5B%22protect_class%22!%3D%2298%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22information%22%3D%22visitor_centre%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22information%22%3D%22office%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*foot.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*hiking.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*bycicle.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*horse.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22leisure%22%3D%22bird_hide%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22amenity%22%3D%22drinking_water%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3Bnwr%5B%22drinking_water%22%3D%22yes%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3B)%3Bout%20body%3Bout%20meta%3B%3E%3Bout%20skel%20qt%3B" |             "(nwr%5B%22amenity%22%3D%22toilets%22%5D%3Bnwr%5B%22amenity%22%3D%22parking%22%5D%3Bnwr%5B%22amenity%22%3D%22bench%22%5D%3Bnwr%5B%22id%22%3D%22location_track%22%5D%3Bnwr%5B%22id%22%3D%22gps%22%5D%3Bnwr%5B%22information%22%3D%22board%22%5D%3Bnwr%5B%22leisure%22%3D%22picnic_table%22%5D%3Bnwr%5B%22selected%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Ahome%22%3D%22yes%22%5D%3Bnwr%5B%22user%3Alocation%22%3D%22yes%22%5D%3Bnwr%5B%22leisure%22%3D%22nature_reserve%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22boundary%22%3D%22protected_area%22%5D%5B%22protect_class%22!%3D%2298%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22information%22%3D%22visitor_centre%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22information%22%3D%22office%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*foot.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*hiking.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*bycicle.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22route%22~%22%5E(.*horse.*)%24%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22leisure%22%3D%22bird_hide%22%5D%5B%22operator%22~%22%5E(.*%5BnN%5Datuurpunt.*)%24%22%5D%3Bnwr%5B%22amenity%22%3D%22drinking_water%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3Bnwr%5B%22drinking_water%22%3D%22yes%22%5D%5B%22man_made%22!%3D%22reservoir_covered%22%5D%5B%22access%22!%3D%22permissive%22%5D%5B%22access%22!%3D%22private%22%5D%3B)%3Bout%20body%3Bout%20meta%3B%3E%3Bout%20skel%20qt%3B" | ||||||
|         ) |         ) | ||||||
|         await main([ |         await main([ | ||||||
|             "natuurpunt", |             "natuurpunt", | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue