Merge branch 'develop'

This commit is contained in:
Pieter Vander Vennet 2022-12-25 23:17:38 +01:00
commit 217c103adc
75 changed files with 4344 additions and 2521 deletions

30
Docs/Hotkeys.md Normal file
View file

@ -0,0 +1,30 @@
Hotkeys
=========
## Table of contents
1. [Hotkeys](#hotkeys)
MapComplete supports the following keys:
Key combination | Action
----------------- | --------
B | Opens the Background, layers and filters panel
Escape | Close the sidebar
L | Pan the map to the current location or zoom the map to the current location. Requests geopermission
M | Switch to a background layer of category map
O | Switch to a background layer of category osmbasedmap
P | Switch to a background layer of category photo
ctrl+F | Select the search bar to search locations
shift+O | Switch to default Mapnik-OpenStreetMap background
This document is autogenerated from

View file

@ -1,230 +1,135 @@
import { Store, UIEventSource } from "../UIEventSource"
import Svg from "../../Svg"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { VariableUiElement } from "../../UI/Base/VariableUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { QueryParameters } from "../Web/QueryParameters"
import { BBox } from "../BBox"
import Constants from "../../Models/Constants"
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"
import { GeoLocationPointProperties, GeoLocationState } from "../State/GeoLocationState"
import State from "../../State"
import { UIEventSource } from "../UIEventSource"
export interface GeoLocationPointProperties {
id: "gps"
"user:location": "yes"
date: string
latitude: number
longitude: number
speed: number
accuracy: number
heading: number
altitude: number
}
/**
* The geolocation-handler takes a map-location and a geolocation state.
* It'll move the map as appropriate given the state of the geolocation-API
* It will also copy the geolocation into the appropriate FeatureSource to display on the map
*/
export default class GeoLocationHandler {
public readonly geolocationState: GeoLocationState
private readonly _state: State
public readonly mapHasMoved: UIEventSource<boolean> = new UIEventSource<boolean>(false)
export default class GeoLocationHandler extends VariableUiElement {
private readonly currentLocation?: SimpleFeatureSource
/**
* Wether or not the geolocation is active, aka the user requested the current location
*/
private readonly _isActive: UIEventSource<boolean>
/**
* Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user
*/
private readonly _isLocked: UIEventSource<boolean>
/**
* The callback over the permission API
* @private
*/
private readonly _permission: UIEventSource<string>
/**
* Literally: _currentGPSLocation.data != undefined
* @private
*/
private readonly _hasLocation: Store<boolean>
private readonly _currentGPSLocation: UIEventSource<GeolocationCoordinates>
/**
* Kept in order to update the marker
* @private
*/
private readonly _leafletMap: UIEventSource<L.Map>
/**
* The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs
*/
private _lastUserRequest: UIEventSource<Date>
/**
* A small flag on localstorage. If the user previously granted the geolocation, it will be set.
* On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions.
*
* Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately.
* If the user denies the geolocation this time, we unset this flag
* @private
*/
private readonly _previousLocationGrant: UIEventSource<string>
private readonly _layoutToUse: LayoutConfig
constructor(state: {
selectedElement: UIEventSource<any>
currentUserLocation?: SimpleFeatureSource
leafletMap: UIEventSource<any>
layoutToUse: LayoutConfig
featureSwitchGeolocation: UIEventSource<boolean>
}) {
const currentGPSLocation = new UIEventSource<GeolocationCoordinates>(
undefined,
"GPS-coordinate"
)
const leafletMap = state.leafletMap
const initedAt = new Date()
let autozoomDone = false
const hasLocation = currentGPSLocation.map((location) => location !== undefined)
const previousLocationGrant = LocalStorageSource.Get("geolocation-permissions")
const isActive = new UIEventSource<boolean>(false)
const isLocked = new UIEventSource<boolean>(false)
const permission = new UIEventSource<string>("")
const lastClick = new UIEventSource<Date>(undefined)
const lastClickWithinThreeSecs = lastClick.map((lastClick) => {
if (lastClick === undefined) {
return false
constructor(
geolocationState: GeoLocationState,
state: State // { locationControl: UIEventSource<Loc>, selectedElement: UIEventSource<any>, leafletMap?: UIEventSource<any> })
) {
this.geolocationState = geolocationState
this._state = state
const mapLocation = state.locationControl
// Did an interaction move the map?
let self = this
let initTime = new Date()
mapLocation.addCallbackD((_) => {
if (new Date().getTime() - initTime.getTime() < 250) {
return
}
const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000
return timeDiff <= 3
self.mapHasMoved.setData(true)
return true // Unsubscribe
})
const latLonGiven =
QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon")
const willFocus = lastClick.map((lastUserRequest) => {
const timeDiffInited = (new Date().getTime() - initedAt.getTime()) / 1000
if (!latLonGiven && !autozoomDone && timeDiffInited < Constants.zoomToLocationTimeout) {
return true
}
if (lastUserRequest === undefined) {
return false
}
const timeDiff = (new Date().getTime() - lastUserRequest.getTime()) / 1000
return timeDiff <= Constants.zoomToLocationTimeout
})
const latLonGivenViaUrl =
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon")
if (latLonGivenViaUrl) {
// The URL counts as a 'user interaction'
this.mapHasMoved.setData(true)
}
lastClick.addCallbackAndRunD((_) => {
window.setTimeout(() => {
if (lastClickWithinThreeSecs.data || willFocus.data) {
lastClick.ping()
this.geolocationState.currentGPSLocation.addCallbackAndRunD((newLocation) => {
const timeSinceLastRequest =
(new Date().getTime() - geolocationState.requestMoment.data?.getTime() ?? 0) / 1000
if (!this.mapHasMoved.data) {
// The map hasn't moved yet; we received our first coordinates, so let's move there!
console.log(
"Moving the map to an initial location; time since last request is",
timeSinceLastRequest
)
if (timeSinceLastRequest < Constants.zoomToLocationTimeout) {
self.MoveMapToCurrentLocation()
}
}, 500)
}
if (this.geolocationState.isLocked.data) {
// Jup, the map is locked to the bound location: move automatically
self.MoveMapToCurrentLocation()
return
}
})
super(
hasLocation.map(
(hasLocationData) => {
if (permission.data === "denied") {
return Svg.location_refused_svg()
}
if (!isActive.data) {
return Svg.location_empty_svg()
}
if (!hasLocationData) {
// Position not yet found but we are active: we spin to indicate activity
// If will focus is active too, we indicate this differently
const icon = willFocus.data ? Svg.location_svg() : Svg.location_empty_svg()
icon.SetStyle("animation: spin 4s linear infinite;")
return icon
}
if (isLocked.data) {
return Svg.location_locked_svg()
}
if (lastClickWithinThreeSecs.data) {
return Svg.location_unlocked_svg()
}
// We have a location, so we show a dot in the center
return Svg.location_svg()
},
[isActive, isLocked, permission, lastClickWithinThreeSecs, willFocus]
)
)
this.SetClass("mapcontrol")
this._isActive = isActive
this._isLocked = isLocked
this._permission = permission
this._previousLocationGrant = previousLocationGrant
this._currentGPSLocation = currentGPSLocation
this._leafletMap = leafletMap
this._layoutToUse = state.layoutToUse
this._hasLocation = hasLocation
this._lastUserRequest = lastClick
const self = this
const currentPointer = this._isActive.map(
(isActive) => {
if (isActive && !self._hasLocation.data) {
return "cursor-wait"
}
return "cursor-pointer"
},
[this._hasLocation]
)
currentPointer.addCallbackAndRun((pointerClass) => {
self.RemoveClass("cursor-wait")
self.RemoveClass("cursor-pointer")
self.SetClass(pointerClass)
})
this.onClick(() => {
/*
* If the previous click was within 3 seconds (and we have an active location), then we lock to the location
*/
if (self._hasLocation.data) {
if (isLocked.data) {
isLocked.setData(false)
} else if (lastClick.data !== undefined) {
const timeDiff = (new Date().getTime() - lastClick.data.getTime()) / 1000
if (timeDiff <= 3) {
isLocked.setData(true)
lastClick.setData(undefined)
} else {
lastClick.setData(new Date())
}
geolocationState.isLocked.map(
(isLocked) => {
if (isLocked) {
state.leafletMap?.data?.dragging?.disable()
} else {
lastClick.setData(new Date())
state.leafletMap?.data?.dragging?.enable()
}
},
[state.leafletMap]
)
this.CopyGeolocationIntoMapstate()
}
/**
* Move the map to the GPS-location, except:
* - If there is a selected element
* - The location is out of the locked bound
* - The GPS-location iss NULL-island
* @constructor
*/
public MoveMapToCurrentLocation() {
const newLocation = this.geolocationState.currentGPSLocation.data
const mapLocation = this._state.locationControl
const state = this._state
// We got a new location.
// Do we move the map to it?
if (state.selectedElement.data !== undefined) {
// Nope, there is something selected, so we don't move to the current GPS-location
return
}
if (newLocation.latitude === 0 && newLocation.longitude === 0) {
console.debug("Not moving to GPS-location: it is null island")
return
}
// We check that the GPS location is not out of bounds
const bounds = state.layoutToUse.lockLocation
if (bounds && bounds !== true) {
// B is an array with our lock-location
const inRange = new BBox(bounds).contains([newLocation.longitude, newLocation.latitude])
if (!inRange) {
return
}
}
self.init(true, true)
mapLocation.setData({
zoom: mapLocation.data.zoom,
lon: newLocation.longitude,
lat: newLocation.latitude,
})
this.mapHasMoved.setData(true)
}
const doAutoZoomToLocation =
!latLonGiven &&
state.featureSwitchGeolocation.data &&
state.selectedElement.data !== undefined
this.init(false, doAutoZoomToLocation)
isLocked.addCallbackAndRunD((isLocked) => {
if (isLocked) {
leafletMap.data?.dragging?.disable()
} else {
leafletMap.data?.dragging?.enable()
private CopyGeolocationIntoMapstate() {
const state = this._state
this.geolocationState.currentGPSLocation.addCallbackAndRun((location) => {
if (location === undefined) {
state.currentUserLocation?.features?.setData([])
return
}
})
this.currentLocation = state.currentUserLocation
this._currentGPSLocation.addCallback((location) => {
self._previousLocationGrant.setData("granted")
const feature = {
type: "Feature",
properties: <GeoLocationPointProperties>{
id: "gps",
"user:location": "yes",
date: new Date().toISOString(),
latitude: location.latitude,
longitude: location.longitude,
speed: location.speed,
accuracy: location.accuracy,
heading: location.heading,
altitude: location.altitude,
...location,
},
geometry: {
type: "Point",
@ -232,164 +137,7 @@ export default class GeoLocationHandler extends VariableUiElement {
},
}
self.currentLocation?.features?.setData([{ feature, freshness: new Date() }])
if (willFocus.data) {
console.log("Zooming to user location: willFocus is set")
lastClick.setData(undefined)
autozoomDone = true
self.MoveToCurrentLocation(16)
} else if (self._isLocked.data) {
self.MoveToCurrentLocation()
}
state.currentUserLocation?.features?.setData([{ feature, freshness: new Date() }])
})
}
private init(askPermission: boolean, zoomToLocation: boolean) {
const self = this
if (self._isActive.data) {
self.MoveToCurrentLocation(16)
return
}
if (typeof navigator === "undefined") {
return
}
try {
navigator?.permissions?.query({ name: "geolocation" })?.then(function (status) {
console.log("Geolocation permission is ", status.state)
if (status.state === "granted") {
self.StartGeolocating(zoomToLocation)
}
self._permission.setData(status.state)
status.onchange = function () {
self._permission.setData(status.state)
}
})
} catch (e) {
console.error(e)
}
if (askPermission) {
self.StartGeolocating(zoomToLocation)
} else if (this._previousLocationGrant.data === "granted") {
this._previousLocationGrant.setData("")
self.StartGeolocating(zoomToLocation)
}
}
/**
* Moves to the currently loaded location.
*
* // Should move to any location
* let resultingLocation = undefined
* let resultingzoom = 1
* const state = {
* selectedElement: new UIEventSource<any>(undefined);
* currentUserLocation: undefined ,
* leafletMap: new UIEventSource<any>({getZoom: () => resultingzoom; setView: (loc, zoom) => {resultingLocation = loc; resultingzoom = zoom}),
* layoutToUse: new LayoutConfig(<any>{
* id: 'test',
* title: {"en":"test"}
* description: "A testing theme",
* layers: []
* }),
* featureSwitchGeolocation : new UIEventSource<boolean>(true)
* }
* const handler = new GeoLocationHandler(state)
* handler._currentGPSLocation.setData(<any> {latitude : 51.3, longitude: 4.1})
* handler.MoveToCurrentLocation()
* resultingLocation // => [51.3, 4.1]
* handler._currentGPSLocation.setData(<any> {latitude : 60, longitude: 60) // out of bounds
* handler.MoveToCurrentLocation()
* resultingLocation // => [60, 60]
*
* // should refuse to move if out of bounds
* let resultingLocation = undefined
* let resultingzoom = 1
* const state = {
* selectedElement: new UIEventSource<any>(undefined);
* currentUserLocation: undefined ,
* leafletMap: new UIEventSource<any>({getZoom: () => resultingzoom; setView: (loc, zoom) => {resultingLocation = loc; resultingzoom = zoom}),
* layoutToUse: new LayoutConfig(<any>{
* id: 'test',
* title: {"en":"test"}
* "lockLocation": [ [ 2.1, 50.4], [6.4, 51.54 ]],
* description: "A testing theme",
* layers: []
* }),
* featureSwitchGeolocation : new UIEventSource<boolean>(true)
* }
* const handler = new GeoLocationHandler(state)
* handler._currentGPSLocation.setData(<any> {latitude : 51.3, longitude: 4.1})
* handler.MoveToCurrentLocation()
* resultingLocation // => [51.3, 4.1]
* handler._currentGPSLocation.setData(<any> {latitude : 60, longitude: 60) // out of bounds
* handler.MoveToCurrentLocation()
* resultingLocation // => [51.3, 4.1]
*/
private MoveToCurrentLocation(targetZoom?: number) {
const location = this._currentGPSLocation.data
this._lastUserRequest.setData(undefined)
if (
this._currentGPSLocation.data.latitude === 0 &&
this._currentGPSLocation.data.longitude === 0
) {
console.debug("Not moving to GPS-location: it is null island")
return
}
// We check that the GPS location is not out of bounds
const b = this._layoutToUse.lockLocation
let inRange = true
if (b) {
if (b !== true) {
// B is an array with our locklocation
inRange = new BBox(b).contains([location.longitude, location.latitude])
}
}
if (!inRange) {
console.log("Not zooming to GPS location: out of bounds", b, location)
} else {
const currentZoom = this._leafletMap.data.getZoom()
this._leafletMap.data.setView(
[location.latitude, location.longitude],
Math.max(targetZoom ?? 0, currentZoom)
)
}
}
private StartGeolocating(zoomToGPS = true) {
const self = this
this._lastUserRequest.setData(zoomToGPS ? new Date() : new Date(0))
if (self._permission.data === "denied") {
self._previousLocationGrant.setData("")
self._isActive.setData(false)
return ""
}
if (this._currentGPSLocation.data !== undefined) {
this.MoveToCurrentLocation(16)
}
if (self._isActive.data) {
return
}
self._isActive.setData(true)
navigator.geolocation.watchPosition(
function (position) {
self._currentGPSLocation.setData(position.coords)
},
function () {
console.warn("Could not get location with navigator.geolocation")
},
{
enableHighAccuracy: true,
}
)
}
}

View file

@ -1,8 +1,6 @@
import * as L from "leaflet"
import { UIEventSource } from "../UIEventSource"
import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"
import FilteredLayer from "../../Models/FilteredLayer"
import Constants from "../../Models/Constants"
import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"
import BaseUIElement from "../../UI/BaseUIElement"
/**
@ -10,70 +8,16 @@ import BaseUIElement from "../../UI/BaseUIElement"
* Shows the given uiToShow-element in the messagebox
*/
export default class StrayClickHandler {
private _lastMarker
constructor(
public static construct = (
state: {
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
selectedElement: UIEventSource<string>
filteredLayers: UIEventSource<FilteredLayer[]>
leafletMap: UIEventSource<L.Map>
leafletMap: UIEventSource<any>
},
uiToShow: ScrollableFullScreen,
iconToShow: BaseUIElement
) {
const self = this
const leafletMap = state.leafletMap
state.filteredLayers.data.forEach((filteredLayer) => {
filteredLayer.isDisplayed.addCallback((isEnabled) => {
if (isEnabled && self._lastMarker && leafletMap.data !== undefined) {
// When a layer is activated, we remove the 'last click location' in order to force the user to reclick
// This reclick might be at a location where a feature now appeared...
state.leafletMap.data.removeLayer(self._lastMarker)
}
})
})
state.LastClickLocation.addCallback(function (lastClick) {
if (self._lastMarker !== undefined) {
state.leafletMap.data?.removeLayer(self._lastMarker)
}
if (lastClick === undefined) {
return
}
state.selectedElement.setData(undefined)
const clickCoor: [number, number] = [lastClick.lat, lastClick.lon]
self._lastMarker = L.marker(clickCoor, {
icon: L.divIcon({
html: iconToShow.ConstructElement(),
iconSize: [50, 50],
iconAnchor: [25, 50],
popupAnchor: [0, -45],
}),
})
self._lastMarker.addTo(leafletMap.data)
self._lastMarker.on("click", () => {
if (leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints) {
leafletMap.data.flyTo(
clickCoor,
Constants.userJourney.minZoomLevelToAddNewPoints
)
return
}
uiToShow.Activate()
})
})
state.selectedElement.addCallback(() => {
if (self._lastMarker !== undefined) {
leafletMap.data.removeLayer(self._lastMarker)
this._lastMarker = undefined
}
})
) => {
return undefined
}
}

View file

@ -16,6 +16,7 @@ export default class SplitAction extends OsmChangeAction {
private readonly _splitPointsCoordinates: [number, number][] // lon, lat
private _meta: { theme: string; changeType: "split" }
private _toleranceInMeters: number
private _withNewCoordinates: (coordinates: [number, number][]) => void
/**
* Create a changedescription for splitting a point.
@ -24,17 +25,20 @@ export default class SplitAction extends OsmChangeAction {
* @param splitPointCoordinates: lon, lat
* @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 withNewCoordinates: an optional callback which will leak the new coordinates of the original way
*/
constructor(
wayId: string,
splitPointCoordinates: [number, number][],
meta: { theme: string },
toleranceInMeters = 5
toleranceInMeters = 5,
withNewCoordinates?: (coordinates: [number, number][]) => void
) {
super(wayId, true)
this.wayId = wayId
this._splitPointsCoordinates = splitPointCoordinates
this._toleranceInMeters = toleranceInMeters
this._withNewCoordinates = withNewCoordinates
this._meta = { ...meta, changeType: "split" }
}
@ -59,7 +63,7 @@ export default class SplitAction extends OsmChangeAction {
const originalElement = <OsmWay>await OsmObject.DownloadObjectAsync(this.wayId)
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)
// Now we have a list with e.g.
// [ { 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[] = []
// Let's create the new points as needed
// Let's create the new nodes as needed
for (const element of splitInfo) {
if (element.originalIndex >= 0) {
continue
@ -114,17 +118,21 @@ export default class SplitAction extends OsmChangeAction {
for (const wayPart of wayParts) {
let isOriginal = wayPart === longest
if (isOriginal) {
// We change the actual element!
// We change the existing way
const nodeIds = wayPart.map((p) => p.originalIndex)
const newCoordinates = wayPart.map((p) => p.lngLat)
changeDescription.push({
type: "way",
id: originalElement.id,
changes: {
coordinates: wayPart.map((p) => p.lngLat),
coordinates: newCoordinates,
nodes: nodeIds,
},
meta: this._meta,
})
if (this._withNewCoordinates) {
this._withNewCoordinates(newCoordinates)
}
allWayIdsInOrder.push(originalElement.id)
allWaysNodesInOrder.push(nodeIds)
} else {
@ -141,6 +149,10 @@ export default class SplitAction extends OsmChangeAction {
kv.push({ k: k, v: originalElement.tags[k] })
}
const nodeIds = wayPart.map((p) => p.originalIndex)
if (nodeIds.length <= 1) {
console.error("Got a segment with only one node - skipping")
continue
}
changeDescription.push({
type: "way",
id: id,

View file

@ -8,7 +8,7 @@ import { LocalStorageSource } from "../Web/LocalStorageSource"
import SimpleMetaTagger from "../SimpleMetaTagger"
import FeatureSource from "../FeatureSource/FeatureSource"
import { ElementStorage } from "../ElementStorage"
import { GeoLocationPointProperties } from "../Actors/GeoLocationHandler"
import { GeoLocationPointProperties } from "../State/GeoLocationState"
import { GeoOperations } from "../GeoOperations"
import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler"
import { OsmConnection } from "./OsmConnection"

View file

@ -0,0 +1,144 @@
import { UIEventSource } from "../UIEventSource"
import { LocalStorageSource } from "../Web/LocalStorageSource"
type GeolocationState = "prompt" | "requested" | "granted" | "denied"
export interface GeoLocationPointProperties extends GeolocationCoordinates {
id: "gps"
"user:location": "yes"
date: string
}
/**
* An abstract representation of the current state of the geolocation.
*
*
*/
export class GeoLocationState {
/**
* What do we know about the current state of having access to the GPS?
* If 'prompt', then we just started and didn't request access yet
* 'requested' means the user tapped the 'locate me' button at least once
* 'granted' means that it is granted
* 'denied' means that we don't have access
*
*/
public readonly permission: UIEventSource<GeolocationState> = new UIEventSource("prompt")
public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined)
/**
* If true: the map will center (and re-center) to this location
*/
public readonly isLocked: UIEventSource<boolean> = new UIEventSource<boolean>(false)
public readonly currentGPSLocation: UIEventSource<GeolocationCoordinates | undefined> =
new UIEventSource<GeolocationCoordinates | undefined>(undefined)
/**
* A small flag on localstorage. If the user previously granted the geolocation, it will be set.
* On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions.
*
* Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately.
* If the user denies the geolocation this time, we unset this flag
* @private
*/
private readonly _previousLocationGrant: UIEventSource<"true" | "false"> = <any>(
LocalStorageSource.Get("geolocation-permissions")
)
/**
* Used to detect a permission retraction
*/
private readonly _grantedThisSession: UIEventSource<boolean> = new UIEventSource<boolean>(false)
constructor() {
const self = this
this.permission.addCallbackAndRunD(async (state) => {
if (state === "granted") {
self._previousLocationGrant.setData("true")
self._grantedThisSession.setData(true)
}
if (state === "prompt" && self._grantedThisSession.data) {
// This is _really_ weird: we had a grant earlier, but it's 'prompt' now?
// This means that the rights have been revoked again!
// self.permission.setData("denied")
self._previousLocationGrant.setData("false")
self.permission.setData("denied")
self.currentGPSLocation.setData(undefined)
console.warn("Detected a downgrade in permissions!")
}
if (state === "denied") {
self._previousLocationGrant.setData("false")
}
})
console.log("Previous location grant:", this._previousLocationGrant.data)
if (this._previousLocationGrant.data === "true") {
// A previous visit successfully granted permission. Chance is high that we are allowed to use it again!
// We set the flag to false again. If the user only wanted to share their location once, we are not gonna keep bothering them
this._previousLocationGrant.setData("false")
console.log("Requesting access to GPS as this was previously granted")
this.requestPermission()
}
window["geolocation_state"] = this
}
/**
* Installs the listener for updates
* @private
*/
private async startWatching() {
const self = this
navigator.geolocation.watchPosition(
function (position) {
self.currentGPSLocation.setData(position.coords)
self._previousLocationGrant.setData("true")
},
function () {
console.warn("Could not get location with navigator.geolocation")
},
{
enableHighAccuracy: true,
}
)
}
/**
* Requests the user to allow access to their position.
* When granted, will be written to the 'geolocationState'.
* This class will start watching
*/
public requestPermission() {
if (typeof navigator === "undefined") {
// Not compatible with this browser
this.permission.setData("denied")
return
}
if (this.permission.data !== "prompt" && this.permission.data !== "requested") {
// If the user denies the first prompt, revokes the deny and then tries again, we have to run the flow as well
// Hence that we continue the flow if it is "requested"
return
}
this.requestMoment.setData(new Date())
this.permission.setData("requested")
try {
navigator?.permissions
?.query({ name: "geolocation" })
.then((status) => {
console.log("Status update: received geolocation permission is ", status.state)
this.permission.setData(status.state)
const self = this
status.onchange = function () {
self.permission.setData(status.state)
}
this.permission.setData("requested")
// We _must_ call 'startWatching', as that is the actual trigger for the popup...
self.startWatching()
})
.catch((e) => console.error("Could not get geopermission", e))
} catch (e) {
console.error("Could not get permission:", e)
}
}
}

View file

@ -1,7 +1,7 @@
import { Utils } from "../Utils"
export default class Constants {
public static vNumber = "0.25.0"
public static vNumber = "0.25.2"
public static ImgurApiKey = "7070e7167f0a25a"
public static readonly mapillary_client_token_v4 =

View file

@ -260,7 +260,7 @@ export interface LayerConfigJson {
/**
* The type of background picture
*/
preferredBackground:
preferredBackground?:
| "osmbasedmap"
| "photo"
| "historicphoto"

124
UI/Base/Hotkeys.ts Normal file
View file

@ -0,0 +1,124 @@
import { Utils } from "../../Utils"
import Combine from "./Combine"
import BaseUIElement from "../BaseUIElement"
import Title from "./Title"
import Table from "./Table"
import { UIEventSource } from "../../Logic/UIEventSource"
import { VariableUiElement } from "./VariableUIElement"
import doc = Mocha.reporters.doc
export default class Hotkeys {
private static readonly _docs: UIEventSource<
{
key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean }
documentation: string
}[]
> = new UIEventSource<
{
key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean }
documentation: string
}[]
>([])
private static textElementSelected(): boolean {
console.log(document.activeElement)
return document?.activeElement?.tagName?.toLowerCase() === "input"
}
public static RegisterHotkey(
key: (
| {
ctrl: string
}
| {
shift: string
}
| {
alt: string
}
| {
nomod: string
}
) & {
onUp?: boolean
},
documentation: string,
action: () => void
) {
const type = key["onUp"] ? "keyup" : "keypress"
let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"]
if (keycode.length == 1) {
keycode = keycode.toLowerCase()
if (key["shift"] !== undefined) {
keycode = keycode.toUpperCase()
}
}
this._docs.data.push({ key, documentation })
this._docs.ping()
if (Utils.runningFromConsole) {
return
}
if (key["ctrl"] !== undefined) {
document.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.key === keycode) {
action()
event.preventDefault()
}
})
} else if (key["shift"] !== undefined) {
document.addEventListener(type, function (event) {
if (Hotkeys.textElementSelected()) {
// A text element is selected, we don't do anything special
return
}
if (event.shiftKey && event.key === keycode) {
action()
event.preventDefault()
}
})
} else if (key["alt"] !== undefined) {
document.addEventListener(type, function (event) {
if (event.altKey && event.key === keycode) {
action()
event.preventDefault()
}
})
} else if (key["nomod"] !== undefined) {
document.addEventListener(type, function (event) {
if (Hotkeys.textElementSelected()) {
// A text element is selected, we don't do anything special
return
}
if (event.key === keycode) {
action()
event.preventDefault()
}
})
}
}
static generateDocumentation(): BaseUIElement {
return new Combine([
new Title("Hotkeys", 1),
"MapComplete supports the following keys:",
new Table(
["Key combination", "Action"],
Hotkeys._docs.data
.map(({ key, documentation }) => {
const modifiers = Object.keys(key).filter(
(k) => k !== "nomod" && k !== "onUp"
)
const keycode: string =
key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"]
modifiers.push(keycode)
return [modifiers.join("+"), documentation]
})
.sort()
),
])
}
static generateDocumentationDynamic(): BaseUIElement {
return new VariableUiElement(Hotkeys._docs.map((_) => Hotkeys.generateDocumentation()))
}
}

View file

@ -14,7 +14,83 @@ import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
import AvailableBaseLayersImplementation from "../../Logic/Actors/AvailableBaseLayersImplementation"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import ShowDataLayerImplementation from "../ShowDataLayer/ShowDataLayerImplementation"
import FilteredLayer from "../../Models/FilteredLayer"
import ScrollableFullScreen from "./ScrollableFullScreen"
import Constants from "../../Models/Constants"
import StrayClickHandler from "../../Logic/Actors/StrayClickHandler"
/**
* The stray-click-hanlders adds a marker to the map if no feature was clicked.
* Shows the given uiToShow-element in the messagebox
*/
export class StrayClickHandlerImplementation {
private _lastMarker
constructor(
state: {
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
selectedElement: UIEventSource<string>
filteredLayers: UIEventSource<FilteredLayer[]>
leafletMap: UIEventSource<L.Map>
},
uiToShow: ScrollableFullScreen,
iconToShow: BaseUIElement
) {
const self = this
const leafletMap = state.leafletMap
state.filteredLayers.data.forEach((filteredLayer) => {
filteredLayer.isDisplayed.addCallback((isEnabled) => {
if (isEnabled && self._lastMarker && leafletMap.data !== undefined) {
// When a layer is activated, we remove the 'last click location' in order to force the user to reclick
// This reclick might be at a location where a feature now appeared...
state.leafletMap.data.removeLayer(self._lastMarker)
}
})
})
state.LastClickLocation.addCallback(function (lastClick) {
if (self._lastMarker !== undefined) {
state.leafletMap.data?.removeLayer(self._lastMarker)
}
if (lastClick === undefined) {
return
}
state.selectedElement.setData(undefined)
const clickCoor: [number, number] = [lastClick.lat, lastClick.lon]
self._lastMarker = L.marker(clickCoor, {
icon: L.divIcon({
html: iconToShow.ConstructElement(),
iconSize: [50, 50],
iconAnchor: [25, 50],
popupAnchor: [0, -45],
}),
})
self._lastMarker.addTo(leafletMap.data)
self._lastMarker.on("click", () => {
if (leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints) {
leafletMap.data.flyTo(
clickCoor,
Constants.userJourney.minZoomLevelToAddNewPoints
)
return
}
uiToShow.Activate()
})
})
state.selectedElement.addCallback(() => {
if (self._lastMarker !== undefined) {
leafletMap.data.removeLayer(self._lastMarker)
this._lastMarker = undefined
}
})
}
}
export default class MinimapImplementation extends BaseUIElement implements MinimapObj {
private static _nextId = 0
public readonly leafletMap: UIEventSource<Map>
@ -53,6 +129,18 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
AvailableBaseLayers.implement(new AvailableBaseLayersImplementation())
Minimap.createMiniMap = (options) => new MinimapImplementation(options)
ShowDataLayer.actualContstructor = (options) => new ShowDataLayerImplementation(options)
StrayClickHandler.construct = (
state: {
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
selectedElement: UIEventSource<string>
filteredLayers: UIEventSource<FilteredLayer[]>
leafletMap: UIEventSource<L.Map>
},
uiToShow: ScrollableFullScreen,
iconToShow: BaseUIElement
) => {
return new StrayClickHandlerImplementation(state, uiToShow, iconToShow)
}
}
public installBounds(factor: number | BBox, showRange?: boolean) {

View file

@ -5,6 +5,7 @@ import { UIEventSource } from "../../Logic/UIEventSource"
import Hash from "../../Logic/Web/Hash"
import BaseUIElement from "../BaseUIElement"
import Title from "./Title"
import Hotkeys from "./Hotkeys"
/**
*
@ -82,12 +83,11 @@ export default class ScrollableFullScreen {
}
private static initEmpty(): FixedUiElement {
document.addEventListener("keyup", function (event) {
if (event.code === "Escape") {
ScrollableFullScreen.collapse()
event.preventDefault()
}
})
Hotkeys.RegisterHotkey(
{ nomod: "Escape", onUp: true },
"Close the sidebar",
ScrollableFullScreen.collapse
)
return new FixedUiElement("")
}
@ -117,7 +117,7 @@ export default class ScrollableFullScreen {
this._fullscreencomponent.AttachTo("fullscreen")
const fs = document.getElementById("fullscreen")
ScrollableFullScreen._currentlyOpen = this
fs.classList.remove("hidden")
fs?.classList?.remove("hidden")
}
private BuildComponent(title: BaseUIElement, content: BaseUIElement): BaseUIElement {

View file

@ -24,6 +24,10 @@ export default abstract class BaseUIElement {
AttachTo(divId: string) {
let element = document.getElementById(divId)
if (element === null) {
if (Utils.runningFromConsole) {
this.ConstructElement()
return
}
throw "SEVERE: could not attach UIElement to " + divId
}

View file

@ -7,6 +7,7 @@ import BaseLayer from "../../Models/BaseLayer"
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
import BaseUIElement from "../BaseUIElement"
import { GeoOperations } from "../../Logic/GeoOperations"
import Hotkeys from "../Base/Hotkeys"
class SingleLayerSelectionButton extends Toggle {
public readonly activate: () => void
@ -48,13 +49,13 @@ class SingleLayerSelectionButton extends Toggle {
let toggle: BaseUIElement = new Toggle(
selected,
unselected,
options.currentBackground.map((bg) => bg.category === options.preferredType)
options.currentBackground.map((bg) => bg?.category === options.preferredType)
)
super(
toggle,
undefined,
available.map((av) => av.category === options.preferredType)
available.map((av) => av?.category === options.preferredType)
)
/**
@ -174,6 +175,7 @@ export default class BackgroundMapSwitch extends Combine {
options?: {
preferredCategory?: string
allowedCategories?: ("osmbasedmap" | "photo" | "map")[]
enableHotkeys?: boolean
}
) {
const allowedCategories = options?.allowedCategories ?? ["osmbasedmap", "photo", "map"]
@ -183,7 +185,7 @@ export default class BackgroundMapSwitch extends Combine {
let activatePrevious: () => void = undefined
for (const category of allowedCategories) {
let preferredLayer = undefined
if (previousLayer.category === category) {
if (previousLayer?.category === category) {
preferredLayer = previousLayer
}
@ -198,6 +200,16 @@ export default class BackgroundMapSwitch extends Combine {
if (category === options?.preferredCategory) {
button.activate()
}
if (options?.enableHotkeys) {
Hotkeys.RegisterHotkey(
{ nomod: category.charAt(0).toUpperCase() },
"Switch to a background layer of category " + category,
() => {
button.activate()
}
)
}
buttons.push(button)
}

View file

@ -26,7 +26,7 @@ export class DownloadPanel extends Toggle {
currentBounds: UIEventSource<BBox>
}) {
const t = Translations.t.general.download
const name = State.state.layoutToUse.id
const name = state.layoutToUse.id
const includeMetaToggle = new CheckBoxes([t.includeMetaData])
const metaisIncluded = includeMetaToggle.GetValue().map((selected) => selected.length > 0)

View file

@ -20,6 +20,7 @@ import FilteredLayer from "../../Models/FilteredLayer"
import CopyrightPanel from "./CopyrightPanel"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import PrivacyPolicy from "./PrivacyPolicy"
import Hotkeys from "../Base/Hotkeys"
export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
public static MoreThemesTabIndex = 1
@ -126,6 +127,7 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
osmcha_link: Utils.OsmChaLinkFor(7),
}),
"<br/>Version " + Constants.vNumber,
Hotkeys.generateDocumentationDynamic(),
]).SetClass("link-underline"),
})

View file

@ -0,0 +1,129 @@
import { VariableUiElement } from "../Base/VariableUIElement"
import Svg from "../../Svg"
import { UIEventSource } from "../../Logic/UIEventSource"
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"
import { BBox } from "../../Logic/BBox"
import Loc from "../../Models/Loc"
import Hotkeys from "../Base/Hotkeys"
/**
* Displays an icon depending on the state of the geolocation.
* Will set the 'lock' if clicked twice
*/
export class GeolocationControl extends VariableUiElement {
constructor(
geolocationHandler: GeoLocationHandler,
state: {
locationControl: UIEventSource<Loc>
currentBounds: UIEventSource<BBox>
}
) {
const lastClick = new UIEventSource<Date>(undefined)
const lastClickWithinThreeSecs = lastClick.map((lastClick) => {
if (lastClick === undefined) {
return false
}
const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000
return timeDiff <= 3
})
const geolocationState = geolocationHandler.geolocationState
super(
geolocationState.permission.map(
(permission) => {
if (permission === "denied") {
return Svg.location_refused_svg()
}
if (geolocationState.isLocked.data) {
return Svg.location_locked_svg()
}
if (geolocationState.currentGPSLocation.data === undefined) {
if (permission === "prompt") {
return Svg.location_empty_svg()
}
// Position not yet found, but permission is either requested or granted: we spin to indicate activity
const icon = !geolocationHandler.mapHasMoved.data
? Svg.location_svg()
: Svg.location_empty_svg()
return icon
.SetClass("cursor-wait")
.SetStyle("animation: spin 4s linear infinite;")
}
// We have a location, so we show a dot in the center
if (
lastClickWithinThreeSecs.data &&
geolocationState.permission.data === "granted"
) {
return Svg.location_unlocked_svg()
}
// We have a location, so we show a dot in the center
return Svg.location_svg()
},
[
geolocationState.currentGPSLocation,
geolocationState.isLocked,
geolocationHandler.mapHasMoved,
lastClickWithinThreeSecs,
]
)
)
async function handleClick() {
if (geolocationState.permission.data !== "granted") {
await geolocationState.requestPermission()
}
if (geolocationState.isLocked.data === true) {
// Unlock
geolocationState.isLocked.setData(false)
return
}
if (geolocationState.currentGPSLocation.data === undefined) {
// No location is known yet, not much we can do
return
}
// A location _is_ known! Let's move to this location
const currentLocation = geolocationState.currentGPSLocation.data
const inBounds = state.currentBounds.data.contains([
currentLocation.longitude,
currentLocation.latitude,
])
geolocationHandler.MoveMapToCurrentLocation()
if (inBounds) {
const lc = state.locationControl.data
state.locationControl.setData({
...lc,
zoom: lc.zoom + 3,
})
}
if (lastClickWithinThreeSecs.data && geolocationState.permission.data === "granted") {
geolocationState.isLocked.setData(true)
lastClick.setData(undefined)
return
}
lastClick.setData(new Date())
}
this.onClick(handleClick)
Hotkeys.RegisterHotkey(
{ nomod: "L" },
"Pan the map to the current location or zoom the map to the current location. Requests geopermission",
handleClick
)
lastClick.addCallbackAndRunD((_) => {
window.setTimeout(() => {
if (lastClickWithinThreeSecs.data) {
lastClick.ping()
}
}, 500)
})
}
}

View file

@ -13,7 +13,7 @@ import { VariableUiElement } from "../Base/VariableUIElement"
import FeatureInfoBox from "../Popup/FeatureInfoBox"
import CopyrightPanel from "./CopyrightPanel"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import { FixedUiElement } from "../Base/FixedUiElement"
import Hotkeys from "../Base/Hotkeys"
export default class LeftControls extends Combine {
constructor(
@ -73,7 +73,7 @@ export default class LeftControls extends Combine {
guiState.downloadControlIsOpened.setData(true)
)
const downloadButtonn = new Toggle(
const downloadButton = new Toggle(
toggledDownload,
undefined,
state.featureSwitchEnableExport.map(
@ -94,11 +94,20 @@ export default class LeftControls extends Combine {
const toggledFilter = new MapControlButton(Svg.layers_svg()).onClick(() =>
guiState.filterViewIsOpened.setData(true)
)
state.featureSwitchFilter.addCallbackAndRun((f) => {
Hotkeys.RegisterHotkey(
{ nomod: "B" },
"Opens the Background, layers and filters panel",
() => {
guiState.filterViewIsOpened.setData(!guiState.filterViewIsOpened.data)
}
)
})
const filterButton = new Toggle(toggledFilter, undefined, state.featureSwitchFilter)
const mapSwitch = new Toggle(
new BackgroundMapSwitch(state, state.backgroundLayer),
new BackgroundMapSwitch(state, state.backgroundLayer, { enableHotkeys: true }),
undefined,
state.featureSwitchBackgroundSelection
)
@ -120,7 +129,7 @@ export default class LeftControls extends Combine {
state.featureSwitchWelcomeMessage
)
super([currentViewAction, filterButton, downloadButtonn, copyright, mapSwitch])
super([currentViewAction, filterButton, downloadButton, copyright, mapSwitch])
this.SetClass("flex flex-col")
}

View file

@ -92,13 +92,13 @@ export default class MoreScreen extends Combine {
if (onMainScreen) {
search.focus()
document.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.code === "KeyF") {
search.focus()
event.preventDefault()
}
})
}
document.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.code === "KeyF") {
search.focus()
event.preventDefault()
}
})
const searchBar = new Combine([
Svg.search_svg().SetClass("w-8"),

View file

@ -5,18 +5,16 @@ import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"
import Svg from "../../Svg"
import MapState from "../../Logic/State/MapState"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import { Utils } from "../../Utils"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import { BBox } from "../../Logic/BBox"
import { OsmFeature } from "../../Models/OsmFeature"
import LevelSelector from "./LevelSelector"
import { GeolocationControl } from "./GeolocationControl"
export default class RightControls extends Combine {
constructor(state: MapState & { featurePipeline: FeaturePipeline }) {
const geolocatioHandler = new GeoLocationHandler(state)
constructor(
state: MapState & { featurePipeline: FeaturePipeline },
geolocationHandler: GeoLocationHandler
) {
const geolocationButton = new Toggle(
new MapControlButton(geolocatioHandler, {
new MapControlButton(new GeolocationControl(geolocationHandler, state), {
dontStyle: true,
}).SetClass("p-1"),
undefined,

View file

@ -1,6 +1,5 @@
import { UIEventSource } from "../../Logic/UIEventSource"
import { Translation } from "../i18n/Translation"
import { VariableUiElement } from "../Base/VariableUIElement"
import Svg from "../../Svg"
import { TextField } from "../Input/TextField"
import { Geocoding } from "../../Logic/Osm/Geocoding"
@ -10,6 +9,7 @@ import Combine from "../Base/Combine"
import Locale from "../i18n/Locale"
export default class SearchAndGo extends Combine {
private readonly _searchField: TextField
constructor(state: { leafletMap: UIEventSource<any>; selectedElement?: UIEventSource<any> }) {
const goButton = Svg.search_ui().SetClass("w-8 h-8 full-rounded border-black float-right")
@ -74,6 +74,11 @@ export default class SearchAndGo extends Combine {
}
searchField.enterPressed.addCallback(runSearch)
this._searchField = searchField
goButton.onClick(runSearch)
}
focus() {
this._searchField.focus()
}
}

View file

@ -80,7 +80,7 @@ export default class ShareScreen extends Combine {
includeCurrentBackground.GetValue().map(
(includeBG) => {
if (includeBG) {
return "background=" + currentLayer.data.id
return "background=" + currentLayer.data?.id
} else {
return null
}
@ -168,7 +168,7 @@ export default class ShareScreen extends Combine {
return `<span class='literal-code iframe-code-block'>
&lt;iframe src="${url}" allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" title="${
layout.title?.txt ?? "MapComplete"
} with MapComplete"&gt;&lt;/iframe&gt
} with MapComplete"&gt;&lt;/iframe&gt
</span>`
})
)

View file

@ -29,6 +29,10 @@ import Img from "./Base/Img"
import UserInformationPanel from "./BigComponents/UserInformation"
import { LoginToggle } from "./Popup/LoginButton"
import { FixedUiElement } from "./Base/FixedUiElement"
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
import { GeoLocationState } from "../Logic/State/GeoLocationState"
import Hotkeys from "./Base/Hotkeys"
import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers"
/**
* The default MapComplete GUI initializer
@ -38,10 +42,14 @@ import { FixedUiElement } from "./Base/FixedUiElement"
export default class DefaultGUI {
private readonly guiState: DefaultGuiState
private readonly state: FeaturePipelineState
private readonly geolocationHandler: GeoLocationHandler | undefined
constructor(state: FeaturePipelineState, guiState: DefaultGuiState) {
this.state = state
this.guiState = guiState
if (this.state.featureSwitchGeolocation.data) {
this.geolocationHandler = new GeoLocationHandler(new GeoLocationState(), state)
}
}
public setup() {
@ -55,6 +63,14 @@ export default class DefaultGUI {
Utils.LoadCustomCss(this.state.layoutToUse.customCss)
}
Hotkeys.RegisterHotkey(
{ shift: "O" },
"Switch to default Mapnik-OpenStreetMap background",
() => {
this.state.backgroundLayer.setData(AvailableBaseLayers.osmCarto)
}
)
Utils.downloadJson("./service-worker-version")
.then((data) => console.log("Service worker", data))
.catch((_) => console.log("Service worker not active"))
@ -122,7 +138,7 @@ export default class DefaultGUI {
.SetStyle("left: calc( 50% - 15px )") // This is a bit hacky, yes I know!
}
new StrayClickHandler(
StrayClickHandler.construct(
state,
addNewPoint,
hasPresets ? new AddNewMarker(state.filteredLayers) : noteMarker
@ -145,6 +161,9 @@ export default class DefaultGUI {
}
private SetupMap() {
if (Utils.runningFromConsole) {
return
}
const state = this.state
const guiState = this.guiState
@ -232,18 +251,27 @@ export default class DefaultGUI {
.AttachTo("on-small-screen")
new Combine([
Toggle.If(state.featureSwitchSearch, () =>
new SearchAndGo(state).SetClass(
Toggle.If(state.featureSwitchSearch, () => {
const search = new SearchAndGo(state).SetClass(
"shadow rounded-full h-min w-full overflow-hidden sm:max-w-sm pointer-events-auto"
)
),
Hotkeys.RegisterHotkey(
{ ctrl: "F" },
"Select the search bar to search locations",
() => {
search.focus()
}
)
return search
}),
]).AttachTo("top-right")
new LeftControls(state, guiState).AttachTo("bottom-left")
new RightControls(state).AttachTo("bottom-right")
new RightControls(state, this.geolocationHandler).AttachTo("bottom-right")
new CenterMessageBox(state).AttachTo("centermessage")
document.getElementById("centermessage").classList.add("pointer-events-none")
document?.getElementById("centermessage")?.classList?.add("pointer-events-none")
// We have to ping the welcomeMessageIsOpened and other isOpened-stuff to activate the FullScreenMessage if needed
for (const state of guiState.allFullScreenStates) {

View file

@ -22,8 +22,10 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { ElementStorage } from "../../Logic/ElementStorage"
import BaseLayer from "../../Models/BaseLayer"
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
private static splitLayerStyling = new LayerConfig(
split_point,
@ -63,6 +65,106 @@ export default class SplitRoadWizard extends Toggle {
// Toggle variable between show split button and map
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
const roadElement = state.allElements.ContainingFeatures.get(id)
@ -96,7 +198,6 @@ export default class SplitRoadWizard extends Toggle {
layerToShow: SplitRoadWizard.splitLayerStyling,
state,
})
/**
* Handles a click on the overleaf map.
* 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])
})
)
// 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
return miniMap
}
}

View file

@ -1,7 +1,7 @@
import { Utils } from "../../Utils"
import { Feature } from "geojson"
import { Point } from "@turf/turf"
import { GeoLocationPointProperties } from "../../Logic/Actors/GeoLocationHandler"
import { GeoLocationPointProperties } from "../../Logic/State/GeoLocationState"
import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI"
import { SpecialVisualization } from "../SpecialVisualization"

View file

@ -17,7 +17,10 @@ export default class ShowDataLayer {
*/
constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) {
if (ShowDataLayer.actualContstructor === undefined) {
throw "Show data layer is called, but it isn't initialized yet. Call ` ShowDataLayer.actualContstructor = (options => new ShowDataLayerImplementation(options)) ` somewhere, e.g. in your init"
console.error(
"Show data layer is called, but it isn't initialized yet. Call ` ShowDataLayer.actualContstructor = (options => new ShowDataLayerImplementation(options)) ` somewhere, e.g. in your init"
)
return
}
ShowDataLayer.actualContstructor(options)
}

View file

@ -143,6 +143,99 @@
"override": {
"condition": "amenity!=bank"
}
},
{
"id": "cash_out",
"question": {
"en": "Can you withdraw cash from this ATM?"
},
"mappings": [
{
"if": "cash_out=",
"then": {
"en": "You can withdraw cash from this ATM"
},
"hideInAnswer": true
},
{
"if": "cash_out=yes",
"then": {
"en": "You can withdraw cash from this ATM"
}
},
{
"if": "cash_out=no",
"then": {
"en": "You cannot withdraw cash from this ATM"
}
}
]
},
{
"id": "cash_in",
"question": {
"en": "Can you deposit cash into this ATM?"
},
"mappings": [
{
"if": "cash_in=",
"then": {
"en": "You probably cannot deposit cash into this ATM"
},
"hideInAnswer": true
},
{
"if": "cash_in=yes",
"then": {
"en": "You can deposit cash into this ATM"
}
},
{
"if": "cash_in=no",
"then": {
"en": "You cannot deposit cash into this ATM"
}
}
]
},
{
"id": "speech_output",
"question": {
"en": "Does this ATM have speech output for visually impaired users?"
},
"mappings": [
{
"if": "speech_output=yes",
"then": {
"en": "This ATM has speech output, usually available through a headphone jack"
}
},
{
"if": "speech_output=no",
"then": {
"en": "This ATM does not have speech output"
}
}
]
},
{
"id": "speech_output_language",
"condition": "speech_output=yes",
"render": {
"special": {
"type": "language_chooser",
"key": "speech_output",
"question": {
"en": "In which languages does this ATM have speech output?"
},
"render_list_item": {
"en": "This ATM has speech output in {language():font-bold}"
},
"render_single_language": {
"en": "This ATM has speech output in {language():font-bold}"
}
}
}
}
],
"mapRendering": [
@ -153,5 +246,19 @@
"centroid"
]
}
],
"filter": [
"open_now",
{
"id": "speech_output",
"options": [
{
"question": {
"en": "With speech output"
},
"osmTags": "speech_output=yes"
}
]
}
]
}
}

View file

@ -2,7 +2,11 @@
{
"path": "bench.svg",
"license": "CC0",
"authors": [],
"sources": []
"authors": [
"Tobias Zwick"
],
"sources": [
"https://github.com/streetcomplete/StreetComplete/"
]
}
]

View file

@ -314,7 +314,10 @@
"cs": "Zde si můžete půjčit cyklistické přilby"
}
}
]
],
"condition": {
"and": []
}
},
{
"id": "rental_types",
@ -443,7 +446,9 @@
"key": "capacity:bicycle_type",
"type": "pnat"
},
"condition": "rental~.*bicycle_type.*"
"condition": {"and":
["rental~.*bicycle_type.*"]
}
}
]
}
@ -554,4 +559,4 @@
}
]
}
}
}

View file

@ -487,7 +487,14 @@
}
]
},
"bicycle_rental.*bicycle_rental",
{
"builtin": "bicycle_rental.*bicycle_rental",
"override": {
"condition": {
"and+": ["service:bicycle:rental=yes"]
}
}
},
{
"id": "bike_repair_second-hand-bikes",
"question": {
@ -783,4 +790,4 @@
}
}
]
}
}

View file

@ -23,7 +23,7 @@
"osmTags": "emergency=defibrillator"
},
"calculatedTags": [
"_days_since_last_survey=Math.floor(new Date() - new Date(feat.properties['survey:date'])/(1000*60*60*24))",
"_days_since_last_survey=Math.floor((new Date() - new Date(feat.properties['survey:date']))/(1000*60*60*24))",
"_recently_surveyed=Number(feat.properties._days_since_last_survey) <= 90"
],
"minzoom": 12,

View file

@ -15,6 +15,7 @@
"osmTags": "amenity=parking_space"
},
"tagRenderings": [
"images",
{
"id": "type",
"question": {

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="700pt" height="700pt" version="1.1" viewBox="0 0 700 700" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g>
<path d="m552.4 181.67c-1.5312 0-3.0078-0.60156-4.1016-1.6953l-98.328-98.328c-1.0938-1.0938-1.6953-2.5156-1.6953-4.1016 0-1.5312 0.60156-3.0078 1.6953-4.1016l27.344-27.344c3.5547-3.5547 8.3125-5.5234 13.344-5.5234h0.054688c5.0312 0 9.7344 1.9688 13.289 5.4688l79.844 79.844c7.3828 7.3828 7.3828 19.359 0 26.688l-27.344 27.344c-1.0938 1.1484-2.5703 1.75-4.1016 1.75zm-91.055-104.12 91.055 91.055 23.68-23.68c3.1172-3.1172 3.1172-8.1484 0-11.266l-79.844-79.844c-1.4766-1.4766-3.4453-2.2969-5.5234-2.2969-2.1328 0-4.1016 0.82031-5.6328 2.3516z"/>
<path d="m240.19 484.04-94.227-94.227 313.8-313.8 94.281 94.281zm-78.805-94.227 78.805 78.805 298.32-298.32-78.75-78.859z"/>
<path d="m209.34 519.42c-5.0312 0-9.7891-1.9688-13.344-5.5234l-79.844-79.844c-3.5547-3.5547-5.5234-8.3125-5.5234-13.344s1.9688-9.7891 5.5234-13.344l27.344-27.344c1.0938-1.0938 2.5156-1.6953 4.1016-1.6953 1.5312 0 3.0078 0.60156 4.1016 1.6953l98.383 98.383c1.0938 1.0938 1.6953 2.5156 1.6953 4.1016 0 1.5312-0.60156 3.0078-1.6953 4.1016l-27.344 27.344c-3.6641 3.5-8.3672 5.4688-13.398 5.4688zm-61.797-128.08-23.68 23.734c-1.5312 1.5312-2.3516 3.5-2.3516 5.6328 0 2.1328 0.82031 4.1016 2.3516 5.6328l79.844 79.844c1.5312 1.5312 3.5 2.3516 5.6328 2.3516s4.1016-0.82031 5.6328-2.3516l23.68-23.68z"/>
<path d="m522.1 79.68-19.523-19.523 21.711-21.711c5.4141-5.4141 14.219-5.4141 19.578 0 2.5703 2.5703 4.0469 6.0156 4.0469 9.6797 0 3.7188-1.4219 7.2188-4.0469 9.8438zm-4.1016-19.523 4.1016 4.1016 14-14c0.54688-0.54687 0.875-1.3125 0.875-2.0781 0-0.4375-0.10937-1.2578-0.82031-1.9688-1.1484-1.1484-3.0078-1.1484-4.1562-0.054688z"/>
<path d="m569.84 127.37-19.523-19.523 21.711-21.711c5.3594-5.3047 14.109-5.3047 19.469 0 2.625 2.5703 4.1016 6.0156 4.1016 9.7344s-1.4219 7.2188-4.0469 9.8438zm-4.1016-19.523 4.1016 4.1016 13.945-13.945c0.54688-0.54688 0.875-1.3125 0.875-2.0781 0-0.4375-0.10937-1.2578-0.82031-1.9688-1.1484-1.1484-2.9531-1.1484-4.1562 0z"/>
<path d="m165.92 525.6c-3.7188 0-7.1641-1.4219-9.7891-4.0469-2.5703-2.5703-4.0469-6.0156-4.0469-9.6797 0-3.7188 1.4219-7.2188 4.0469-9.8438l21.711-21.711 19.523 19.523-21.711 21.711c-2.5703 2.5703-6.0156 4.0469-9.6797 4.0469h-0.054687zm11.922-29.859-13.945 13.945c-0.54688 0.54688-0.875 1.3125-0.875 2.0781 0 0.4375 0.10937 1.2578 0.82031 1.9688 0.60156 0.60156 1.3125 0.875 2.0781 0.875 0.4375 0 1.2578-0.10938 1.9688-0.82031l14-14z"/>
<path d="m118.23 477.91c-3.7188 0-7.1641-1.4219-9.7891-4.0469-2.5703-2.5703-4.0469-6.0156-4.0469-9.6797 0-3.7188 1.4219-7.2188 4.0469-9.8438l21.711-21.711 19.523 19.523-21.711 21.711c-2.5703 2.5703-6.0156 4.0469-9.6797 4.0469h-0.054687zm11.922-29.859-13.945 13.945c-0.54688 0.54688-0.875 1.3125-0.875 2.0781 0 0.4375 0.10937 1.2578 0.82031 1.9688 0.60156 0.60156 1.3125 0.875 2.0781 0.875 0.4375 0 1.2578-0.10938 1.9688-0.82031l14-14z"/>
<path d="m297.07 284.02 7.7344 7.7344-16.781 16.781-7.7344-7.7344z"/>
<path d="m370.96 210.19 7.7344 7.7344-57.656 57.656-7.7344-7.7344z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -60,6 +60,17 @@
"https://github.com/streetcomplete/StreetComplete/blob/master/res/graphics/recycling%20icons/engine_oil.svg"
]
},
{
"path": "fluorescent_tubes.svg",
"license": "CC-BY",
"authors": [
"Noun Project",
"shashank singh"
],
"sources": [
"https://thenounproject.com/icon/tube-fluorescent-light-3756518/"
]
},
{
"path": "garden_waste.svg",
"license": "CC-BY-SA",
@ -91,6 +102,17 @@
"https://github.com/streetcomplete/StreetComplete/blob/master/res/graphics/recycling%20icons/glass_bottles.svg"
]
},
{
"path": "light_bulbs.svg",
"license": "CC0",
"authors": [
"OpenClipArt",
"fabiovaleggia"
],
"sources": [
"https://openclipart.org/detail/175842/basic-light-bulb"
]
},
{
"path": "newspaper.svg",
"license": "CC-BY-SA",

View file

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://web.resource.org/cc/" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:ns1="http://sozi.baierouge.fr" id="svg2" viewBox="0 0 801 801" version="1.0">
<defs id="defs4">
<linearGradient id="linearGradient3187">
<stop id="stop3189" stop-color="#fff" offset="0"/>
<stop id="stop3191" stop-color="#fff" stop-opacity="0" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient3175">
<stop id="stop3177" offset="0"/>
<stop id="stop3179" stop-opacity="0" offset="1"/>
</linearGradient>
<filter id="filter4712" y="-.18408" width="1.4494" height="1.3682" x="-.22472">
<feGaussianBlur id="feGaussianBlur4714" stdDeviation="44.016276"/>
</filter>
<radialGradient id="radialGradient5307" gradientUnits="userSpaceOnUse" cy="891.75" cx="995.08" gradientTransform="matrix(1.7827 1.8967e-7 -4.1911e-8 .39392 -778.86 540.47)" r="52.236">
<stop id="stop4522" offset="0"/>
<stop id="stop4524" stop-opacity="0" offset="1"/>
</radialGradient>
<radialGradient id="radialGradient5309" xlink:href="#linearGradient3175" gradientUnits="userSpaceOnUse" cy="717.11" cx="509.03" gradientTransform="matrix(.73643 8.7347e-7 -.0000011447 .96512 152.63 23.39)" r="64.5"/>
<radialGradient id="radialGradient5311" xlink:href="#linearGradient3187" gradientUnits="userSpaceOnUse" cy="716.36" cx="512.8" gradientTransform="matrix(.48062 -9.5378e-8 1.5922e-7 .80233 316.04 140.73)" r="64.5"/>
<radialGradient id="radialGradient5313" xlink:href="#linearGradient3175" gradientUnits="userSpaceOnUse" cy="717.11" cx="590.74" gradientTransform="matrix(.45736 -3.8247e-7 8.0707e-7 .96512 319.32 23.391)" r="64.5"/>
<radialGradient id="radialGradient5315" xlink:href="#linearGradient3187" gradientUnits="userSpaceOnUse" cy="716.36" cx="598.74" gradientTransform="matrix(.30233 3.7735e-8 -1.0014e-7 .80233 428.99 140.73)" r="64.5"/>
<radialGradient id="radialGradient5317" xlink:href="#linearGradient3175" gradientUnits="userSpaceOnUse" cy="717.11" cx="753.17" gradientTransform="matrix(.68217 -4.4781e-7 6.3355e-7 .96512 122.21 23.391)" r="64.5"/>
<radialGradient id="radialGradient5319" xlink:href="#linearGradient3175" gradientUnits="userSpaceOnUse" cy="717.11" cx="509.03" gradientTransform="matrix(.72462 8.7347e-7 -.0000011263 .96512 -12.477 15.53)" r="64.5"/>
<radialGradient id="radialGradient5321" xlink:href="#linearGradient3187" gradientUnits="userSpaceOnUse" cy="716.36" cx="512.8" gradientTransform="matrix(.47291 -9.5378e-8 1.5667e-7 .80233 148.31 132.87)" r="64.5"/>
<radialGradient id="radialGradient5323" xlink:href="#linearGradient3175" gradientUnits="userSpaceOnUse" cy="717.11" cx="590.74" gradientTransform="matrix(.45108 .0069435 -.014382 .96501 161.23 11.508)" r="64.5"/>
<radialGradient id="radialGradient5325" xlink:href="#linearGradient3187" gradientUnits="userSpaceOnUse" cy="716.36" cx="598.74" gradientTransform="matrix(.29748 3.7735e-8 -9.8537e-8 .80233 259.44 132.87)" r="64.5"/>
<radialGradient id="radialGradient5327" xlink:href="#linearGradient3175" gradientUnits="userSpaceOnUse" cy="717.11" cx="753.17" gradientTransform="matrix(.67123 -4.4781e-7 6.2339e-7 .96512 -42.412 15.531)" r="64.5"/>
<radialGradient id="radialGradient5329" xlink:href="#linearGradient3175" gradientUnits="userSpaceOnUse" cy="717.11" cx="509.03" gradientTransform="matrix(.73642 -.0042366 .0030467 .52973 -17.205 458.52)" r="64.5"/>
<radialGradient id="radialGradient5331" xlink:href="#linearGradient3187" gradientUnits="userSpaceOnUse" cy="716.36" cx="512.8" gradientTransform="matrix(.48062 .000392 -.00034729 .42594 148.97 528.69)" r="64.5"/>
<radialGradient id="radialGradient5333" xlink:href="#linearGradient3175" gradientUnits="userSpaceOnUse" cy="717.11" cx="590.74" gradientTransform="matrix(.45736 -.00082768 .00093239 .51491 151.73 463.54)" r="64.5"/>
<radialGradient id="radialGradient5335" xlink:href="#linearGradient3187" gradientUnits="userSpaceOnUse" cy="716.36" cx="598.74" gradientTransform="matrix(.30232 -.0012309 .0017686 .43441 260.37 520.55)" r="64.5"/>
<radialGradient id="radialGradient5337" xlink:href="#linearGradient3175" gradientUnits="userSpaceOnUse" cy="717.11" cx="753.17" gradientTransform="matrix(.68199 .015520 -.011766 .51703 -36.145 446.9)" r="64.5"/>
<radialGradient id="radialGradient5389" xlink:href="#linearGradient3175" gradientUnits="userSpaceOnUse" cy="717.11" cx="509.03" gradientTransform="matrix(.73643 8.7347e-7 -.0000011447 .96512 -18.367 115.54)" r="64.5"/>
<radialGradient id="radialGradient5391" xlink:href="#linearGradient3187" gradientUnits="userSpaceOnUse" cy="716.36" cx="512.8" gradientTransform="matrix(.48062 -9.5378e-8 1.5922e-7 .80233 145.62 232.86)" r="64.5"/>
<radialGradient id="radialGradient5393" xlink:href="#linearGradient3175" gradientUnits="userSpaceOnUse" cy="717.11" cx="590.74" gradientTransform="matrix(.45736 -3.8247e-7 8.0707e-7 .96512 149.43 115.52)" r="64.5"/>
<radialGradient id="radialGradient5395" xlink:href="#linearGradient3187" gradientUnits="userSpaceOnUse" cy="716.36" cx="598.74" gradientTransform="matrix(.30233 3.7735e-8 -1.0014e-7 .80233 258.58 232.89)" r="64.5"/>
<radialGradient id="radialGradient5397" xlink:href="#linearGradient3175" gradientUnits="userSpaceOnUse" cy="717.11" cx="753.17" gradientTransform="matrix(.68217 -4.4781e-7 6.3355e-7 .96512 -47.791 115.52)" r="64.5"/>
<radialGradient id="radialGradient2544" gradientUnits="userSpaceOnUse" cy="422.33" cx="1345.1" gradientTransform="matrix(.99128 -.0053344 .027760 5.1585 -.84309 -1759.8)" r="195.88">
<stop id="stop4646" stop-color="#66676a" offset="0"/>
<stop id="stop4648" stop-color="#66676a" stop-opacity="0" offset="1"/>
</radialGradient>
</defs>
<g id="layer1" transform="translate(-821.7 -56.241)">
<path id="path3446" d="m1235.6 199.12c-108.2 0-195.9 87.75-195.9 195.87 0 74.78 62.3 147.32 87.6 172.57 22.8 22.85 5.8 81.09 50.5 109.49l115.6 0.29c41.2-30.97 24.8-84.81 52.7-109.57 29.2-21.4 85.3-98 85.3-172.78 0-108.12-87.7-195.87-195.8-195.87z" stroke="#7f868f" stroke-linecap="round" fill="#fff"/>
<path id="path3448" opacity=".29365" filter="url(#filter4712)" d="m1235.6 199.12c-108.2 0-195.9 87.75-195.9 195.87 0 74.78 65.5 149.92 89.3 172.79 24.4 23.39-0.3 77.64 48.8 109.56h115.6c41.2-30.97 24.2-86.49 52.7-109.57 28.6-23.07 85.3-98 85.3-172.78 0-108.12-87.7-195.87-195.8-195.87z" fill="url(#radialGradient2544)"/>
<path id="path3450" fill="#fff" d="m1229.7 212.8c-1.1 0-2.2 0.01-3.3 0.03 88.9 11.45 157.6 87.42 157.6 179.34 0 69.04-51.9 139.76-78.8 159.52-25.8 22.85-10.6 72.56-48.7 101.15h-82.3c0.7 0.5 1.5 0.99 2.2 1.48h106.7c38.1-28.6 22.8-78.31 48.7-101.16 26.9-19.76 78.8-90.48 78.8-159.52 0-99.82-81-180.84-180.9-180.84z"/>
<g id="g3452" stroke-linecap="round" transform="translate(828.16 .0000025)">
<g id="g3454" stroke="#000" transform="matrix(1.0301 0 0 .93888 -654.71 -74.398)">
<path id="path3456" d="m1078 891.75a51.736 20.077 0 1 1 -103.52 0 51.736 20.077 0 1 1 103.52 0z" fill="#000051" transform="translate(.77236 -.36379)"/>
<path id="path3458" d="m1078 891.75a51.736 20.077 0 1 1 -103.52 0 51.736 20.077 0 1 1 103.52 0z" fill="url(#radialGradient5307)" transform="translate(.77236 -.36379)"/>
</g>
<g id="g3460" transform="translate(-174.44 -28.235)">
<path id="path3462" d="m525.49 705.74h110.02c4.98 0 8.99 1.76 8.99 3.94v11.61c0 2.19-4.01 3.95-8.99 3.95h-110.02c-4.98 0-8.99-1.76-8.99-3.95v-11.61c0-2.18 4.01-3.94 8.99-3.94z" stroke="#000" fill="url(#radialGradient5309)"/>
<path id="path3464" d="m525.49 705.74h110.02c4.98 0 8.99 1.76 8.99 3.94v11.61c0 2.19-4.01 3.95-8.99 3.95h-110.02c-4.98 0-8.99-1.76-8.99-3.95v-11.61c0-2.18 4.01-3.94 8.99-3.94z" stroke="#000" fill="url(#radialGradient5311)"/>
<path id="path3466" d="m525.49 705.74h110.02c4.98 0 8.99 1.76 8.99 3.94v11.61c0 2.19-4.01 3.95-8.99 3.95h-110.02c-4.98 0-8.99-1.76-8.99-3.95v-11.61c0-2.18 4.01-3.94 8.99-3.94z" stroke="#000" fill="url(#radialGradient5313)"/>
<path id="path3468" d="m525.49 705.74h110.02c4.98 0 8.99 1.76 8.99 3.94v11.61c0 2.19-4.01 3.95-8.99 3.95h-110.02c-4.98 0-8.99-1.76-8.99-3.95v-11.61c0-2.18 4.01-3.94 8.99-3.94z" stroke="#000" fill="url(#radialGradient5315)"/>
<path id="path3470" d="m525.49 705.74h110.02c4.98 0 8.99 1.76 8.99 3.94v11.61c0 2.19-4.01 3.95-8.99 3.95h-110.02c-4.98 0-8.99-1.76-8.99-3.95v-11.61c0-2.18 4.01-3.94 8.99-3.94z" stroke="#565352" fill="url(#radialGradient5317)"/>
</g>
<path id="path3472" d="m354.4 697.88h108.26c4.9 0 2.26 0.47 2.54-1.06l0.74 6.46c0 2.19-3.31 3.47-8.21 3.47l-103.33 10.63c-4.9 0-8.84-1.76-8.84-3.95v-11.61c0-2.18 3.94-3.94 8.84-3.94z" stroke="#000" stroke-width=".99195" fill="url(#radialGradient5319)"/>
<path id="path3474" d="m354.4 697.88l107.78-0.42c4.9 0 2.91 0.67 2.71-0.8l0.99 6.64c0 2.18-3.4 3.29-8.3 3.29l-103.18 10.79c-4.9 0-8.84-1.76-8.84-3.95v-11.61c0-2.18 3.94-3.94 8.84-3.94z" stroke="#000" stroke-width=".99195" fill="url(#radialGradient5321)"/>
<path id="path3476" d="m354.4 697.88h108.26c4.9 0 2.69-3.72 2.69-1.53l0.6 7.34c0 2.18-3.54 2.39-8.44 2.39l-103.11 11.3c-4.9 0-8.84-1.76-8.84-3.95v-11.61c0-2.18 3.94-3.94 8.84-3.94z" stroke="#000" stroke-width=".99195" fill="url(#radialGradient5323)"/>
<path id="path3478" d="m354.4 697.88h108.26c4.9 0 3-3.72 3-1.53l-0.34 6.66c0 2.19-3.81 3.23-8.51 3.74l-102.41 10.63c-4.9 0-8.84-1.76-8.84-3.95v-11.61c0-2.18 3.94-3.94 8.84-3.94z" stroke="#000" stroke-width=".99195" fill="url(#radialGradient5325)"/>
<path id="path3480" d="m354.4 697.88h108.26c3.82-1.57 3.3-3.87 3.3-1.69l0.16 5.98c0 2.19-3.95 3.95-8.85 3.95l-102.87 11.26c-4.9 0-8.84-1.76-8.84-3.95v-11.61c0-2.18 3.94-3.94 8.84-3.94z" stroke="#4e4b4b" stroke-width=".99195" fill="url(#radialGradient5327)"/>
<g id="g3482" transform="matrix(.95599 0 0 1 -333.55 -40.073)">
<path id="path3484" d="m825.87 742.05c-0.21 0.01-0.43 0.03-0.65 0.04-0.3 0-0.59 0.01-0.88 0.03l-103.91 6.97c-2.79 0.2-4.79 0.88-6.12 1.78-1.6 0.96-2.53 2.3-2.47 3.97 0.05 1.46 1.11 2.58 2.66 3.37 1.44 0.81 3.4 1.29 5.56 1.31-2.27 0.21-3.98 0.74-5.22 1.44-0.04 0.02-0.09 0.04-0.12 0.06-0.1 0.05-0.2 0.11-0.29 0.16l-0.03 0.03-0.09 0.06c-0.16 0.1-0.32 0.21-0.47 0.32-0.02 0.01-0.04 0.04-0.06 0.06-1.26 0.92-1.99 2.09-1.94 3.56 0.04 1.29 0.87 2.33 2.13 3.09 0.01 0.01 0.01 0.03 0.03 0.04 0.15 0.09 0.33 0.19 0.5 0.28 1.18 0.67 2.72 1.07 4.43 1.22 0.07 0 0.13 0.02 0.19 0.03 0.19 0.02 0.37 0.02 0.56 0.03h0.47c0.42 0.02 0.86-0.02 1.28-0.03h0.07c0.12-0.01 0.25 0.01 0.37 0l103.91-6.97c1.2-0.08 2.32-0.32 3.31-0.69 3.21-0.98 5.09-2.7 4.81-5.09-0.27-2.43-2.54-4.05-5.75-4.5-0.02-0.01-0.04 0-0.06 0-0.05-0.01-0.1-0.03-0.16-0.03-0.03-0.01-0.06 0-0.09 0-0.29-0.04-0.57-0.08-0.87-0.1-0.23-0.01-0.49-0.02-0.72-0.03 0.89-0.09 1.73-0.27 2.5-0.53 3.41-0.97 5.44-2.75 5.15-5.22-0.28-2.48-2.64-4.09-5.97-4.5-0.62-0.09-1.32-0.16-2.06-0.16zm-0.65 21.04c-0.3 0-0.59 0.01-0.88 0.03l-103.91 6.97c-2.73 0.2-4.7 0.85-6.03 1.71-0.03 0.02-0.06 0.05-0.09 0.07l-0.09 0.06c-0.02 0.01-0.02 0.02-0.04 0.03-1.51 0.96-2.39 2.26-2.34 3.88 0.04 1.28 0.85 2.32 2.09 3.09 1.24 0.86 3.02 1.36 5 1.53 0.07 0.01 0.13 0.03 0.19 0.03 0.19 0.02 0.37 0.02 0.56 0.03-2.34 0.26-4.07 0.88-5.28 1.66l-0.09 0.06-0.09 0.06c-0.02 0.01-0.02 0.03-0.04 0.04-1.51 0.95-2.39 2.25-2.34 3.87 0.04 1.29 0.85 2.33 2.09 3.09 1.24 0.87 3.02 1.37 5 1.54 0.07 0 0.13 0.02 0.19 0.03 0.19 0.02 0.37 0.02 0.56 0.03h0.47c0.42 0.02 0.86-0.02 1.28-0.03h0.07c0.12-0.01 0.25 0.01 0.37 0l103.91-6.97c1.2-0.08 2.32-0.32 3.31-0.69 3.21-0.98 5.09-2.73 4.81-5.12-0.27-2.43-2.54-4.02-5.75-4.47-0.02-0.01-0.04 0-0.06 0-0.05-0.01-0.11-0.03-0.16-0.03-0.32-0.05-0.64-0.07-1-0.1-0.13-0.01-0.26-0.03-0.4-0.03 0.92-0.12 1.78-0.33 2.56-0.62 3.21-0.99 5.09-2.74 4.81-5.13-0.27-2.42-2.54-4.01-5.75-4.47-0.02 0-0.04 0.01-0.06 0-0.05-0.01-0.11-0.02-0.16-0.03-0.32-0.05-0.64-0.07-1-0.09-0.13-0.01-0.26-0.03-0.4-0.03-0.1-0.01-0.21 0-0.31 0-0.21-0.01-0.42-0.01-0.63 0-0.12-0.01-0.25-0.01-0.37 0zm0 21.15c-0.3 0-0.59 0.04-0.88 0.06l-8.69 0.6-95.22 6.34c-2.47 0.19-4.31 0.76-5.62 1.5-0.22 0.12-0.43 0.25-0.63 0.38-1.51 0.95-2.39 2.25-2.34 3.87 0.04 1.3 0.87 2.33 2.13 3.1 0.01 0 0.01 0.02 0.03 0.03 0.15 0.09 0.33 0.19 0.5 0.28 1.18 0.67 2.72 1.07 4.43 1.22 0.07 0 0.13 0.02 0.19 0.03 0.19 0.02 0.37 0.02 0.56 0.03h0.47c0.42 0.02 0.86-0.02 1.28-0.03h0.07c0.12-0.01 0.25 0.01 0.37 0l103.91-6.97c1.2-0.08 2.32-0.32 3.31-0.69 3.21-0.98 5.09-2.7 4.81-5.09-0.27-2.42-2.54-4.05-5.75-4.5-0.02-0.01-0.04 0-0.06 0-0.05-0.01-0.1-0.03-0.16-0.03-0.03-0.01-0.06 0-0.09 0-0.3-0.04-0.59-0.08-0.91-0.1-0.4-0.02-0.86-0.04-1.31-0.03h-0.4zm0.65 10.91c-0.17 0-0.35 0.02-0.53 0.03h-0.12c-0.17 0-0.34 0.02-0.5 0.03-0.13 0.01-0.25-0.01-0.38 0l-103.91 6.97c-2.72 0.2-4.67 0.86-6 1.72-0.03 0.02-0.08 0.04-0.12 0.06-1.6 0.96-2.53 2.3-2.47 3.97 0.05 1.46 1.11 2.59 2.66 3.37 1.23 0.7 2.83 1.13 4.62 1.25 0.31 0.03 0.62 0.06 0.94 0.07h0.94c0.14-0.01 0.29-0.02 0.43-0.03h0.07c0.12-0.01 0.25 0 0.37 0l103.91-6.97c1.06-0.07 2.07-0.3 2.97-0.6 3.41-0.96 5.44-2.75 5.15-5.22-0.31-2.71-3.1-4.43-6.93-4.62-0.35-0.02-0.73-0.03-1.1-0.03z" stroke="#000" stroke-width=".71131" fill="#fff"/>
<g id="g3486" stroke-width=".73116" transform="matrix(.94643 0 0 1 255.03 -82.084)">
<g id="g3488" transform="translate(134.21)">
<path id="path3490" d="m357.51 831.16l109.79-6.96c4.98-0.32 9.6 1.57 9.97 4.61s-4.01 5.41-9.31 5.79l-109.79 6.97c-5.64 0.29-9.51-2.3-9.57-4.64-0.06-2.35 2.15-5.29 8.91-5.77z" stroke="#000" fill="url(#radialGradient5329)"/>
<path id="path3492" d="m357.91 831.16l109.8-6.96c6.82-0.5 9.32 2.79 9.45 4.88 0.14 2.17-3.82 5.21-8.79 5.52l-109.79 6.97c-4.98 0.31-9.45-1.3-9.82-4.34s2.14-5.5 9.15-6.07z" stroke="#000" fill="url(#radialGradient5331)"/>
<path id="path3494" d="m358.37 831.16l109.8-6.97c4.54-0.29 8.02 1.23 8.61 4.23 0.6 3.06-2.98 5.86-7.95 6.18l-109.8 6.96c-4.97 0.32-9.59-1.57-9.96-4.61-0.38-3.04 3.87-5.54 9.3-5.79z" stroke="#000" fill="url(#radialGradient5333)"/>
<path id="path3496" d="m357.98 831.16l109.8-6.97c4.97-0.31 8.78 1.35 9.29 4.45 0.52 3.11-3.08 5.35-8.63 5.96l-109.8 6.96c-4.97 0.32-9.47-1.59-9.85-4.63-0.37-3.03 3.23-5.36 9.19-5.77z" stroke="#000" fill="url(#radialGradient5335)"/>
<path id="path3498" d="m358.46 831.16l109.79-6.97c4.98-0.32 8.79 1.49 9.17 4.6s-3.1 5.13-8.5 5.8l-109.8 6.97c-4.97 0.32-10.55-1.31-10.67-4.65-0.13-3.42 4.03-5.41 10.01-5.75z" stroke="#565352" fill="url(#radialGradient5337)"/>
</g>
<g id="g3500" transform="translate(134.21 10.389)">
<path id="path3502" d="m357.51 831.16l109.79-6.96c4.98-0.32 9.6 1.57 9.97 4.61s-4.01 5.41-9.31 5.79l-109.79 6.97c-5.64 0.29-9.51-2.3-9.57-4.64-0.06-2.35 2.15-5.29 8.91-5.77z" stroke="#000" fill="url(#radialGradient5329)"/>
<path id="path3504" d="m357.91 831.16l109.8-6.96c6.82-0.5 9.32 2.79 9.45 4.88 0.14 2.17-3.82 5.21-8.79 5.52l-109.79 6.97c-4.98 0.31-9.45-1.3-9.82-4.34s2.14-5.5 9.15-6.07z" stroke="#000" fill="url(#radialGradient5331)"/>
<path id="path3506" d="m358.37 831.16l109.8-6.97c4.54-0.29 8.02 1.23 8.61 4.23 0.6 3.06-2.98 5.86-7.95 6.18l-109.8 6.96c-4.97 0.32-9.59-1.57-9.96-4.61-0.38-3.04 3.87-5.54 9.3-5.79z" stroke="#000" fill="url(#radialGradient5333)"/>
<path id="path3508" d="m357.98 831.16l109.8-6.97c4.97-0.31 8.78 1.35 9.29 4.45 0.52 3.11-3.08 5.35-8.63 5.96l-109.8 6.96c-4.97 0.32-9.47-1.59-9.85-4.63-0.37-3.03 3.23-5.36 9.19-5.77z" stroke="#000" fill="url(#radialGradient5335)"/>
<path id="path3510" d="m358.46 831.16l109.79-6.97c4.98-0.32 8.79 1.49 9.17 4.6s-3.1 5.13-8.5 5.8l-109.8 6.97c-4.97 0.32-10.55-1.31-10.67-4.65-0.13-3.42 4.03-5.41 10.01-5.75z" stroke="#565352" fill="url(#radialGradient5337)"/>
</g>
<g id="g3512" transform="translate(134.21 21.011)">
<path id="path3514" d="m357.51 831.16l109.79-6.96c4.98-0.32 9.6 1.57 9.97 4.61s-4.01 5.41-9.31 5.79l-109.79 6.97c-5.64 0.29-9.51-2.3-9.57-4.64-0.06-2.35 2.15-5.29 8.91-5.77z" stroke="#000" fill="url(#radialGradient5329)"/>
<path id="path3516" d="m357.91 831.16l109.8-6.96c6.82-0.5 9.32 2.79 9.45 4.88 0.14 2.17-3.82 5.21-8.79 5.52l-109.79 6.97c-4.98 0.31-9.45-1.3-9.82-4.34s2.14-5.5 9.15-6.07z" stroke="#000" fill="url(#radialGradient5331)"/>
<path id="path3518" d="m358.37 831.16l109.8-6.97c4.54-0.29 8.02 1.23 8.61 4.23 0.6 3.06-2.98 5.86-7.95 6.18l-109.8 6.96c-4.97 0.32-9.59-1.57-9.96-4.61-0.38-3.04 3.87-5.54 9.3-5.79z" stroke="#000" fill="url(#radialGradient5333)"/>
<path id="path3520" d="m357.98 831.16l109.8-6.97c4.97-0.31 8.78 1.35 9.29 4.45 0.52 3.11-3.08 5.35-8.63 5.96l-109.8 6.96c-4.97 0.32-9.47-1.59-9.85-4.63-0.37-3.03 3.23-5.36 9.19-5.77z" stroke="#000" fill="url(#radialGradient5335)"/>
<path id="path3522" d="m358.46 831.16l109.79-6.97c4.98-0.32 8.79 1.49 9.17 4.6s-3.1 5.13-8.5 5.8l-109.8 6.97c-4.97 0.32-10.55-1.31-10.67-4.65-0.13-3.42 4.03-5.41 10.01-5.75z" stroke="#565352" fill="url(#radialGradient5337)"/>
</g>
<g id="g3524" transform="translate(134.21 31.385)">
<path id="path3526" d="m357.51 831.16l109.79-6.96c4.98-0.32 9.6 1.57 9.97 4.61s-4.01 5.41-9.31 5.79l-109.79 6.97c-5.64 0.29-9.51-2.3-9.57-4.64-0.06-2.35 2.15-5.29 8.91-5.77z" stroke="#000" fill="url(#radialGradient5329)"/>
<path id="path3528" d="m357.91 831.16l109.8-6.96c6.82-0.5 9.32 2.79 9.45 4.88 0.14 2.17-3.82 5.21-8.79 5.52l-109.79 6.97c-4.98 0.31-9.45-1.3-9.82-4.34s2.14-5.5 9.15-6.07z" stroke="#000" fill="url(#radialGradient5331)"/>
<path id="path3530" d="m358.37 831.16l109.8-6.97c4.54-0.29 8.02 1.23 8.61 4.23 0.6 3.06-2.98 5.86-7.95 6.18l-109.8 6.96c-4.97 0.32-9.59-1.57-9.96-4.61-0.38-3.04 3.87-5.54 9.3-5.79z" stroke="#000" fill="url(#radialGradient5333)"/>
<path id="path3532" d="m357.98 831.16l109.8-6.97c4.97-0.31 8.78 1.35 9.29 4.45 0.52 3.11-3.08 5.35-8.63 5.96l-109.8 6.96c-4.97 0.32-9.47-1.59-9.85-4.63-0.37-3.03 3.23-5.36 9.19-5.77z" stroke="#000" fill="url(#radialGradient5335)"/>
<path id="path3534" d="m358.46 831.16l109.79-6.97c4.98-0.32 8.79 1.49 9.17 4.6s-3.1 5.13-8.5 5.8l-109.8 6.97c-4.97 0.32-10.55-1.31-10.67-4.65-0.13-3.42 4.03-5.41 10.01-5.75z" stroke="#565352" fill="url(#radialGradient5337)"/>
</g>
<g id="g3536" transform="translate(134.21 42.169)">
<path id="path3538" d="m357.51 831.16l109.79-6.96c4.98-0.32 9.6 1.57 9.97 4.61s-4.01 5.41-9.31 5.79l-109.79 6.97c-5.64 0.29-9.51-2.3-9.57-4.64-0.06-2.35 2.15-5.29 8.91-5.77z" stroke="#000" fill="url(#radialGradient5329)"/>
<path id="path3540" d="m357.91 831.16l109.8-6.96c6.82-0.5 9.32 2.79 9.45 4.88 0.14 2.17-3.82 5.21-8.79 5.52l-109.79 6.97c-4.98 0.31-9.45-1.3-9.82-4.34s2.14-5.5 9.15-6.07z" stroke="#000" fill="url(#radialGradient5331)"/>
<path id="path3542" d="m358.37 831.16l109.8-6.97c4.54-0.29 8.02 1.23 8.61 4.23 0.6 3.06-2.98 5.86-7.95 6.18l-109.8 6.96c-4.97 0.32-9.59-1.57-9.96-4.61-0.38-3.04 3.87-5.54 9.3-5.79z" stroke="#000" fill="url(#radialGradient5333)"/>
<path id="path3544" d="m357.98 831.16l109.8-6.97c4.97-0.31 8.78 1.35 9.29 4.45 0.52 3.11-3.08 5.35-8.63 5.96l-109.8 6.96c-4.97 0.32-9.47-1.59-9.85-4.63-0.37-3.03 3.23-5.36 9.19-5.77z" stroke="#000" fill="url(#radialGradient5335)"/>
<path id="path3546" d="m358.46 831.16l109.79-6.97c4.98-0.32 8.79 1.49 9.17 4.6s-3.1 5.13-8.5 5.8l-109.8 6.97c-4.97 0.32-10.55-1.31-10.67-4.65-0.13-3.42 4.03-5.41 10.01-5.75z" stroke="#565352" fill="url(#radialGradient5337)"/>
</g>
<g id="g3548" transform="translate(134.21 53.089)">
<path id="path3550" d="m357.51 831.16l109.79-6.96c4.98-0.32 9.6 1.57 9.97 4.61s-4.01 5.41-9.31 5.79l-109.79 6.97c-5.64 0.29-9.51-2.3-9.57-4.64-0.06-2.35 2.15-5.29 8.91-5.77z" stroke="#000" fill="url(#radialGradient5329)"/>
<path id="path3552" d="m357.91 831.16l109.8-6.96c6.82-0.5 9.32 2.79 9.45 4.88 0.14 2.17-3.82 5.21-8.79 5.52l-109.79 6.97c-4.98 0.31-9.45-1.3-9.82-4.34s2.14-5.5 9.15-6.07z" stroke="#000" fill="url(#radialGradient5331)"/>
<path id="path3554" d="m358.37 831.16l109.8-6.97c4.54-0.29 8.02 1.23 8.61 4.23 0.6 3.06-2.98 5.86-7.95 6.18l-109.8 6.96c-4.97 0.32-9.59-1.57-9.96-4.61-0.38-3.04 3.87-5.54 9.3-5.79z" stroke="#000" fill="url(#radialGradient5333)"/>
<path id="path3556" d="m357.98 831.16l109.8-6.97c4.97-0.31 8.78 1.35 9.29 4.45 0.52 3.11-3.08 5.35-8.63 5.96l-109.8 6.96c-4.97 0.32-9.47-1.59-9.85-4.63-0.37-3.03 3.23-5.36 9.19-5.77z" stroke="#000" fill="url(#radialGradient5335)"/>
<path id="path3558" d="m358.46 831.16l109.79-6.97c4.98-0.32 8.79 1.49 9.17 4.6s-3.1 5.13-8.5 5.8l-109.8 6.97c-4.97 0.32-10.55-1.31-10.67-4.65-0.13-3.42 4.03-5.41 10.01-5.75z" stroke="#565352" fill="url(#radialGradient5337)"/>
</g>
</g>
</g>
<g id="g3560" transform="matrix(.23216 0 0 .11037 265.8 686.62)">
<path id="path3562" d="m543.29 847.29h110.02c4.98 0 8.68 3.83 8.86 9.68s-3.42 9.21-8.86 9.82h-110.02c-4.98 0-10.44-3.69-10.35-9.95 0.09-6.39 4.37-9.62 10.35-9.55z" stroke="#565352" fill="#fff"/>
<g id="g3564" transform="translate(187.8 49.415)">
<path id="path3566" d="m354.49 797.88h110.02c4.98 0 9.48 4.09 9.65 9.8 0.18 5.71-4.34 9.62-9.65 9.7h-110.02c-5.64-0.13-9.34-5.41-9.26-9.79 0.09-4.39 2.49-9.62 9.26-9.71z" stroke="#000" fill="url(#radialGradient5389)"/>
<path id="path3568" d="m355.07 797.86h110.02c6.83-0.12 9.13 6.31 9.13 10.24 0 4.06-4.15 9.26-9.13 9.26h-110.02c-4.98 0-9.34-3.54-9.52-9.26-0.18-5.71 2.49-10.02 9.52-10.24z" stroke="#000" fill="url(#radialGradient5391)"/>
<path id="path3570" d="m355.6 797.87h110.02c4.55 0 7.93 3.25 8.33 8.91 0.41 5.79-3.35 10.59-8.33 10.59h-110.02c-4.98 0-9.47-4.08-9.65-9.8-0.18-5.71 4.21-9.88 9.65-9.7z" stroke="#000" fill="url(#radialGradient5393)"/>
<path id="path3572" d="m355.09 797.89h110.02c4.98 0 8.67 3.56 8.99 9.41 0.32 5.86-3.42 9.62-8.99 10.09h-110.02c-4.98 0-9.36-4.1-9.54-9.81-0.17-5.72 3.56-9.62 9.54-9.69z" stroke="#000" fill="url(#radialGradient5395)"/>
<path id="path3574" d="m355.49 797.86h110.02c4.98 0 8.67 3.83 8.85 9.68s-3.41 9.21-8.85 9.82h-110.02c-4.98 0-10.45-3.69-10.36-9.95 0.1-6.39 4.38-9.62 10.36-9.55z" stroke="#565352" fill="url(#radialGradient5397)"/>
</g>
</g>
</g>
</g>
<metadata>
<rdf:RDF>
<cc:Work>
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/>
<dc:publisher>
<cc:Agent rdf:about="http://openclipart.org/">
<dc:title>Openclipart</dc:title>
</cc:Agent>
</dc:publisher>
<dc:title>basic light bulb</dc:title>
<dc:date>2007-02-16T04:05:06</dc:date>
<dc:description/>
<dc:source>https://openclipart.org/detail/175842/Clipart-by-drunken_duck</dc:source>
<dc:creator>
<cc:Agent>
<dc:title>drunken_duck</dc:title>
</cc:Agent>
</dc:creator>
<dc:subject>
<rdf:Bag>
<rdf:li>light</rdf:li>
<rdf:li>bulb</rdf:li>
<rdf:li>electricity</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
<cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/">
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
</cc:License>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -128,6 +128,15 @@
},
"then": "circle:white;./assets/layers/recycling/engine_oil.svg"
},
{
"if": {
"and": [
"_waste_amount=1",
"recycling:fluorescent_tubes=yes"
]
},
"then": "circle:white;./assets/layers/recycling/fluorescent_tubes.svg"
},
{
"if": {
"and": [
@ -160,6 +169,15 @@
},
"then": "circle:white;./assets/layers/recycling/garden_waste.svg"
},
{
"if": {
"and": [
"_waste_amount=1",
"recycling:light_bulbs=yes"
]
},
"then": "circle:white;./assets/layers/recycling/light_bulbs.svg"
},
{
"if": {
"and": [
@ -303,6 +321,15 @@
},
"then": "circle:white;./assets/layers/recycling/engine_oil.svg"
},
{
"if": {
"and": [
"_waste_amount>1",
"recycling:fluorescent_tubes=yes"
]
},
"then": "circle:white;./assets/layers/recycling/fluorescent_tubes.svg"
},
{
"if": {
"and": [
@ -335,6 +362,15 @@
},
"then": "circle:white;./assets/layers/recycling/garden_waste.svg"
},
{
"if": {
"and": [
"_waste_amount>1",
"recycling:light_bulbs=yes"
]
},
"then": "circle:white;./assets/layers/recycling/light_bulbs.svg"
},
{
"if": {
"and": [
@ -500,7 +536,9 @@
"recycling:clothes=",
"recycling:cooking_oil=",
"recycling:engine_oil=",
"recycling:fluorescent_tubes=",
"recycling:green_waste=",
"recycling:light_bulbs=",
"recycling:organic=",
"recycling:glass_bottles=",
"recycling:glass=",
@ -698,6 +736,18 @@
"class": "medium"
}
},
{
"if": "recycling:fluorescent_tubes=yes",
"ifnot": "recycling:fluorescent_tubes=",
"then": {
"en": "Fluorescent tubes can be recycled here",
"nl": "TL-buizen kunnen hier gerecycled worden"
},
"icon": {
"path": "./assets/layers/recycling/fluorescent_tubes.svg",
"class": "medium"
}
},
{
"if": "recycling:green_waste=yes",
"ifnot": "recycling:green_waste=",
@ -761,6 +811,18 @@
"class": "medium"
}
},
{
"if": "recycling:light_bulbs=yes",
"ifnot": "recycling:light_bulbs=",
"then": {
"en": "Light bulbs can be recycled here",
"nl": "Lampen kunnen hier gerecycled worden"
},
"icon": {
"path": "./assets/layers/recycling/light_bulbs.svg",
"class": "medium"
}
},
{
"if": "recycling:newspaper=yes",
"ifnot": "recycling:newspaper=",
@ -1047,6 +1109,13 @@
},
"osmTags": "recycling:engine_oil=yes"
},
{
"question": {
"en": "Recycling of fluorescent tubes",
"nl": "Recycling van tl-buizen"
},
"osmTags": "recycling:fluorescent_tubes=yes"
},
{
"question": {
"en": "Recycling of green waste",
@ -1081,6 +1150,13 @@
},
"osmTags": "recycling:glass=yes"
},
{
"question": {
"en": "Recycling of light bulbs",
"nl": "Recycling van lampen"
},
"osmTags": "recycling:light_bulbs=yes"
},
{
"question": {
"en": "Recycling of newspapers",

View file

@ -528,7 +528,8 @@
"de": "eine an einer Wand montierte Überwachungskamera"
},
"preciseInput": {
"snapToLayer": "walls_and_buildings"
"snapToLayer": "walls_and_buildings",
"preferredBackground": ["photo", "osmbasedmap","map"]
}
}
],
@ -597,4 +598,4 @@
"nl": "Deze laag toont bewakingscamera's en laat toe om de informatie te verrijken en om nieuwe camera\"s toe te voegen",
"de": "Diese Ebene zeigt die Überwachungskameras an und ermöglicht es, Informationen zu aktualisieren und neue Kameras hinzuzufügen"
}
}
}

View file

@ -68,7 +68,7 @@
"then": {
"en": "This ticket validator accepts OV-Chipkaart"
},
"hideInAnswer": "_country!=nl"
"hideInAnswer": true
},
{
"if": "payment:ov-chipkaart=yes",
@ -76,7 +76,7 @@
"then": {
"en": "This ticket validator accepts OV-Chipkaart"
},
"hideInAnswer": true
"hideInAnswer": "_country!=nl"
}
]
}
@ -90,6 +90,9 @@
"title": {
"en": "a ticket validator",
"de": "einen Fahrkartenentwerter"
},
"description": {
"en": "A ticket validator to validate a public transport ticket. This can be either a digital reader, reading a card or ticket, or a machine stamping or punching a ticket."
}
}
],
@ -103,4 +106,4 @@
]
}
]
}
}

View file

@ -12,7 +12,7 @@
"license": "CC-BY",
"authors": [
"asianson.design",
" Pieter Vander Vennet"
"Pieter Vander Vennet"
],
"sources": [
"https://thenounproject.com/term/urinal/1307984/"
@ -22,6 +22,8 @@
"path": "wheelchair.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://wiki.openstreetmap.org/wiki/File:Wheelchair_symbol.svg"
]
}
]

View file

@ -233,7 +233,7 @@
"id": "toilet-charge"
},
{
"builtin": "payment-options",
"builtin": "payment-options-split",
"override": {
"condition": "fee=yes"
}

View file

@ -23,6 +23,8 @@
"path": "wheelchair.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://wiki.openstreetmap.org/wiki/File:Wheelchair_symbol.svg"
]
}
]

View file

@ -32,7 +32,7 @@
}
},
"description": {
"en": "This is a public waste basket, thrash can, where you can throw away your thrash.",
"en": "This is a public waste basket, trash can, where you can throw away your trash.",
"nl": "Dit is een publieke vuilnisbak waar je je afval kan weggooien.",
"de": "Dies ist ein öffentlicher Abfalleimer, in den Sie Ihren Müll entsorgen können.",
"hu": "Ez egy nyilvános szemétkosár vagy kuka, ahová kidobhatod a szemetedet.",
@ -369,4 +369,4 @@
}
}
]
}
}

View file

@ -1,8 +0,0 @@
[
{
"path": "watermill.svg",
"license": "CC0",
"authors": [],
"sources": []
}
]

View file

@ -1,191 +0,0 @@
{
"id": "watermill",
"name": {
"nl": "Watermolens",
"en": "Watermill",
"de": "Wassermühlen",
"ru": "Водяная мельница",
"id": "Kincir Air",
"fr": "Moulin à eau",
"ca": "Molí d'aigua",
"da": "Vandmølle"
},
"minzoom": 12,
"source": {
"osmTags": {
"and": [
"man_made=watermill"
]
}
},
"title": {
"render": {
"nl": "Watermolens"
},
"mappings": [
{
"if": {
"and": [
"name:nl~*"
]
},
"then": {
"nl": "{name:nl}"
}
},
{
"if": {
"and": [
"name~*"
]
},
"then": {
"nl": "{name}"
}
}
]
},
"description": {
"nl": "Watermolens"
},
"tagRenderings": [
"images",
{
"render": {
"nl": "De toegankelijkheid van dit gebied is: {access:description}"
},
"question": {
"nl": "Is dit gebied toegankelijk?"
},
"freeform": {
"key": "access:description"
},
"mappings": [
{
"if": {
"and": [
"access=yes",
"fee="
]
},
"then": {
"nl": "Vrij toegankelijk"
}
},
{
"if": {
"and": [
"access=no",
"fee="
]
},
"then": {
"nl": "Niet toegankelijk"
}
},
{
"if": {
"and": [
"access=private",
"fee="
]
},
"then": {
"nl": "Niet toegankelijk, want privégebied"
}
},
{
"if": {
"and": [
"access=permissive",
"fee="
]
},
"then": {
"nl": "Toegankelijk, ondanks dat het privegebied is"
}
},
{
"if": {
"and": [
"access=guided",
"fee="
]
},
"then": {
"nl": "Enkel toegankelijk met een gids of tijdens een activiteit"
}
},
{
"if": {
"and": [
"access=yes",
"fee=yes"
]
},
"then": {
"nl": "Toegankelijk mits betaling"
}
}
],
"id": "Access tag"
},
{
"render": {
"nl": "Beheer door {operator}"
},
"question": {
"nl": "Wie beheert dit pad?"
},
"freeform": {
"key": "operator"
},
"mappings": [
{
"if": {
"and": [
"operator=Natuurpunt"
]
},
"then": {
"nl": "Dit gebied wordt beheerd door Natuurpunt"
},
"icon": {
"path": "./assets/themes/buurtnatuur/Natuurpunt.jpg",
"class": "small"
}
},
{
"if": {
"and": [
"operator~(n|N)atuurpunt.*"
]
},
"then": {
"nl": "Dit gebied wordt beheerd door {operator}"
},
"hideInAnswer": true,
"icon": {
"path": "./assets/themes/buurtnatuur/Natuurpunt.jpg",
"class": "small"
}
}
],
"id": "Operator tag"
}
],
"mapRendering": [
{
"icon": {
"render": "./assets/layers/watermill/watermill.svg"
},
"iconSize": {
"render": "50,50,center"
},
"location": [
"point",
"centroid"
]
}
]
}

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg" width="281px" height="374px" viewBox="0 0 281 374" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 173.078125 146.714844 L 281 39.171875 L 269.894531 28.089844 L 257.675781 15.707031 L 159.9375 113.457031 C 154.6875 108.464844 147.746094 105.632812 140.5 105.511719 L 136.058594 105.511719 L 34.617188 4.621094 L 23.511719 15.707031 L 11.105469 27.902344 L 111.066406 127.871094 C 110.085938 130.175781 109.398438 132.597656 109.03125 135.074219 L 107.179688 143.945312 L 0 250.566406 L 11.105469 261.652344 L 23.324219 274.035156 L 94.964844 202.519531 L 66.085938 342.21875 L 45.167969 342.21875 L 45.167969 369.378906 L 240.644531 369.378906 L 240.644531 342.21875 L 218.988281 342.21875 L 193.070312 231.347656 L 246.382812 284.5625 L 257.488281 273.480469 L 269.894531 261.28125 L 178.820312 170.925781 Z M 162.898438 340.183594 L 122.171875 340.183594 L 122.171875 287.152344 C 122.171875 276.03125 131.207031 267.007812 142.351562 267.007812 C 147.765625 266.910156 152.992188 268.992188 156.859375 272.777344 C 160.722656 276.566406 162.898438 281.746094 162.898438 287.152344 Z M 162.898438 340.183594 "/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -499,12 +499,6 @@
"https://www.iconpacks.net/free-icon-pack/gender-107.html"
]
},
{
"path": "gender_female.svg",
"license": "CC0",
"authors": [],
"sources": []
},
{
"path": "gender_female.svg",
"license": "CC0",
@ -525,12 +519,6 @@
"https://www.iconpacks.net/free-icon-pack/gender-107.html"
]
},
{
"path": "gender_male.svg",
"license": "CC0",
"authors": [],
"sources": []
},
{
"path": "gender_male.svg",
"license": "CC0",
@ -551,12 +539,6 @@
"https://www.iconpacks.net/free-icon-pack/gender-107.html"
]
},
{
"path": "gender_trans.svg",
"license": "CC0",
"authors": [],
"sources": []
},
{
"path": "gender_trans.svg",
"license": "CC0",
@ -899,7 +881,7 @@
},
{
"path": "none.svg",
"license": "CC0",
"license": "trivial",
"authors": [],
"sources": []
},
@ -1277,9 +1259,11 @@
},
{
"path": "teardrop_with_hole_green.svg",
"license": "CC0",
"license": "Creative Commons 4.0 BY-NC",
"authors": [],
"sources": []
"sources": [
"https://pngimg.com/image/46283"
]
},
{
"path": "translate.svg",

View file

@ -919,6 +919,7 @@
"icon": "./assets/tagRenderings/coins.svg",
"then": {
"en": "Coins are accepted here",
"nl": "Muntgeld wordt hier aanvaard",
"de": "Münzen werden hier akzeptiert"
}
},
@ -928,6 +929,7 @@
"icon": "./assets/tagRenderings/notes.svg",
"then": {
"en": "Bank notes are accepted here",
"nl": "Bankbiljetten worden hier aanvaard",
"de": "Geldscheine werden hier akzeptiert"
}
},
@ -937,6 +939,7 @@
"icon": "./assets/tagRenderings/payment_card.svg",
"then": {
"en": "Debit cards are accepted here",
"nl": "Betalen met debetkaarten kan hier",
"de": "Debitkarten werden hier akzeptiert"
}
},
@ -946,6 +949,7 @@
"icon": "./assets/tagRenderings/payment_card.svg",
"then": {
"en": "Credit cards are accepted here",
"nl": "Betalen met creditkaarten kan hier",
"de": "Kreditkarten werden hier akzeptiert"
}
}
@ -1664,4 +1668,4 @@
"es": "El nombre de red es <b>{internet_access:ssid}</b>"
}
}
}
}

View file

@ -13,8 +13,13 @@
{
"path": "car.svg",
"license": "CC0",
"authors": [],
"sources": []
"authors": [
"Simon Child",
"The Noun Project"
],
"sources": [
"https://thenounproject.com/icon/electric-car-55511/"
]
},
{
"path": "logo.svg",

View file

@ -39,6 +39,7 @@
"socialImage": "./assets/themes/cyclofix/logo.svg",
"layers": [
"bike_cafe",
"bike_shop",
{
"builtin": [
"bicycle_rental"
@ -87,7 +88,6 @@
]
}
},
"bike_shop",
{
"builtin": "bicycle_library",
"override": {
@ -130,4 +130,4 @@
}
}
]
}
}

View file

@ -3,37 +3,49 @@
"path": "bench.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "birdhide.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "drips.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "information.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "information_board.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "nature_reserve.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "natuurpunt.png",
@ -49,72 +61,88 @@
"path": "parking.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "parkingbike.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "parkingmotor.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "parkingwheels.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "picnic_table.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "pushchair.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "toilets.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "trail.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "urinal.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "walk_wheelchair.svg",
"license": "CC0",
"authors": [],
"sources": []
},
{
"path": "watermill.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
},
{
"path": "wheelchair.svg",
"license": "CC0",
"authors": [],
"sources": []
"sources": [
"https://osoc.be/editions/2021/nature-moves"
]
}
]

View file

@ -323,24 +323,6 @@
]
}
},
{
"builtin": "watermill",
"override": {
"minzoom": "14",
"source": {
"geoJson": "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/natuurpunt_cache/natuurpunt_{layer}_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 12,
"isOsmCache": true
},
"mapRendering": [
{
"icon": {
"render": "circle:#FE6F32;./assets/themes/natuurpunt/watermill.svg"
}
}
]
}
},
{
"builtin": "gps_track",
"override": {
@ -353,4 +335,4 @@
"enableIframePopout": false,
"enableBackgroundLayerSelection": false,
"enableNoteImports": false
}
}

View file

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="-10 8 15.18 19.74"
version="1.1"
id="svg1054"
sodipodi:docname="watermill.svg"
width="15.18"
height="19.74"
inkscape:version="1.1.1 (1:1.1+202109281949+c3084ef5ed)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1056"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="6.0407534"
inkscape:cx="-31.949657"
inkscape:cy="10.594705"
inkscape:current-layer="svg1054" />
<defs
id="defs1048">
<style
id="style1046">.cls-1{fill:#fff;}</style>
</defs>
<g
id="Layer_2"
data-name="Layer 2"
transform="matrix(0.58392824,0,0,0.58392824,-6.8420153,12.106628)">
<g
id="Layer_1-2"
data-name="Layer 1">
<path
class="cls-1"
d="M 9.35,7.69 15.18,1.87 14.58,1.27 13.92,0.6 8.64,5.89 A 1.56,1.56 0 0 0 7.59,5.46 H 7.35 L 1.87,0 1.27,0.6 0.6,1.26 6,6.67 A 1.6,1.6 0 0 0 5.89,7.06 v 0 L 5.79,7.54 0,13.31 l 0.6,0.6 0.66,0.67 3.87,-3.87 -1.56,7.56 H 2.44 v 1.47 H 13 v -1.47 h -1.17 l -1.4,-6 2.88,2.88 0.6,-0.6 0.67,-0.66 L 9.66,9 Z M 8.8,18.16 H 6.6 V 15.29 A 1.09,1.09 0 0 1 7.69,14.2 v 0 a 1.09,1.09 0 0 1 1.11,1.09 z"
id="path1050" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -189,6 +189,25 @@
]
}
},
{
"builtin": "parking_spaces",
"override": {
"source": {
"osmTags": "parking_space=disabled"
},
"mapRendering": [
{
"icon": {
"mappings": null
}
}
]
},
"hideTagRenderingsWithLabels": [
"type",
"capacity"
]
},
{
"builtin": "shops",
"override": {

View file

@ -107,9 +107,11 @@
},
{
"path": "logo.svg",
"license": "CC0",
"license": "Creative Commons 4.0 BY-NC",
"authors": [],
"sources": []
"sources": [
"https://pngimg.com/image/46283"
]
},
{
"path": "observation_platform.svg",

View file

@ -2,7 +2,9 @@
{
"path": "icon.svg",
"license": "CC0; trivial",
"authors": [],
"authors": [
"Pieter Vander Vennet"
],
"sources": []
}
]

View file

@ -913,6 +913,7 @@
"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",
"split": "Split",
"splitAgain": "Split this road again",
"splitTitle": "Choose on the map where the properties of this road change"
},
"translations": {

View file

@ -881,31 +881,31 @@
"2": {
"then": "Ací es poden reciclar llaunes"
},
"7": {
"8": {
"then": "Ací es poden reciclar residus orgànics"
},
"8": {
"9": {
"then": "Ací es poden reciclar ampolles de vidre"
},
"9": {
"10": {
"then": "Ací es pot reciclar vidre"
},
"10": {
"12": {
"then": "Ací es poden reciclar diaris"
},
"11": {
"13": {
"then": "Ací es pot reciclar paper"
},
"12": {
"14": {
"then": "Ací es poden reciclar ampolles de plàstic"
},
"13": {
"15": {
"then": "Ací es poden reciclar envasos de plàstic"
},
"14": {
"16": {
"then": "Ací es pot reciclar plàstic"
},
"20": {
"22": {
"then": "Ací es pot reciclar el rebuig"
}
}
@ -1052,9 +1052,6 @@
"render": "Paperera"
}
},
"watermill": {
"name": "Molí d'aigua"
},
"windturbine": {
"title": {
"mappings": {

View file

@ -6322,37 +6322,37 @@
"6": {
"question": "Recycling von Motoröl"
},
"7": {
"8": {
"question": "Recycling von Grünabfällen"
},
"8": {
"9": {
"question": "Recycling von Glasflaschen"
},
"9": {
"10": {
"question": "Recycling von Glas"
},
"10": {
"12": {
"question": "Recycling von Zeitungen"
},
"11": {
"13": {
"question": "Recycling von Papier"
},
"12": {
"14": {
"question": "Recycling von Plastikflaschen"
},
"13": {
"15": {
"question": "Recycling von Kunststoffverpackungen"
},
"14": {
"16": {
"question": "Recycling von Kunststoffen"
},
"15": {
"17": {
"question": "Recycling von Metallschrott"
},
"16": {
"18": {
"question": "Recycling von Elektrokleingeräten"
},
"17": {
"19": {
"question": "Recycling von Restabfällen"
}
}
@ -6411,49 +6411,49 @@
"5": {
"then": "Motoröl kann hier recycelt werden"
},
"6": {
"7": {
"then": "Grünabfälle können hier recycelt werden"
},
"7": {
"8": {
"then": "Bio-Abfall kann hier recycelt werden"
},
"8": {
"9": {
"then": "Glasflaschen können hier recycelt werden"
},
"9": {
"10": {
"then": "Glas kann hier recycelt werden"
},
"10": {
"12": {
"then": "Zeitungen können hier recycelt werden"
},
"11": {
"13": {
"then": "Papier kann hier recycelt werden"
},
"12": {
"14": {
"then": "Plastikflaschen können hier recycelt werden"
},
"13": {
"15": {
"then": "Kunststoffverpackungen können hier recycelt werden"
},
"14": {
"16": {
"then": "Kunststoff kann hier recycelt werden"
},
"15": {
"17": {
"then": "Metallschrott kann hier recycelt werden"
},
"16": {
"18": {
"then": "Schuhe können hier recycelt werden"
},
"17": {
"then": "Elektrokleingeräte können hier recycelt werden"
},
"18": {
"then": "Elektrokleingeräte können hier recycelt werden"
},
"19": {
"then": "Nadeln können hier recycelt werden"
"then": "Elektrokleingeräte können hier recycelt werden"
},
"20": {
"then": "Elektrokleingeräte können hier recycelt werden"
},
"21": {
"then": "Nadeln können hier recycelt werden"
},
"22": {
"then": "Restmüll kann hier recycelt werden"
}
},
@ -8308,9 +8308,6 @@
"render": "Mülltonne"
}
},
"watermill": {
"name": "Wassermühlen"
},
"windturbine": {
"description": "Moderne Windmühlen zur Stromerzeugung",
"name": "Windräder",

View file

@ -169,6 +169,15 @@
},
"atm": {
"description": "ATMS to withdraw money",
"filter": {
"1": {
"options": {
"0": {
"question": "With speech output"
}
}
}
},
"name": "ATMs",
"presets": {
"0": {
@ -186,6 +195,34 @@
"question": "What brand is this ATM?",
"render": "The brand of this ATM is {brand}"
},
"cash_in": {
"mappings": {
"0": {
"then": "You probably cannot deposit cash into this ATM"
},
"1": {
"then": "You can deposit cash into this ATM"
},
"2": {
"then": "You cannot deposit cash into this ATM"
}
},
"question": "Can you deposit cash into this ATM?"
},
"cash_out": {
"mappings": {
"0": {
"then": "You can withdraw cash from this ATM"
},
"1": {
"then": "You can withdraw cash from this ATM"
},
"2": {
"then": "You cannot withdraw cash from this ATM"
}
},
"question": "Can you withdraw cash from this ATM?"
},
"name": {
"render": "The name of this ATM is {name}"
},
@ -195,6 +232,26 @@
},
"question": "What company operates this ATM?",
"render": "The ATM is operated by {operator}"
},
"speech_output": {
"mappings": {
"0": {
"then": "This ATM has speech output, usually available through a headphone jack"
},
"1": {
"then": "This ATM does not have speech output"
}
},
"question": "Does this ATM have speech output for visually impaired users?"
},
"speech_output_language": {
"render": {
"special": {
"question": "In which languages does this ATM have speech output?",
"render_list_item": "This ATM has speech output in {language():font-bold}",
"render_single_language": "This ATM has speech output in {language():font-bold}"
}
}
}
},
"title": {
@ -6323,36 +6380,42 @@
"question": "Recycling of engine oil"
},
"7": {
"question": "Recycling of green waste"
"question": "Recycling of fluorescent tubes"
},
"8": {
"question": "Recycling of glass bottles"
"question": "Recycling of green waste"
},
"9": {
"question": "Recycling of glass"
"question": "Recycling of glass bottles"
},
"10": {
"question": "Recycling of newspapers"
"question": "Recycling of glass"
},
"11": {
"question": "Recycling of paper"
"question": "Recycling of light bulbs"
},
"12": {
"question": "Recycling of plastic bottles"
"question": "Recycling of newspapers"
},
"13": {
"question": "Recycling of plastic packaging"
"question": "Recycling of paper"
},
"14": {
"question": "Recycling of plastic"
"question": "Recycling of plastic bottles"
},
"15": {
"question": "Recycling of scrap metal"
"question": "Recycling of plastic packaging"
},
"16": {
"question": "Recycling of small electrical appliances"
"question": "Recycling of plastic"
},
"17": {
"question": "Recycling of scrap metal"
},
"18": {
"question": "Recycling of small electrical appliances"
},
"19": {
"question": "Recycling of residual waste"
}
}
@ -6412,48 +6475,54 @@
"then": "Engine oil can be recycled here"
},
"6": {
"then": "Green waste can be recycled here"
"then": "Fluorescent tubes can be recycled here"
},
"7": {
"then": "Organic waste can be recycled here"
"then": "Green waste can be recycled here"
},
"8": {
"then": "Glass bottles can be recycled here"
"then": "Organic waste can be recycled here"
},
"9": {
"then": "Glass can be recycled here"
"then": "Glass bottles can be recycled here"
},
"10": {
"then": "Newspapers can be recycled here"
"then": "Glass can be recycled here"
},
"11": {
"then": "Paper can be recycled here"
"then": "Light bulbs can be recycled here"
},
"12": {
"then": "Plastic bottles can be recycled here"
"then": "Newspapers can be recycled here"
},
"13": {
"then": "Plastic packaging can be recycled here"
"then": "Paper can be recycled here"
},
"14": {
"then": "Plastic can be recycled here"
"then": "Plastic bottles can be recycled here"
},
"15": {
"then": "Scrap metal can be recycled here"
"then": "Plastic packaging can be recycled here"
},
"16": {
"then": "Shoes can be recycled here"
"then": "Plastic can be recycled here"
},
"17": {
"then": "Small electrical appliances can be recycled here"
"then": "Scrap metal can be recycled here"
},
"18": {
"then": "Small electrical appliances can be recycled here"
"then": "Shoes can be recycled here"
},
"19": {
"then": "Needles can be recycled here"
"then": "Small electrical appliances can be recycled here"
},
"20": {
"then": "Small electrical appliances can be recycled here"
},
"21": {
"then": "Needles can be recycled here"
},
"22": {
"then": "Residual waste can be recycled here"
}
},
@ -7407,6 +7476,7 @@
"name": "Ticket Validators",
"presets": {
"0": {
"description": "A ticket validator to validate a public transport ticket. This can be either a digital reader, reading a card or ticket, or a machine stamping or punching a ticket.",
"title": "a ticket validator"
}
},
@ -8308,9 +8378,6 @@
"render": "Waste Disposal"
}
},
"watermill": {
"name": "Watermill"
},
"windturbine": {
"description": "Modern windmills generating electricity",
"name": "wind turbine",

View file

@ -3189,28 +3189,28 @@
"6": {
"question": "Reciclaje de aceite de motor"
},
"8": {
"9": {
"question": "Reciclaje de botellas de cristal"
},
"9": {
"10": {
"question": "Reciclaje de cristal"
},
"10": {
"12": {
"question": "Reciclaje de periódicos"
},
"11": {
"13": {
"question": "Reciclaje de papel"
},
"12": {
"14": {
"question": "Reciclaje de botellas de papel"
},
"13": {
"15": {
"question": "Reciclaje de embalajes plásticos"
},
"14": {
"16": {
"question": "Reciclaje de plástico"
},
"15": {
"17": {
"question": "Reciclaje de chatarra"
}
}
@ -3266,34 +3266,34 @@
"5": {
"then": "Aquí se puede reciclar aceite de motor"
},
"7": {
"8": {
"then": "Aquí se pueden reciclar residuos orgánicos"
},
"8": {
"9": {
"then": "Aquí se pueden reciclar botellas de cristal"
},
"9": {
"10": {
"then": "Aquí se puede reciclar cristal"
},
"10": {
"12": {
"then": "Aquí se pueden reciclar periódicos"
},
"11": {
"13": {
"then": "Aquí se puede reciclar papel"
},
"12": {
"14": {
"then": "Aquí se pueden reciclar botellas de plástico"
},
"13": {
"15": {
"then": "Aquí se pueden reciclar embalajes plásticos"
},
"14": {
"16": {
"then": "Aquí se puede reciclar plástico"
},
"15": {
"17": {
"then": "Aquí se puede reciclar chatarra"
},
"16": {
"18": {
"then": "Aquí se pueden reciclar zapatos"
}
},

View file

@ -1766,37 +1766,37 @@
"6": {
"question": "Riciclo di olio da motore"
},
"7": {
"8": {
"question": "Riciclo di umido"
},
"8": {
"9": {
"question": "Riciclo di bottiglie di vetro"
},
"9": {
"10": {
"question": "Riciclo di vetro"
},
"10": {
"12": {
"question": "Riciclo di giornali"
},
"11": {
"13": {
"question": "Riciclo di carta"
},
"12": {
"14": {
"question": "Riciclo di bottiglie di plastica"
},
"13": {
"15": {
"question": "Riciclo di confezioni di plastica"
},
"14": {
"16": {
"question": "Riciclo di plastica"
},
"15": {
"17": {
"question": "Riciclo di rottami metallici"
},
"16": {
"18": {
"question": "Riciclo di piccoli elettrodomestici"
},
"17": {
"19": {
"question": "Riciclo di secco"
}
}
@ -1855,49 +1855,49 @@
"5": {
"then": "Olio di motore"
},
"6": {
"7": {
"then": "Verde"
},
"7": {
"8": {
"then": "Umido"
},
"8": {
"9": {
"then": "Bottiglie di vetro"
},
"9": {
"10": {
"then": "Vetro"
},
"10": {
"12": {
"then": "Giornali"
},
"11": {
"13": {
"then": "Carta"
},
"12": {
"14": {
"then": "Bottiglie di platica"
},
"13": {
"15": {
"then": "Confezioni di plastica"
},
"14": {
"16": {
"then": "Plastica"
},
"15": {
"17": {
"then": "Rottami metallici"
},
"16": {
"18": {
"then": "Scarpe"
},
"17": {
"then": "Piccoli elettrodomestici"
},
"18": {
"then": "Piccoli elettrodomestici"
},
"19": {
"then": "Aghi e oggetti appuntiti"
"then": "Piccoli elettrodomestici"
},
"20": {
"then": "Piccoli elettrodomestici"
},
"21": {
"then": "Aghi e oggetti appuntiti"
},
"22": {
"then": "Secco"
}
},

View file

@ -5988,36 +5988,42 @@
"question": "Recycling van motorolie"
},
"7": {
"question": "Recycling van groen afval"
"question": "Recycling van tl-buizen"
},
"8": {
"question": "Recycling van glazen flessen"
"question": "Recycling van groen afval"
},
"9": {
"question": "Recycling van glas"
"question": "Recycling van glazen flessen"
},
"10": {
"question": "Recycling van kranten"
"question": "Recycling van glas"
},
"11": {
"question": "Recycling van papier"
"question": "Recycling van lampen"
},
"12": {
"question": "Recycling van plastic flessen"
"question": "Recycling van kranten"
},
"13": {
"question": "Recycling van plastic verpakking"
"question": "Recycling van papier"
},
"14": {
"question": "Recycling van plastic"
"question": "Recycling van plastic flessen"
},
"15": {
"question": "Recycling van oud metaal"
"question": "Recycling van plastic verpakking"
},
"16": {
"question": "Recycling van kleine elektrische apparaten"
"question": "Recycling van plastic"
},
"17": {
"question": "Recycling van oud metaal"
},
"18": {
"question": "Recycling van kleine elektrische apparaten"
},
"19": {
"question": "Recycling van restafval"
}
}
@ -6077,48 +6083,54 @@
"then": "Motorolie kan hier gerecycled worden"
},
"6": {
"then": "Groen afval kan hier gerecycled worden"
"then": "TL-buizen kunnen hier gerecycled worden"
},
"7": {
"then": "Organisch afval kan hier gerecycled worden"
"then": "Groen afval kan hier gerecycled worden"
},
"8": {
"then": "Glazen flessen kunnen hier gerecycled worden"
"then": "Organisch afval kan hier gerecycled worden"
},
"9": {
"then": "Glas kan hier gerecycled worden"
"then": "Glazen flessen kunnen hier gerecycled worden"
},
"10": {
"then": "Kranten kunnen hier gerecycled worden"
"then": "Glas kan hier gerecycled worden"
},
"11": {
"then": "Papier kan hier gerecycled worden"
"then": "Lampen kunnen hier gerecycled worden"
},
"12": {
"then": "Plastic flessen kunnen hier gerecycled worden"
"then": "Kranten kunnen hier gerecycled worden"
},
"13": {
"then": "Plastic verpakking kan hier gerecycled worden"
"then": "Papier kan hier gerecycled worden"
},
"14": {
"then": "Plastic kan hier gerecycled worden"
"then": "Plastic flessen kunnen hier gerecycled worden"
},
"15": {
"then": "Oud metaal kan hier gerecycled worden"
"then": "Plastic verpakking kan hier gerecycled worden"
},
"16": {
"then": "Schoenen kunnen hier gerecycled worden"
"then": "Plastic kan hier gerecycled worden"
},
"17": {
"then": "Kleine elektrische apparaten kunnen hier gerecycled worden"
"then": "Oud metaal kan hier gerecycled worden"
},
"18": {
"then": "Kleine elektrische apparaten kunnen hier gerecycled worden"
"then": "Schoenen kunnen hier gerecycled worden"
},
"19": {
"then": "Injectienaalden kunnen hier gerecycled worden"
"then": "Kleine elektrische apparaten kunnen hier gerecycled worden"
},
"20": {
"then": "Kleine elektrische apparaten kunnen hier gerecycled worden"
},
"21": {
"then": "Injectienaalden kunnen hier gerecycled worden"
},
"22": {
"then": "Restafval kan hier gerecycled worden"
}
},
@ -7872,59 +7884,6 @@
"render": "Afvalbak"
}
},
"watermill": {
"description": "Watermolens",
"name": "Watermolens",
"tagRenderings": {
"Access tag": {
"mappings": {
"0": {
"then": "Vrij toegankelijk"
},
"1": {
"then": "Niet toegankelijk"
},
"2": {
"then": "Niet toegankelijk, want privégebied"
},
"3": {
"then": "Toegankelijk, ondanks dat het privegebied is"
},
"4": {
"then": "Enkel toegankelijk met een gids of tijdens een activiteit"
},
"5": {
"then": "Toegankelijk mits betaling"
}
},
"question": "Is dit gebied toegankelijk?",
"render": "De toegankelijkheid van dit gebied is: {access:description}"
},
"Operator tag": {
"mappings": {
"0": {
"then": "Dit gebied wordt beheerd door Natuurpunt"
},
"1": {
"then": "Dit gebied wordt beheerd door {operator}"
}
},
"question": "Wie beheert dit pad?",
"render": "Beheer door {operator}"
}
},
"title": {
"mappings": {
"0": {
"then": "{name:nl}"
},
"1": {
"then": "{name}"
}
},
"render": "Watermolens"
}
},
"windturbine": {
"description": "Windturbines (moderne windmolens die elektriciteit genereren)",
"name": "windturbine",

View file

@ -179,6 +179,24 @@
}
}
},
"payment-options-split": {
"override": {
"mappings+": {
"0": {
"then": "Muntgeld wordt hier aanvaard"
},
"1": {
"then": "Bankbiljetten worden hier aanvaard"
},
"2": {
"then": "Betalen met debetkaarten kan hier"
},
"3": {
"then": "Betalen met creditkaarten kan hier"
}
}
}
},
"phone": {
"question": "Wat is het telefoonnummer van {title()}?"
},

View file

@ -443,14 +443,14 @@
},
"onwheels": {
"layers": {
"18": {
"19": {
"override": {
"=title": {
"render": "Estadístiques"
}
}
},
"19": {
"20": {
"override": {
"+tagRenderings": {
"0": {

View file

@ -680,14 +680,14 @@
}
}
},
"18": {
"19": {
"override": {
"=title": {
"render": "Statistikker"
}
}
},
"19": {
"20": {
"override": {
"+tagRenderings": {
"0": {

View file

@ -909,14 +909,14 @@
}
}
},
"18": {
"19": {
"override": {
"=title": {
"render": "Statistik"
}
}
},
"19": {
"20": {
"override": {
"+tagRenderings": {
"0": {

View file

@ -909,14 +909,14 @@
}
}
},
"18": {
"19": {
"override": {
"=title": {
"render": "Statistics"
}
}
},
"19": {
"20": {
"override": {
"+tagRenderings": {
"0": {

View file

@ -888,14 +888,14 @@
}
}
},
"18": {
"19": {
"override": {
"=title": {
"render": "Statistiques"
}
}
},
"19": {
"20": {
"override": {
"+tagRenderings": {
"0": {

View file

@ -441,14 +441,14 @@
},
"onwheels": {
"layers": {
"18": {
"19": {
"override": {
"=title": {
"render": "Statistikk"
}
}
},
"19": {
"20": {
"override": {
"+tagRenderings": {
"0": {

View file

@ -1036,14 +1036,14 @@
}
}
},
"18": {
"19": {
"override": {
"=title": {
"render": "Statistieken"
}
}
},
"19": {
"20": {
"override": {
"+tagRenderings": {
"0": {

View file

@ -168,14 +168,14 @@
},
"onwheels": {
"layers": {
"18": {
"19": {
"override": {
"=title": {
"render": "انکڑے"
}
}
},
"19": {
"20": {
"override": {
"+tagRenderings": {
"0": {

4119
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "mapcomplete",
"version": "0.0.5",
"version": "0.25.1",
"repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues",

View file

@ -16,7 +16,13 @@ import SharedTagRenderings from "../Customizations/SharedTagRenderings"
import { writeFile } from "fs"
import Translations from "../UI/i18n/Translations"
import * as themeOverview from "../assets/generated/theme_overview.json"
import DefaultGUI from "../UI/DefaultGUI"
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
import * as bookcases from "../assets/generated/themes/bookcases.json"
import { DefaultGuiState } from "../UI/DefaultGuiState"
import * as fakedom from "fake-dom"
import Hotkeys from "../UI/Base/Hotkeys"
function WriteFile(
filename,
html: BaseUIElement,
@ -217,5 +223,13 @@ WriteFile("./Docs/URL_Parameters.md", QueryParameterDocumentation.GenerateQueryP
"Logic/Web/QueryParameters.ts",
"UI/QueryParameterDocumentation.ts",
])
if (fakedom === undefined || window === undefined) {
throw "FakeDom not initialized"
}
new DefaultGUI(
new FeaturePipelineState(new LayoutConfig(<any>bookcases)),
new DefaultGuiState()
).setup()
WriteFile("./Docs/Hotkeys.md", Hotkeys.generateDocumentation(), [])
console.log("Generated docs")

View file

@ -279,7 +279,23 @@ function main(args: string[]) {
const invalidLicenses = licenseInfos
.filter((l) => (l.license ?? "") === "")
.map((l) => `License for artwork ${l.path} is empty string or undefined`)
let invalid = 0
for (const licenseInfo of licenseInfos) {
const isTrivial =
licenseInfo.license
.split(";")
.map((l) => l.trim().toLowerCase())
.indexOf("trivial") >= 0
if (licenseInfo.sources.length + licenseInfo.authors.length == 0 && !isTrivial) {
invalid++
invalidLicenses.push(
"Invalid license: No sources nor authors given in the license for " +
JSON.stringify(licenseInfo)
)
continue
}
for (const source of licenseInfo.sources) {
if (source == "") {
invalidLicenses.push(
@ -294,7 +310,7 @@ function main(args: string[]) {
}
}
if (missingLicenses.length > 0) {
if (missingLicenses.length > 0 || invalidLicenses.length) {
const msg = `There are ${missingLicenses.length} licenses missing and ${invalidLicenses.length} invalid licenses.`
console.log(missingLicenses.concat(invalidLicenses).join("\n"))
console.error(msg)

View file

@ -7594,7 +7594,7 @@ describe("GenerateCache", () => {
}
mkdirSync(dir + "np-cache")
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([
"natuurpunt",