Merge branches

This commit is contained in:
Pieter Vander Vennet 2024-05-13 18:55:54 +02:00
commit bae90d50bc
304 changed files with 49983 additions and 31589 deletions

View file

@ -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(""),

View file

@ -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"

View file

@ -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

View file

@ -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
}))
}

View file

@ -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
}

View file

@ -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(","),

View file

@ -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"),

View file

@ -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
}
}
}
}

View file

@ -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]
}
}

View file

@ -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

View file

@ -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[] {

View file

@ -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
}
}

View file

@ -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, ...