forked from MapComplete/MapComplete
Merge branches
This commit is contained in:
commit
bae90d50bc
304 changed files with 49983 additions and 31589 deletions
|
|
@ -16,6 +16,7 @@ export class LastClickFeatureSource {
|
|||
private i: number = 0
|
||||
private readonly hasPresets: boolean
|
||||
private readonly hasNoteLayer: boolean
|
||||
public static readonly newPointElementId= "new_point_dialog"
|
||||
|
||||
constructor(layout: LayoutConfig) {
|
||||
this.hasNoteLayer = layout.hasNoteLayer()
|
||||
|
|
@ -46,7 +47,7 @@ export class LastClickFeatureSource {
|
|||
|
||||
public createFeature(lon: number, lat: number): Feature<Point, OsmTags> {
|
||||
const properties: OsmTags = {
|
||||
id: "new_point_dialog",
|
||||
id: LastClickFeatureSource.newPointElementId,
|
||||
has_note_layer: this.hasNoteLayer ? "yes" : "no",
|
||||
has_presets: this.hasPresets ? "yes" : "no",
|
||||
renderings: this.renderings.join(""),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import Constants from "../Models/Constants"
|
||||
|
||||
export interface MaprouletteTask {
|
||||
name: string,
|
||||
description: string,
|
||||
instruction: string
|
||||
}
|
||||
export default class Maproulette {
|
||||
public static readonly defaultEndpoint = "https://maproulette.org/api/v2"
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import Title from "../../UI/Base/Title"
|
|||
import Table from "../../UI/Base/Table"
|
||||
import ChangeLocationAction from "./Actions/ChangeLocationAction"
|
||||
import ChangeTagAction from "./Actions/ChangeTagAction"
|
||||
import FeatureSwitchState from "../State/FeatureSwitchState"
|
||||
|
||||
/**
|
||||
* Handles all changes made to OSM.
|
||||
|
|
@ -28,7 +29,7 @@ export class Changes {
|
|||
public readonly pendingChanges: UIEventSource<ChangeDescription[]> =
|
||||
LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
||||
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)
|
||||
public readonly state: { allElements?: IndexedFeatureSource; osmConnection: OsmConnection }
|
||||
public readonly state: { allElements?: IndexedFeatureSource; osmConnection: OsmConnection, featureSwitches?: FeatureSwitchState }
|
||||
public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined)
|
||||
public readonly backend: string
|
||||
public readonly isUploading = new UIEventSource(false)
|
||||
|
|
@ -45,7 +46,8 @@ export class Changes {
|
|||
allElements?: IndexedFeatureSource
|
||||
featurePropertiesStore?: FeaturePropertiesStore
|
||||
osmConnection: OsmConnection
|
||||
historicalUserLocations?: FeatureSource
|
||||
historicalUserLocations?: FeatureSource,
|
||||
featureSwitches?: FeatureSwitchState
|
||||
},
|
||||
leftRightSensitive: boolean = false
|
||||
) {
|
||||
|
|
@ -431,6 +433,9 @@ export class Changes {
|
|||
// Probably irrelevant, such as a new helper node
|
||||
return
|
||||
}
|
||||
if(this.state.featureSwitches.featureSwitchMorePrivacy?.data){
|
||||
return
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const recentLocationPoints = locations
|
||||
|
|
|
|||
|
|
@ -6,14 +6,6 @@ import Constants from "../../Models/Constants"
|
|||
import { Changes } from "./Changes"
|
||||
import { Utils } from "../../Utils"
|
||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
||||
import ChangeLocationAction from "./Actions/ChangeLocationAction"
|
||||
import ChangeTagAction from "./Actions/ChangeTagAction"
|
||||
import DeleteAction from "./Actions/DeleteAction"
|
||||
import LinkImageAction from "./Actions/LinkImageAction"
|
||||
import OsmChangeAction from "./Actions/OsmChangeAction"
|
||||
import RelationSplitHandler from "./Actions/RelationSplitHandler"
|
||||
import ReplaceGeometryAction from "./Actions/ReplaceGeometryAction"
|
||||
import SplitAction from "./Actions/SplitAction"
|
||||
|
||||
export interface ChangesetTag {
|
||||
key: string
|
||||
|
|
@ -232,7 +224,7 @@ export class ChangesetHandler {
|
|||
if (newMetaTag === undefined) {
|
||||
extraMetaTags.push({
|
||||
key: key,
|
||||
value: oldCsTags[key],
|
||||
value: oldCsTags[key]
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
|
@ -361,21 +353,22 @@ export class ChangesetHandler {
|
|||
}
|
||||
|
||||
private defaultChangesetTags(): ChangesetTag[] {
|
||||
const usedGps = this.changes.state["currentUserLocation"]?.features?.data?.length > 0
|
||||
const hasMorePrivacy = !!this.changes.state?.featureSwitches?.featureSwitchMorePrivacy?.data
|
||||
const setSourceAsSurvey = !hasMorePrivacy && usedGps
|
||||
return [
|
||||
["created_by", `MapComplete ${Constants.vNumber}`],
|
||||
["locale", Locale.language.data],
|
||||
["host", `${window.location.origin}${window.location.pathname}`],
|
||||
[
|
||||
"source",
|
||||
this.changes.state["currentUserLocation"]?.features?.data?.length > 0
|
||||
? "survey"
|
||||
: undefined,
|
||||
setSourceAsSurvey ? "survey" : undefined
|
||||
],
|
||||
["imagery", this.changes.state["backgroundLayer"]?.data?.id],
|
||||
["imagery", this.changes.state["backgroundLayer"]?.data?.id]
|
||||
].map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
aggregate: false,
|
||||
aggregate: false
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-ignore
|
||||
import { osmAuth } from "osm-auth"
|
||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||
import { OsmPreferences } from "./OsmPreferences"
|
||||
|
|
@ -6,7 +5,18 @@ import { Utils } from "../../Utils"
|
|||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { AuthConfig } from "./AuthConfig"
|
||||
import Constants from "../../Models/Constants"
|
||||
import OSMAuthInstance = OSMAuth.OSMAuthInstance
|
||||
|
||||
interface OsmUserInfo {
|
||||
id: number
|
||||
display_name: string
|
||||
account_created: string
|
||||
description: string
|
||||
contributor_terms: { agreed: boolean }
|
||||
roles: []
|
||||
changesets: { count: number }
|
||||
traces: { count: number }
|
||||
blocks: { received: { count: number; active: number } }
|
||||
}
|
||||
|
||||
export default class UserDetails {
|
||||
public loggedIn = false
|
||||
|
|
@ -31,7 +41,7 @@ export default class UserDetails {
|
|||
export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable"
|
||||
|
||||
export class OsmConnection {
|
||||
public auth: OSMAuthInstance
|
||||
public auth: osmAuth
|
||||
public userDetails: UIEventSource<UserDetails>
|
||||
public isLoggedIn: Store<boolean>
|
||||
public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
|
||||
|
|
@ -49,7 +59,7 @@ export class OsmConnection {
|
|||
private readonly _dryRun: Store<boolean>
|
||||
private readonly fakeUser: boolean
|
||||
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []
|
||||
private readonly _iframeMode: Boolean | boolean
|
||||
private readonly _iframeMode: boolean
|
||||
private readonly _singlePage: boolean
|
||||
private isChecking = false
|
||||
private readonly _doCheckRegularly
|
||||
|
|
@ -99,20 +109,19 @@ export class OsmConnection {
|
|||
ud.languages = ["en"]
|
||||
this.loadingStatus.setData("logged-in")
|
||||
}
|
||||
const self = this
|
||||
this.UpdateCapabilities()
|
||||
|
||||
this.isLoggedIn = this.userDetails.map(
|
||||
(user) =>
|
||||
user.loggedIn &&
|
||||
(self.apiIsOnline.data === "unknown" || self.apiIsOnline.data === "online"),
|
||||
(this.apiIsOnline.data === "unknown" || this.apiIsOnline.data === "online"),
|
||||
[this.apiIsOnline]
|
||||
)
|
||||
this.isLoggedIn.addCallback((isLoggedIn) => {
|
||||
if (self.userDetails.data.loggedIn == false && isLoggedIn == true) {
|
||||
if (this.userDetails.data.loggedIn == false && isLoggedIn == true) {
|
||||
// We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do
|
||||
// This means someone attempted to toggle this; so we attempt to login!
|
||||
self.AttemptLogin()
|
||||
this.AttemptLogin()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -120,37 +129,36 @@ export class OsmConnection {
|
|||
|
||||
this.updateAuthObject()
|
||||
if (!this.fakeUser) {
|
||||
self.CheckForMessagesContinuously()
|
||||
this.CheckForMessagesContinuously()
|
||||
}
|
||||
|
||||
this.preferencesHandler = new OsmPreferences(this.auth, this, this.fakeUser)
|
||||
|
||||
if (options.oauth_token?.data !== undefined) {
|
||||
console.log(options.oauth_token.data)
|
||||
const self = this
|
||||
this.auth.bootstrapToken(options.oauth_token.data, (err, result) => {
|
||||
console.log("Bootstrap token called back", err, result)
|
||||
self.AttemptLogin()
|
||||
this.AttemptLogin()
|
||||
})
|
||||
|
||||
options.oauth_token.setData(undefined)
|
||||
}
|
||||
if (this.auth.authenticated() && options.attemptLogin !== false) {
|
||||
if (!Utils.runningFromConsole && this.auth.authenticated() && options.attemptLogin !== false) {
|
||||
this.AttemptLogin()
|
||||
} else {
|
||||
console.log("Not authenticated")
|
||||
}
|
||||
}
|
||||
|
||||
public GetPreference(
|
||||
public GetPreference<T extends string = string>(
|
||||
key: string,
|
||||
defaultValue: string = undefined,
|
||||
options?: {
|
||||
documentation?: string
|
||||
prefix?: string
|
||||
}
|
||||
): UIEventSource<string> {
|
||||
return this.preferencesHandler.GetPreference(key, defaultValue, options)
|
||||
): UIEventSource<T | undefined> {
|
||||
return <UIEventSource<T>>this.preferencesHandler.GetPreference(key, defaultValue, options)
|
||||
}
|
||||
|
||||
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
|
||||
|
|
@ -192,7 +200,7 @@ export class OsmConnection {
|
|||
console.log("AttemptLogin called, but ignored as fakeUser is set")
|
||||
return
|
||||
}
|
||||
const self = this
|
||||
|
||||
console.log("Trying to log in...")
|
||||
this.updateAuthObject()
|
||||
|
||||
|
|
@ -202,33 +210,33 @@ export class OsmConnection {
|
|||
this.auth.xhr(
|
||||
{
|
||||
method: "GET",
|
||||
path: "/api/0.6/user/details",
|
||||
path: "/api/0.6/user/details"
|
||||
},
|
||||
function (err, details: XMLDocument) {
|
||||
(err, details: XMLDocument) => {
|
||||
if (err != null) {
|
||||
console.log("Could not login due to:", err)
|
||||
self.loadingStatus.setData("error")
|
||||
this.loadingStatus.setData("error")
|
||||
if (err.status == 401) {
|
||||
console.log("Clearing tokens...")
|
||||
// Not authorized - our token probably got revoked
|
||||
self.auth.logout()
|
||||
self.LogOut()
|
||||
this.auth.logout()
|
||||
this.LogOut()
|
||||
} else {
|
||||
console.log("Other error. Status:", err.status)
|
||||
self.apiIsOnline.setData("unreachable")
|
||||
this.apiIsOnline.setData("unreachable")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (details == null) {
|
||||
self.loadingStatus.setData("error")
|
||||
this.loadingStatus.setData("error")
|
||||
return
|
||||
}
|
||||
|
||||
// details is an XML DOM of user details
|
||||
let userInfo = details.getElementsByTagName("user")[0]
|
||||
const userInfo = details.getElementsByTagName("user")[0]
|
||||
|
||||
let data = self.userDetails.data
|
||||
const data = this.userDetails.data
|
||||
data.loggedIn = true
|
||||
console.log("Login completed, userinfo is ", userInfo)
|
||||
data.name = userInfo.getAttribute("display_name")
|
||||
|
|
@ -261,18 +269,18 @@ export class OsmConnection {
|
|||
data.home = { lat: lat, lon: lon }
|
||||
}
|
||||
|
||||
self.loadingStatus.setData("logged-in")
|
||||
this.loadingStatus.setData("logged-in")
|
||||
const messages = userInfo
|
||||
.getElementsByTagName("messages")[0]
|
||||
.getElementsByTagName("received")[0]
|
||||
data.unreadMessages = parseInt(messages.getAttribute("unread"))
|
||||
data.totalMessages = parseInt(messages.getAttribute("count"))
|
||||
|
||||
self.userDetails.ping()
|
||||
for (const action of self._onLoggedIn) {
|
||||
action(self.userDetails.data)
|
||||
this.userDetails.ping()
|
||||
for (const action of this._onLoggedIn) {
|
||||
action(this.userDetails.data)
|
||||
}
|
||||
self._onLoggedIn = []
|
||||
this._onLoggedIn = []
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -289,11 +297,11 @@ export class OsmConnection {
|
|||
public async interact(
|
||||
path: string,
|
||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||
header?: Record<string, string | number>,
|
||||
header?: Record<string, string>,
|
||||
content?: string,
|
||||
allowAnonymous: boolean = false
|
||||
): Promise<string> {
|
||||
let connection: OSMAuthInstance = this.auth
|
||||
const connection: osmAuth = this.auth
|
||||
if (allowAnonymous && !this.auth.authenticated()) {
|
||||
const possibleResult = await Utils.downloadAdvanced(
|
||||
`${this.Backend()}/api/0.6/${path}`,
|
||||
|
|
@ -310,15 +318,13 @@ export class OsmConnection {
|
|||
|
||||
return new Promise((ok, error) => {
|
||||
connection.xhr(
|
||||
<any>{
|
||||
{
|
||||
method,
|
||||
options: {
|
||||
header,
|
||||
},
|
||||
headers: header,
|
||||
content,
|
||||
path: `/api/0.6/${path}`,
|
||||
path: `/api/0.6/${path}`
|
||||
},
|
||||
function (err, response) {
|
||||
function(err, response) {
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
|
|
@ -329,32 +335,32 @@ export class OsmConnection {
|
|||
})
|
||||
}
|
||||
|
||||
public async post(
|
||||
public async post<T extends string>(
|
||||
path: string,
|
||||
content?: string,
|
||||
header?: Record<string, string | number>,
|
||||
header?: Record<string, string>,
|
||||
allowAnonymous: boolean = false
|
||||
): Promise<any> {
|
||||
return await this.interact(path, "POST", header, content, allowAnonymous)
|
||||
): Promise<T> {
|
||||
return <T> await this.interact(path, "POST", header, content, allowAnonymous)
|
||||
}
|
||||
|
||||
public async put(
|
||||
public async put<T extends string>(
|
||||
path: string,
|
||||
content?: string,
|
||||
header?: Record<string, string | number>
|
||||
): Promise<any> {
|
||||
return await this.interact(path, "PUT", header, content)
|
||||
header?: Record<string, string>
|
||||
): Promise<T> {
|
||||
return <T> await this.interact(path, "PUT", header, content)
|
||||
}
|
||||
|
||||
public async get(
|
||||
path: string,
|
||||
header?: Record<string, string | number>,
|
||||
header?: Record<string, string>,
|
||||
allowAnonymous: boolean = false
|
||||
): Promise<string> {
|
||||
return await this.interact(path, "GET", header, undefined, allowAnonymous)
|
||||
}
|
||||
|
||||
public closeNote(id: number | string, text?: string): Promise<void> {
|
||||
public closeNote(id: number | string, text?: string): Promise<string> {
|
||||
let textSuffix = ""
|
||||
if ((text ?? "") !== "") {
|
||||
textSuffix = "?text=" + encodeURIComponent(text)
|
||||
|
|
@ -362,17 +368,17 @@ export class OsmConnection {
|
|||
if (this._dryRun.data) {
|
||||
console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text)
|
||||
return new Promise((ok) => {
|
||||
ok()
|
||||
ok("")
|
||||
})
|
||||
}
|
||||
return this.post(`notes/${id}/close${textSuffix}`)
|
||||
}
|
||||
|
||||
public reopenNote(id: number | string, text?: string): Promise<void> {
|
||||
public reopenNote(id: number | string, text?: string): Promise<string> {
|
||||
if (this._dryRun.data) {
|
||||
console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text)
|
||||
return new Promise((ok) => {
|
||||
ok()
|
||||
return new Promise(resolve => {
|
||||
resolve("")
|
||||
})
|
||||
}
|
||||
let textSuffix = ""
|
||||
|
|
@ -398,7 +404,7 @@ export class OsmConnection {
|
|||
"notes.json",
|
||||
content,
|
||||
{
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
|
||||
},
|
||||
true
|
||||
)
|
||||
|
|
@ -439,7 +445,7 @@ export class OsmConnection {
|
|||
file: gpx,
|
||||
description: options.description,
|
||||
tags: options.labels?.join(",") ?? "",
|
||||
visibility: options.visibility,
|
||||
visibility: options.visibility
|
||||
}
|
||||
|
||||
if (!contents.description) {
|
||||
|
|
@ -447,9 +453,9 @@ export class OsmConnection {
|
|||
}
|
||||
const extras = {
|
||||
file:
|
||||
'; filename="' +
|
||||
"; filename=\"" +
|
||||
(options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) +
|
||||
'"\r\nContent-Type: application/gpx+xml',
|
||||
"\"\r\nContent-Type: application/gpx+xml"
|
||||
}
|
||||
|
||||
const boundary = "987654"
|
||||
|
|
@ -457,7 +463,7 @@ export class OsmConnection {
|
|||
let body = ""
|
||||
for (const key in contents) {
|
||||
body += "--" + boundary + "\r\n"
|
||||
body += 'Content-Disposition: form-data; name="' + key + '"'
|
||||
body += "Content-Disposition: form-data; name=\"" + key + "\""
|
||||
if (extras[key] !== undefined) {
|
||||
body += extras[key]
|
||||
}
|
||||
|
|
@ -468,7 +474,7 @@ export class OsmConnection {
|
|||
|
||||
const response = await this.post("gpx/create", body, {
|
||||
"Content-Type": "multipart/form-data; boundary=" + boundary,
|
||||
"Content-Length": body.length,
|
||||
"Content-Length": ""+body.length
|
||||
})
|
||||
const parsed = JSON.parse(response)
|
||||
console.log("Uploaded GPX track", parsed)
|
||||
|
|
@ -491,9 +497,9 @@ export class OsmConnection {
|
|||
{
|
||||
method: "POST",
|
||||
|
||||
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`,
|
||||
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`
|
||||
},
|
||||
function (err, _) {
|
||||
function(err) {
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
|
|
@ -508,7 +514,7 @@ export class OsmConnection {
|
|||
* To be called by land.html
|
||||
*/
|
||||
public finishLogin(callback: (previousURL: string) => void) {
|
||||
this.auth.authenticate(function () {
|
||||
this.auth.authenticate(function() {
|
||||
// Fully authed at this point
|
||||
console.log("Authentication successful!")
|
||||
const previousLocation = LocalStorageSource.Get("location_before_login")
|
||||
|
|
@ -517,28 +523,6 @@ export class OsmConnection {
|
|||
}
|
||||
|
||||
private updateAuthObject() {
|
||||
let pwaStandAloneMode = false
|
||||
try {
|
||||
if (Utils.runningFromConsole) {
|
||||
pwaStandAloneMode = true
|
||||
} else if (
|
||||
window.matchMedia("(display-mode: standalone)").matches ||
|
||||
window.matchMedia("(display-mode: fullscreen)").matches
|
||||
) {
|
||||
pwaStandAloneMode = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"Detecting standalone mode failed",
|
||||
e,
|
||||
". Assuming in browser and not worrying furhter"
|
||||
)
|
||||
}
|
||||
const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage
|
||||
|
||||
// In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
|
||||
// Same for an iframe...
|
||||
|
||||
this.auth = new osmAuth({
|
||||
client_id: this._oauth_config.oauth_client_id,
|
||||
url: this._oauth_config.url,
|
||||
|
|
@ -546,23 +530,22 @@ export class OsmConnection {
|
|||
redirect_uri: Utils.runningFromConsole
|
||||
? "https://mapcomplete.org/land.html"
|
||||
: window.location.protocol + "//" + window.location.host + "/land.html",
|
||||
singlepage: true,
|
||||
auto: true,
|
||||
singlepage: true, // We always use 'singlePage', it is the most stable - including in PWA
|
||||
auto: true
|
||||
})
|
||||
}
|
||||
|
||||
private CheckForMessagesContinuously() {
|
||||
const self = this
|
||||
if (this.isChecking) {
|
||||
return
|
||||
}
|
||||
Stores.Chronic(3 * 1000).addCallback((_) => {
|
||||
if (!(self.apiIsOnline.data === "unreachable" || self.apiIsOnline.data === "offline")) {
|
||||
Stores.Chronic(3 * 1000).addCallback(() => {
|
||||
if (!(this.apiIsOnline.data === "unreachable" || this.apiIsOnline.data === "offline")) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
console.log("Api is offline - trying to reconnect...")
|
||||
self.AttemptLogin()
|
||||
this.AttemptLogin()
|
||||
} catch (e) {
|
||||
console.log("Could not login due to", e)
|
||||
}
|
||||
|
|
@ -571,10 +554,10 @@ export class OsmConnection {
|
|||
if (!this._doCheckRegularly) {
|
||||
return
|
||||
}
|
||||
Stores.Chronic(60 * 5 * 1000).addCallback((_) => {
|
||||
if (self.isLoggedIn.data) {
|
||||
Stores.Chronic(60 * 5 * 1000).addCallback(() => {
|
||||
if (this.isLoggedIn.data) {
|
||||
try {
|
||||
self.AttemptLogin()
|
||||
this.AttemptLogin()
|
||||
} catch (e) {
|
||||
console.log("Could not login due to", e)
|
||||
}
|
||||
|
|
@ -592,19 +575,9 @@ export class OsmConnection {
|
|||
})
|
||||
}
|
||||
|
||||
private readonly _userInfoCache: Record<number, any> = {}
|
||||
private readonly _userInfoCache: Record<number, OsmUserInfo> = {}
|
||||
|
||||
public async getInformationAboutUser(id: number): Promise<{
|
||||
id: number
|
||||
display_name: string
|
||||
account_created: string
|
||||
description: string
|
||||
contributor_terms: { agreed: boolean }
|
||||
roles: []
|
||||
changesets: { count: number }
|
||||
traces: { count: number }
|
||||
blocks: { received: { count: number; active: number } }
|
||||
}> {
|
||||
public async getInformationAboutUser(id: number): Promise<OsmUserInfo> {
|
||||
if (id === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
|
|||
public readonly overpassMaxZoom: UIEventSource<number>
|
||||
public readonly osmApiTileSize: UIEventSource<number>
|
||||
public readonly backgroundLayerId: UIEventSource<string>
|
||||
public readonly featureSwitchMorePrivacy: UIEventSource<boolean>
|
||||
|
||||
public constructor(layoutToUse?: LayoutConfig) {
|
||||
super()
|
||||
|
|
@ -164,6 +165,14 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
|
|||
"If true, shows some extra debugging help such as all the available tags on every object"
|
||||
)
|
||||
|
||||
|
||||
this.featureSwitchMorePrivacy = QueryParameters.GetBooleanQueryParameter(
|
||||
"moreprivacy",
|
||||
layoutToUse.enableMorePrivacy,
|
||||
"If true, the location distance indication will not be written to the changeset and other privacy enhancing measures might be taken."
|
||||
)
|
||||
|
||||
|
||||
this.overpassUrl = QueryParameters.GetQueryParameter(
|
||||
"overpassUrl",
|
||||
(layoutToUse?.overpassUrl ?? Constants.defaultOverpassUrls).join(","),
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export default class UserRelatedState {
|
|||
public readonly fixateNorth: UIEventSource<undefined | "yes">
|
||||
public readonly a11y: UIEventSource<undefined | "always" | "never" | "default">
|
||||
public readonly homeLocation: FeatureSource
|
||||
public readonly morePrivacy: UIEventSource<undefined | "yes" | "no">
|
||||
/**
|
||||
* The language as saved into the preferences of the user, if logged in.
|
||||
* Note that this is _different_ from the languages a user can set via the osm.org interface here: https://www.openstreetmap.org/preferences
|
||||
|
|
@ -106,12 +107,12 @@ export default class UserRelatedState {
|
|||
})
|
||||
)
|
||||
this.language = this.osmConnection.GetPreference("language")
|
||||
this.showTags = <UIEventSource<any>>this.osmConnection.GetPreference("show_tags")
|
||||
this.showCrosshair = <UIEventSource<any>>this.osmConnection.GetPreference("show_crosshair")
|
||||
this.fixateNorth = <UIEventSource<"yes">>this.osmConnection.GetPreference("fixate-north")
|
||||
this.a11y = <UIEventSource<"always" | "never" | "default">>(
|
||||
this.osmConnection.GetPreference("a11y")
|
||||
)
|
||||
this.showTags = this.osmConnection.GetPreference("show_tags")
|
||||
this.showCrosshair = this.osmConnection.GetPreference("show_crosshair")
|
||||
this.fixateNorth = this.osmConnection.GetPreference("fixate-north")
|
||||
this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no")
|
||||
|
||||
this.a11y = this.osmConnection.GetPreference("a11y")
|
||||
|
||||
this.mangroveIdentity = new MangroveIdentity(
|
||||
this.osmConnection.GetLongPreference("identity", "mangrove"),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Tag } from "./Tag"
|
|||
import { RegexTag } from "./RegexTag"
|
||||
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
|
||||
import { ExpressionSpecification } from "maplibre-gl"
|
||||
import ComparingTag from "./ComparingTag"
|
||||
|
||||
export class And extends TagsFilter {
|
||||
public and: TagsFilter[]
|
||||
|
|
@ -242,6 +243,27 @@ export class And extends TagsFilter {
|
|||
* const raw = {"and": [{"and":["advertising=screen"]}, {"and":["advertising~*"]}]}]
|
||||
* const parsed = TagUtils.Tag(raw)
|
||||
* parsed.optimize().asJson() // => "advertising=screen"
|
||||
*
|
||||
* const raw = {"and": ["count=0", "count>0"]}
|
||||
* const parsed = TagUtils.Tag(raw)
|
||||
* parsed.optimize() // => false
|
||||
*
|
||||
* const raw = {"and": ["count>0", "count>10"]}
|
||||
* const parsed = TagUtils.Tag(raw)
|
||||
* parsed.optimize().asJson() // => "count>0"
|
||||
*
|
||||
* // regression test
|
||||
* const orig = {
|
||||
* "and": [
|
||||
* "sport=climbing",
|
||||
* "climbing!~route",
|
||||
* "climbing!=route_top",
|
||||
* "climbing!=route_bottom",
|
||||
* "leisure!~sports_centre"
|
||||
* ]
|
||||
* }
|
||||
* const parsed = TagUtils.Tag(orig)
|
||||
* parsed.optimize().asJson() // => orig
|
||||
*/
|
||||
optimize(): TagsFilter | boolean {
|
||||
if (this.and.length === 0) {
|
||||
|
|
@ -256,9 +278,30 @@ export class And extends TagsFilter {
|
|||
}
|
||||
const optimized = <TagsFilter[]>optimizedRaw
|
||||
|
||||
for (let i = 0; i <optimized.length; i++) {
|
||||
for (let j = i + 1; j < optimized.length; j++) {
|
||||
const ti = optimized[i]
|
||||
const tj = optimized[j]
|
||||
if(ti.shadows(tj)){
|
||||
// if 'ti' is true, this implies 'tj' is always true as well.
|
||||
// if 'ti' is false, then 'tj' might be true or false
|
||||
// (e.g. let 'ti' be 'count>0' and 'tj' be 'count>10'.
|
||||
// As such, it is no use to keep 'tj' around:
|
||||
// If 'ti' is true, then 'tj' will be true too and 'tj' can be ignored
|
||||
// If 'ti' is false, then the entire expression will be false and it doesn't matter what 'tj' yields
|
||||
optimized.splice(j, 1)
|
||||
}else if (tj.shadows(ti)){
|
||||
optimized.splice(i, 1)
|
||||
i--
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
// Conflicting keys do return false
|
||||
const properties: object = {}
|
||||
const properties: Record<string, string> = {}
|
||||
for (const opt of optimized) {
|
||||
if (opt instanceof Tag) {
|
||||
properties[opt.key] = opt.value
|
||||
|
|
@ -277,8 +320,7 @@ export class And extends TagsFilter {
|
|||
// detected an internal conflict
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (opt instanceof RegexTag) {
|
||||
} else if (opt instanceof RegexTag) {
|
||||
const k = opt.key
|
||||
if (typeof k !== "string") {
|
||||
continue
|
||||
|
|
@ -316,6 +358,11 @@ export class And extends TagsFilter {
|
|||
i--
|
||||
}
|
||||
}
|
||||
}else if(opt instanceof ComparingTag) {
|
||||
const ct = opt
|
||||
if(properties[ct.key] !== undefined && !ct.matchesProperties(properties)){
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { TagsFilter } from "./TagsFilter"
|
||||
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
|
||||
import { Tag } from "./Tag"
|
||||
import { ExpressionSpecification } from "maplibre-gl"
|
||||
|
||||
export default class ComparingTag extends TagsFilter {
|
||||
private readonly _key: string
|
||||
public readonly key: string
|
||||
private readonly _predicate: (value: string) => boolean
|
||||
private readonly _representation: "<" | ">" | "<=" | ">="
|
||||
private readonly _boundary: string
|
||||
|
|
@ -16,7 +15,7 @@ export default class ComparingTag extends TagsFilter {
|
|||
boundary: string
|
||||
) {
|
||||
super()
|
||||
this._key = key
|
||||
this.key = key
|
||||
this._predicate = predicate
|
||||
this._representation = representation
|
||||
this._boundary = boundary
|
||||
|
|
@ -27,7 +26,7 @@ export default class ComparingTag extends TagsFilter {
|
|||
}
|
||||
|
||||
asHumanString() {
|
||||
return this._key + this._representation + this._boundary
|
||||
return this.asJson()
|
||||
}
|
||||
|
||||
asOverpass(): string[] {
|
||||
|
|
@ -55,7 +54,7 @@ export default class ComparingTag extends TagsFilter {
|
|||
return true
|
||||
}
|
||||
if (other instanceof ComparingTag) {
|
||||
if (other._key !== this._key) {
|
||||
if (other.key !== this.key) {
|
||||
return false
|
||||
}
|
||||
const selfDesc = this._representation === "<" || this._representation === "<="
|
||||
|
|
@ -76,7 +75,7 @@ export default class ComparingTag extends TagsFilter {
|
|||
}
|
||||
|
||||
if (other instanceof Tag) {
|
||||
if (other.key !== this._key) {
|
||||
if (other.key !== this.key) {
|
||||
return false
|
||||
}
|
||||
if (this.matchesProperties({ [other.key]: other.value })) {
|
||||
|
|
@ -101,19 +100,25 @@ export default class ComparingTag extends TagsFilter {
|
|||
* t.matchesProperties({differentKey: 42}) // => false
|
||||
*/
|
||||
matchesProperties(properties: Record<string, string>): boolean {
|
||||
return this._predicate(properties[this._key])
|
||||
return this._predicate(properties[this.key])
|
||||
}
|
||||
|
||||
usedKeys(): string[] {
|
||||
return [this._key]
|
||||
return [this.key]
|
||||
}
|
||||
|
||||
usedTags(): { key: string; value: string }[] {
|
||||
return []
|
||||
}
|
||||
|
||||
asJson(): TagConfigJson {
|
||||
return this._key + this._representation
|
||||
/**
|
||||
* import { TagUtils } from "../../../src/Logic/Tags/TagUtils"
|
||||
*
|
||||
* TagUtils.Tag("count>42").asJson() // => "count>42"
|
||||
* TagUtils.Tag("count<0").asJson() // => "count<0"
|
||||
*/
|
||||
asJson(): string {
|
||||
return this.key + this._representation + this._boundary
|
||||
}
|
||||
|
||||
optimize(): TagsFilter | boolean {
|
||||
|
|
@ -124,11 +129,11 @@ export default class ComparingTag extends TagsFilter {
|
|||
return true
|
||||
}
|
||||
|
||||
visit(f: (TagsFilter) => void) {
|
||||
visit(f: (tf: TagsFilter) => void) {
|
||||
f(this)
|
||||
}
|
||||
|
||||
asMapboxExpression(): ExpressionSpecification {
|
||||
return [this._representation, ["get", this._key], this._boundary]
|
||||
return [this._representation, ["get", this.key], this._boundary]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -259,6 +259,13 @@ export class RegexTag extends TagsFilter {
|
|||
* new RegexTag("key",/^..*$/, true).shadows(new Tag("key","")) // => true
|
||||
* new RegexTag("key","value", true).shadows(new Tag("key","value")) // => false
|
||||
* new RegexTag("key","value", true).shadows(new Tag("key","some_other_value")) // => false
|
||||
* new RegexTag("key","value", true).shadows(new Tag("key","some_other_value", true)) // => false
|
||||
*
|
||||
* const route = TagUtils.Tag("climbing!~route")
|
||||
* const routeBottom = TagUtils.Tag("climbing!~route_bottom")
|
||||
* route.shadows(routeBottom) // => false
|
||||
* routeBottom.shadows(route) // => false
|
||||
*
|
||||
*/
|
||||
shadows(other: TagsFilter): boolean {
|
||||
if (other instanceof RegexTag) {
|
||||
|
|
@ -267,7 +274,7 @@ export class RegexTag extends TagsFilter {
|
|||
return false
|
||||
}
|
||||
if (
|
||||
(other.value["source"] ?? other.key) === (this.value["source"] ?? this.key) &&
|
||||
(other.value["source"] ?? other.value) === (this.value["source"] ?? this.value) &&
|
||||
this.invert == other.invert
|
||||
) {
|
||||
// Values (and inverts) match
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Utils } from "../../Utils"
|
|||
import { TagsFilter } from "./TagsFilter"
|
||||
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
|
||||
import { ExpressionSpecification } from "maplibre-gl"
|
||||
import { RegexTag } from "./RegexTag"
|
||||
|
||||
export class Tag extends TagsFilter {
|
||||
public key: string
|
||||
|
|
@ -122,6 +123,7 @@ export class Tag extends TagsFilter {
|
|||
/**
|
||||
*
|
||||
* import {RegexTag} from "./RegexTag";
|
||||
* import {And} from "./And";
|
||||
*
|
||||
* // should handle advanced regexes
|
||||
* new Tag("key", "aaa").shadows(new RegexTag("key", /a+/)) // => true
|
||||
|
|
@ -131,14 +133,20 @@ export class Tag extends TagsFilter {
|
|||
* new Tag("key","value").shadows(new RegexTag("key", "value", true)) // => false
|
||||
* new Tag("key","value").shadows(new RegexTag("otherkey", "value", true)) // => false
|
||||
* new Tag("key","value").shadows(new RegexTag("otherkey", "value", false)) // => false
|
||||
* new Tag("key","value").shadows(new And([new Tag("x","y"), new RegexTag("a","b", true)]) // => false
|
||||
*/
|
||||
shadows(other: TagsFilter): boolean {
|
||||
if (other["key"] !== undefined) {
|
||||
if (other["key"] !== this.key) {
|
||||
return false
|
||||
}
|
||||
if ((other["key"] !== this.key)) {
|
||||
return false
|
||||
}
|
||||
return other.matchesProperties({ [this.key]: this.value })
|
||||
if(other instanceof Tag){
|
||||
// Other.key === this.key
|
||||
return other.value === this.value
|
||||
}
|
||||
if(other instanceof RegexTag){
|
||||
return other.matchesProperties({[this.key]: this.value})
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
usedKeys(): string[] {
|
||||
|
|
|
|||
|
|
@ -27,23 +27,23 @@ export default class LinkedDataLoader {
|
|||
opening_hours: { "@id": "http://schema.org/openingHoursSpecification" },
|
||||
openingHours: { "@id": "http://schema.org/openingHours", "@container": "@set" },
|
||||
geo: { "@id": "http://schema.org/geo" },
|
||||
alt_name: { "@id": "http://schema.org/alternateName" },
|
||||
alt_name: { "@id": "http://schema.org/alternateName" }
|
||||
}
|
||||
private static COMPACTING_CONTEXT_OH = {
|
||||
dayOfWeek: { "@id": "http://schema.org/dayOfWeek", "@container": "@set" },
|
||||
closes: {
|
||||
"@id": "http://schema.org/closes",
|
||||
"@type": "http://www.w3.org/2001/XMLSchema#time",
|
||||
"@type": "http://www.w3.org/2001/XMLSchema#time"
|
||||
},
|
||||
opens: {
|
||||
"@id": "http://schema.org/opens",
|
||||
"@type": "http://www.w3.org/2001/XMLSchema#time",
|
||||
},
|
||||
"@type": "http://www.w3.org/2001/XMLSchema#time"
|
||||
}
|
||||
}
|
||||
private static formatters: Record<"phone" | "email" | "website", Validator> = {
|
||||
phone: new PhoneValidator(),
|
||||
email: new EmailValidator(),
|
||||
website: new UrlValidator(undefined, undefined, true),
|
||||
website: new UrlValidator(undefined, undefined, true)
|
||||
}
|
||||
private static ignoreKeys = [
|
||||
"http://schema.org/logo",
|
||||
|
|
@ -56,7 +56,7 @@ export default class LinkedDataLoader {
|
|||
"http://schema.org/description",
|
||||
"http://schema.org/hasMap",
|
||||
"http://schema.org/priceRange",
|
||||
"http://schema.org/contactPoint",
|
||||
"http://schema.org/contactPoint"
|
||||
]
|
||||
|
||||
private static shapeToPolygon(str: string): Polygon {
|
||||
|
|
@ -69,8 +69,8 @@ export default class LinkedDataLoader {
|
|||
.trim()
|
||||
.split(" ")
|
||||
.map((n) => Number(n))
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,18 +92,18 @@ export default class LinkedDataLoader {
|
|||
const context = {
|
||||
lat: {
|
||||
"@id": "http://schema.org/latitude",
|
||||
"@type": "http://www.w3.org/2001/XMLSchema#double",
|
||||
"@type": "http://www.w3.org/2001/XMLSchema#double"
|
||||
},
|
||||
lon: {
|
||||
"@id": "http://schema.org/longitude",
|
||||
"@type": "http://www.w3.org/2001/XMLSchema#double",
|
||||
},
|
||||
"@type": "http://www.w3.org/2001/XMLSchema#double"
|
||||
}
|
||||
}
|
||||
const flattened = await jsonld.compact(geo, context)
|
||||
|
||||
return {
|
||||
type: "Point",
|
||||
coordinates: [Number(flattened.lon), Number(flattened.lat)],
|
||||
coordinates: [Number(flattened.lon), Number(flattened.lat)]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -194,7 +194,7 @@ export default class LinkedDataLoader {
|
|||
)
|
||||
delete compacted["openingHours"]
|
||||
}
|
||||
if(compacted["opening_hours"] === undefined){
|
||||
if (compacted["opening_hours"] === undefined) {
|
||||
delete compacted["opening_hours"]
|
||||
}
|
||||
if (compacted["geo"]) {
|
||||
|
|
@ -288,7 +288,7 @@ export default class LinkedDataLoader {
|
|||
if (properties["latitude"] && properties["longitude"]) {
|
||||
geometry = {
|
||||
type: "Point",
|
||||
coordinates: [Number(properties["longitude"]), Number(properties["latitude"])],
|
||||
coordinates: [Number(properties["longitude"]), Number(properties["latitude"])]
|
||||
}
|
||||
delete properties["latitude"]
|
||||
delete properties["longitude"]
|
||||
|
|
@ -300,7 +300,7 @@ export default class LinkedDataLoader {
|
|||
const geo: GeoJSON = {
|
||||
type: "Feature",
|
||||
properties,
|
||||
geometry,
|
||||
geometry
|
||||
}
|
||||
delete linkedData.geo
|
||||
delete properties.shape
|
||||
|
|
@ -331,7 +331,7 @@ export default class LinkedDataLoader {
|
|||
return
|
||||
}
|
||||
output[key] = output[key].map((v) => applyF(v))
|
||||
if(!output[key].some(v => v !== undefined)){
|
||||
if (!output[key].some(v => v !== undefined)) {
|
||||
delete output[key]
|
||||
}
|
||||
}
|
||||
|
|
@ -416,7 +416,7 @@ export default class LinkedDataLoader {
|
|||
"brede publiek",
|
||||
"iedereen",
|
||||
"bezoekers",
|
||||
"iedereen - vooral bezoekers gemeentehuis of bibliotheek.",
|
||||
"iedereen - vooral bezoekers gemeentehuis of bibliotheek."
|
||||
].indexOf(audience.toLowerCase()) >= 0
|
||||
) {
|
||||
return "yes"
|
||||
|
|
@ -483,7 +483,6 @@ export default class LinkedDataLoader {
|
|||
}
|
||||
rename("capacityElectric", "capacity:electric_bicycle")
|
||||
|
||||
delete output["name"]
|
||||
delete output["numberOfLevels"]
|
||||
|
||||
return output
|
||||
|
|
@ -500,13 +499,14 @@ export default class LinkedDataLoader {
|
|||
mv: "http://schema.mobivoc.org/",
|
||||
gr: "http://purl.org/goodrelations/v1#",
|
||||
vp: "https://data.velopark.be/openvelopark/vocabulary#",
|
||||
vpt: "https://data.velopark.be/openvelopark/terms#",
|
||||
vpt: "https://data.velopark.be/openvelopark/terms#"
|
||||
},
|
||||
[url],
|
||||
undefined,
|
||||
" ?parking a <http://schema.mobivoc.org/BicycleParkingStation>",
|
||||
"?parking " + property + " " + (variable ?? "")
|
||||
)
|
||||
console.log("Fetching a velopark property gave", property, results)
|
||||
return results
|
||||
}
|
||||
|
||||
|
|
@ -521,7 +521,7 @@ export default class LinkedDataLoader {
|
|||
mv: "http://schema.mobivoc.org/",
|
||||
gr: "http://purl.org/goodrelations/v1#",
|
||||
vp: "https://data.velopark.be/openvelopark/vocabulary#",
|
||||
vpt: "https://data.velopark.be/openvelopark/terms#",
|
||||
vpt: "https://data.velopark.be/openvelopark/terms#"
|
||||
},
|
||||
[url],
|
||||
"g",
|
||||
|
|
@ -643,36 +643,41 @@ export default class LinkedDataLoader {
|
|||
allPartialResults.push(r)
|
||||
}
|
||||
|
||||
const results = this.mergeResults(...allPartialResults)
|
||||
|
||||
return results
|
||||
return this.mergeResults(...allPartialResults)
|
||||
}
|
||||
|
||||
private static veloparkCache : Record<string, Feature[]> = {}
|
||||
private static veloparkCache: Record<string, Feature[]> = {}
|
||||
|
||||
/**
|
||||
* Fetches all data relevant to velopark.
|
||||
* The id will be saved as `ref:velopark`
|
||||
* @param url
|
||||
*/
|
||||
public static async fetchVeloparkEntry(url: string): Promise<Feature[]> {
|
||||
if(this.veloparkCache[url]){
|
||||
return this.veloparkCache[url]
|
||||
public static async fetchVeloparkEntry(url: string, includeExtras: boolean = false): Promise<Feature[]> {
|
||||
const cacheKey = includeExtras + url
|
||||
if (this.veloparkCache[cacheKey]) {
|
||||
return this.veloparkCache[cacheKey]
|
||||
}
|
||||
const withProxyUrl = Constants.linkedDataProxy.replace("{url}", encodeURIComponent(url))
|
||||
const optionalPaths: Record<string, string | Record<string, string>> = {
|
||||
"schema:interactionService": {
|
||||
"schema:url": "website",
|
||||
"schema:url": "website"
|
||||
},
|
||||
"schema:name": "name",
|
||||
"mv:operatedBy": {
|
||||
"gr:legalName": "operator",
|
||||
"gr:legalName": "operator"
|
||||
},
|
||||
"schema:contactPoint": {
|
||||
"schema:email": "email",
|
||||
"schema:telephone": "phone",
|
||||
"schema:telephone": "phone"
|
||||
},
|
||||
"schema:dateModified": "_last_edit_timestamp",
|
||||
"schema:dateModified": "_last_edit_timestamp"
|
||||
}
|
||||
if (includeExtras) {
|
||||
optionalPaths["schema:address"] = {
|
||||
"schema:streetAddress": "addr"
|
||||
}
|
||||
optionalPaths["schema:name"] = "name"
|
||||
optionalPaths["schema:description"] = "description"
|
||||
}
|
||||
|
||||
const graphOptionalPaths = {
|
||||
|
|
@ -687,19 +692,19 @@ export default class LinkedDataLoader {
|
|||
"schema:geo": {
|
||||
"schema:latitude": "latitude",
|
||||
"schema:longitude": "longitude",
|
||||
"schema:polygon": "shape",
|
||||
"schema:polygon": "shape"
|
||||
},
|
||||
"schema:priceSpecification": {
|
||||
"mv:freeOfCharge": "fee",
|
||||
"schema:price": "charge",
|
||||
},
|
||||
"schema:price": "charge"
|
||||
}
|
||||
}
|
||||
|
||||
const extra = [
|
||||
"schema:priceSpecification [ mv:dueForTime [ mv:timeStartValue ?chargeStart; mv:timeEndValue ?chargeEnd; mv:timeUnit ?timeUnit ] ]",
|
||||
"vp:allows [vp:bicycleType <https://data.velopark.be/openvelopark/terms#CargoBicycle>; vp:bicyclesAmount ?capacityCargobike; vp:bicycleType ?cargoBikeType]",
|
||||
"vp:allows [vp:bicycleType <https://data.velopark.be/openvelopark/terms#ElectricBicycle>; vp:bicyclesAmount ?capacityElectric; vp:bicycleType ?electricBikeType]",
|
||||
"vp:allows [vp:bicycleType <https://data.velopark.be/openvelopark/terms#TandemBicycle>; vp:bicyclesAmount ?capacityTandem; vp:bicycleType ?tandemBikeType]",
|
||||
"vp:allows [vp:bicycleType <https://data.velopark.be/openvelopark/terms#TandemBicycle>; vp:bicyclesAmount ?capacityTandem; vp:bicycleType ?tandemBikeType]"
|
||||
]
|
||||
|
||||
const unpatched = await this.fetchEntry(
|
||||
|
|
@ -714,7 +719,7 @@ export default class LinkedDataLoader {
|
|||
p["ref:velopark"] = [section]
|
||||
patched.push(LinkedDataLoader.asGeojson(p))
|
||||
}
|
||||
this.veloparkCache[url] = patched
|
||||
this.veloparkCache[cacheKey] = patched
|
||||
return patched
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import type { Feature, FeatureCollection, MultiPolygon } from "geojson"
|
|||
import * as turf from "@turf/turf"
|
||||
import { Utils } from "../../Utils"
|
||||
import TagInfo from "./TagInfo"
|
||||
import type { Feature, MultiPolygon } from "geojson"
|
||||
import * as turf from "@turf/turf"
|
||||
|
||||
/**
|
||||
* Main name suggestion index file
|
||||
|
|
@ -140,6 +142,7 @@ export default class NameSuggestionIndex {
|
|||
return true
|
||||
}
|
||||
const resolvedSet = NameSuggestionIndex.loco.resolveLocationSet(i.locationSet)
|
||||
|
||||
if (resolvedSet) {
|
||||
// We actually have a location set, so we can check if the feature is in it, by determining if our point is inside the MultiPolygon using @turf/boolean-point-in-polygon
|
||||
// This might occur for some extra boundaries, such as counties, ...
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue