Merge develop

This commit is contained in:
Pieter Vander Vennet 2024-08-26 01:30:48 +02:00
commit 125139a672
313 changed files with 2392 additions and 19940 deletions

View file

@ -1,5 +1,6 @@
import { Store, UIEventSource } from "../UIEventSource"
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
import { eliCategory } from "../../Models/RasterLayerProperties"
/**
* Selects the appropriate raster layer as background for the given query parameter, theme setting, user preference or default value.
@ -64,7 +65,7 @@ export class PreferredRasterLayerSelector {
private async updateLayer() {
// What is the ID of the layer we have to (try to) load?
const targetLayerId = (
this._queryParameter.data ?? this._preferredBackgroundLayer.data
(this._queryParameter.data ?? this._preferredBackgroundLayer.data)?.toLowerCase()
)?.toLowerCase()
if (targetLayerId === undefined || targetLayerId === "default") {
return
@ -77,8 +78,7 @@ export class PreferredRasterLayerSelector {
return
}
await AvailableRasterLayers.editorLayerIndex()
const isCategory =
targetLayerId === "photo" || targetLayerId === "osmbasedmap" || targetLayerId === "map"
const isCategory = (eliCategory).indexOf(<any> targetLayerId) >= 0
const available = this._availableLayers.store.data
const foundLayer = isCategory
? available.find((l) => l.properties.category === targetLayerId)

View file

@ -37,11 +37,11 @@ export class Changes {
public readonly isUploading = new UIEventSource(false)
public readonly errors = new UIEventSource<string[]>([], "upload-errors")
private readonly historicalUserLocations?: FeatureSource
private _nextId: number = -1 // Newly assigned ID's are negative
private _nextId: number = 0 // Newly assigned ID's are negative
private readonly previouslyCreated: OsmObject[] = []
private readonly _leftRightSensitive: boolean
public readonly _changesetHandler: ChangesetHandler
private readonly _reportError?: (string: string | Error) => void
private readonly _reportError?: (string: string | Error, extramessage?: string) => void
constructor(
state: {
@ -53,7 +53,7 @@ export class Changes {
featureSwitches?: FeatureSwitchState
},
leftRightSensitive: boolean = false,
reportError?: (string: string | Error) => void
reportError?: (string: string | Error, extramessage?: string) => void,
) {
this._leftRightSensitive = leftRightSensitive
// We keep track of all changes just as well
@ -68,7 +68,7 @@ export class Changes {
state.osmConnection,
state.featurePropertiesStore,
this,
(e) => this._reportError(e)
(e, extramessage: string) => this._reportError(e, extramessage),
)
this.historicalUserLocations = state.historicalUserLocations
@ -76,13 +76,13 @@ export class Changes {
// This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset
}
static createChangesetFor(
static buildChangesetXML(
csId: string,
allChanges: {
modifiedObjects: OsmObject[]
newObjects: OsmObject[]
deletedObjects: OsmObject[]
}
},
): string {
const changedElements = allChanges.modifiedObjects ?? []
const newElements = allChanges.newObjects ?? []
@ -172,7 +172,7 @@ export class Changes {
docs: "The identifier of the used background layer, this will probably be an identifier from the [editor layer index](https://github.com/osmlab/editor-layer-index)",
},
],
"default"
"default",
),
...addSource(ChangeTagAction.metatags, "ChangeTag"),
...addSource(ChangeLocationAction.metatags, "ChangeLocation"),
@ -201,7 +201,7 @@ export class Changes {
: "",
].join("\n"),
source,
])
]),
),
].join("\n\n")
}
@ -214,7 +214,11 @@ export class Changes {
* Returns a new ID and updates the value for the next ID
*/
public getNewID() {
return this._nextId--
// See #2082. We check for previous rewritings, as a remapping might be from a previous session
do {
this._nextId--
} while (this._changesetHandler._remappings.has("node/" + this._nextId) || this._changesetHandler._remappings.has("way/" + this._nextId) || this._changesetHandler._remappings.has("relation/" + this._nextId))
return this._nextId
}
/**
@ -233,9 +237,9 @@ export class Changes {
console.log("Uploading changes due to: ", flushreason)
this.isUploading.setData(true)
try {
const csNumber = await this.flushChangesAsync()
await this.flushChangesAsync()
this.isUploading.setData(false)
console.log("Changes flushed. Your changeset is " + csNumber)
console.log("Changes flushed")
this.errors.setData([])
} catch (e) {
this._reportError(e)
@ -250,7 +254,7 @@ export class Changes {
const changeDescriptions = await action.Perform(this)
const remapped = ChangeDescriptionTools.rewriteAllIds(
changeDescriptions,
this._changesetHandler._remappings
this._changesetHandler._remappings,
)
remapped[0].meta.distanceToObject = this.calculateDistanceToChanges(action, remapped)
@ -458,7 +462,7 @@ export class Changes {
result.modifiedObjects.length,
"modified;",
result.deletedObjects.length,
"deleted"
"deleted",
)
}
return result
@ -466,7 +470,7 @@ export class Changes {
private calculateDistanceToChanges(
change: OsmChangeAction,
changeDescriptions: ChangeDescription[]
changeDescriptions: ChangeDescription[],
) {
const locations = this.historicalUserLocations?.features?.data
if (locations === undefined) {
@ -486,7 +490,7 @@ export class Changes {
.filter((feat) => feat.geometry.type === "Point")
.filter((feat) => {
const visitTime = new Date(
(<GeoLocationPointProperties>(<any>feat.properties)).date
(<GeoLocationPointProperties>(<any>feat.properties)).date,
)
// In seconds
const diff = (now.getTime() - visitTime.getTime()) / 1000
@ -533,9 +537,9 @@ export class Changes {
...recentLocationPoints.map((gpsPoint) => {
const otherCoor = GeoOperations.centerpointCoordinates(gpsPoint)
return GeoOperations.distanceBetween(coor, otherCoor)
})
)
)
}),
),
),
)
}
@ -571,7 +575,7 @@ export class Changes {
public fragmentChanges(
pending: ChangeDescription[],
objects: OsmObject[]
objects: OsmObject[],
): {
refused: ChangeDescription[]
toUpload: ChangeDescription[]
@ -581,7 +585,7 @@ export class Changes {
// All ids which have an 'update'
const createdIds = new Set(
pending.filter((cd) => cd.changes !== undefined).map((cd) => cd.id)
pending.filter((cd) => cd.changes !== undefined).map((cd) => cd.id),
)
pending.forEach((c) => {
if (c.id < 0) {
@ -590,7 +594,7 @@ export class Changes {
} else {
this._reportError(
`Got an orphaned change. The 'creation'-change description for ${c.type}/${c.id} got lost. Permanently dropping this change:` +
JSON.stringify(c)
JSON.stringify(c),
)
}
return
@ -601,10 +605,10 @@ export class Changes {
} else {
console.log(
"Refusing change about " +
c.type +
"/" +
c.id +
" as not in the objects. No internet?"
c.type +
"/" +
c.id +
" as not in the objects. No internet?",
)
refused.push(c)
}
@ -619,17 +623,18 @@ export class Changes {
*/
private async flushSelectChanges(
pending: ChangeDescription[],
openChangeset: UIEventSource<number>
openChangeset: UIEventSource<number>,
): Promise<ChangeDescription[]> {
const neededIds = Changes.GetNeededIds(pending)
// We _do not_ pass in the Changes object itself - we want the data from OSM directly in order to apply the changes
/* Download the latest version of the OSM-objects
* We _do not_ pass in the Changes object itself - we want the data from OSM directly in order to apply the changes
*/
const downloader = new OsmObjectDownloader(this.backend, undefined)
let osmObjects = await Promise.all<{ id: string; osmObj: OsmObject | "deleted" }>(
neededIds.map((id) => this.getOsmObject(id, downloader))
)
osmObjects = Utils.NoNull(osmObjects)
const osmObjects = Utils.NoNull(await Promise.all<{ id: string; osmObj: OsmObject | "deleted" }>(
neededIds.map((id) => this.getOsmObject(id, downloader)),
))
// Drop changes to deleted items
for (const { osmObj, id } of osmObjects) {
if (osmObj === "deleted") {
pending = pending.filter((ch) => ch.type + "/" + ch.id !== id)
@ -649,20 +654,56 @@ export class Changes {
return undefined
}
const metatags = this.buildChangesetTags(pending)
let { toUpload, refused } = this.fragmentChanges(pending, objects)
if (toUpload.length === 0) {
return refused
}
await this._changesetHandler.UploadChangeset(
(csId, remappings) => {
if (remappings.size > 0) {
toUpload = toUpload.map((ch) =>
ChangeDescriptionTools.rewriteIds(ch, remappings),
)
}
const changes: {
newObjects: OsmObject[]
modifiedObjects: OsmObject[]
deletedObjects: OsmObject[]
} = this.CreateChangesetObjects(toUpload, objects)
return Changes.buildChangesetXML("" + csId, changes)
},
metatags,
openChangeset,
)
console.log("Upload successful! Refused changes are", refused)
return refused
}
/**
* Builds all the changeset tags, such as `theme=cyclofix; answer=42; add-image: 5`, ...
*/
private buildChangesetTags(pending: ChangeDescription[]) {
// Build statistics for the changeset tags
const perType = Array.from(
Utils.Hist(
pending
.filter(
(descr) =>
descr.meta.changeType !== undefined && descr.meta.changeType !== null
descr.meta.changeType !== undefined && descr.meta.changeType !== null,
)
.map((descr) => descr.meta.changeType)
.map((descr) => descr.meta.changeType),
),
([key, count]) => ({
key: key,
value: count,
aggregate: true,
})
}),
)
const motivations = pending
.filter((descr) => descr.meta.specialMotivation !== undefined)
@ -701,7 +742,7 @@ export class Changes {
value: count,
aggregate: true,
}
})
}),
)
// This method is only called with changedescriptions for this theme
@ -724,34 +765,7 @@ export class Changes {
...motivations,
...perBinMessage,
]
let { toUpload, refused } = this.fragmentChanges(pending, objects)
if (toUpload.length === 0) {
return refused
}
await this._changesetHandler.UploadChangeset(
(csId, remappings) => {
if (remappings.size > 0) {
toUpload = toUpload.map((ch) =>
ChangeDescriptionTools.rewriteIds(ch, remappings)
)
}
const changes: {
newObjects: OsmObject[]
modifiedObjects: OsmObject[]
deletedObjects: OsmObject[]
} = this.CreateChangesetObjects(toUpload, objects)
return Changes.createChangesetFor("" + csId, changes)
},
metatags,
openChangeset
)
console.log("Upload successful! Refused changes are", refused)
return refused
return metatags
}
private async flushChangesAsync(): Promise<void> {
@ -774,14 +788,14 @@ export class Changes {
try {
const openChangeset = UIEventSource.asInt(
this.state.osmConnection.GetPreference(
"current-open-changeset-" + theme
)
"current-open-changeset-" + theme,
),
)
console.log(
"Using current-open-changeset-" +
theme +
" from the preferences, got " +
openChangeset.data
theme +
" from the preferences, got " +
openChangeset.data,
)
const refused = await self.flushSelectChanges(pendingChanges, openChangeset)
@ -796,7 +810,7 @@ export class Changes {
this.errors.ping()
return pendingChanges
}
})
}),
)
// We keep all the refused changes to try them again
@ -804,7 +818,7 @@ export class Changes {
} catch (e) {
console.error(
"Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those",
e
e,
)
this.errors.data.push(e)
this.errors.ping()

View file

@ -13,6 +13,19 @@ export interface ChangesetTag {
aggregate?: boolean
}
export type ChangesetMetadata = {
id: number
created_at: string
open: boolean
closed_at?: string
uid: number
user: string
changes_count: number
tags: Record<string, string>,
minlat: number, minlon: number, maxlat: number, maxlon: number
comments_count: number
}
export class ChangesetHandler {
private readonly allElements: FeaturePropertiesStore
private osmConnection: OsmConnection
@ -26,7 +39,7 @@ export class ChangesetHandler {
* @private
*/
public readonly _remappings = new Map<string, string>()
private readonly _reportError: (e: string | Error) => void
private readonly _reportError: (e: string | Error, extramsg: string) => void
constructor(
dryRun: Store<boolean>,
@ -36,7 +49,7 @@ export class ChangesetHandler {
| { addAlias: (id0: string, id1: string) => void }
| undefined,
changes: Changes,
reportError: (e: string | Error) => void
reportError: (e: string | Error, extramessage: string) => void,
) {
this.osmConnection = osmConnection
this._reportError = reportError
@ -94,6 +107,27 @@ export class ChangesetHandler {
return hasChange
}
private async UploadWithNew(generateChangeXML: (csid: number, remappings: Map<string, string>) => string, openChangeset: UIEventSource<number>, extraMetaTags: ChangesetTag[]) {
const csId = await this.OpenChangeset(extraMetaTags)
openChangeset.setData(csId)
const changeset = generateChangeXML(csId, this._remappings)
console.log(
"Opened a new changeset (openChangeset.data is undefined):",
changeset,
extraMetaTags,
)
const changes = await this.UploadChange(csId, changeset)
const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(
extraMetaTags,
changes,
)
if (hasSpecialMotivationChanges) {
// At this point, 'extraMetaTags' will have changed - we need to set the tags again
await this.UpdateTags(csId, extraMetaTags)
}
}
/**
* The full logic to upload a change to one or more elements.
*
@ -107,7 +141,7 @@ export class ChangesetHandler {
public async UploadChangeset(
generateChangeXML: (csid: number, remappings: Map<string, string>) => string,
extraMetaTags: ChangesetTag[],
openChangeset: UIEventSource<number>
openChangeset: UIEventSource<number>,
): Promise<void> {
if (
!extraMetaTags.some((tag) => tag.key === "comment") ||
@ -130,83 +164,60 @@ export class ChangesetHandler {
return
}
if (openChangeset.data === undefined) {
// We have to open a new changeset
try {
const csId = await this.OpenChangeset(extraMetaTags)
openChangeset.setData(csId)
const changeset = generateChangeXML(csId, this._remappings)
console.log(
"Opened a new changeset (openChangeset.data is undefined):",
changeset,
extraMetaTags
)
const changes = await this.UploadChange(csId, changeset)
const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(
extraMetaTags,
changes
)
if (hasSpecialMotivationChanges) {
// At this point, 'extraMetaTags' will have changed - we need to set the tags again
await this.UpdateTags(csId, extraMetaTags)
}
} catch (e) {
if (this._reportError) {
this._reportError(e)
}
if ((<XMLHttpRequest>e).status === 400) {
// This request is invalid. We simply drop the changes and hope that someone will analyze what went wrong with it in the upload; we pretend everything went fine
return
}
console.warn(
"Could not open/upload changeset due to ",
e,
"trying again with a another fresh changeset "
)
openChangeset.setData(undefined)
throw e
}
} else {
// There still exists an open changeset (or at least we hope so)
// Let's check!
const csId = openChangeset.data
console.log("Trying to reuse changeset", openChangeset.data)
if (openChangeset.data) {
try {
const csId = openChangeset.data
const oldChangesetMeta = await this.GetChangesetMeta(csId)
if (!oldChangesetMeta.open) {
// Mark the CS as closed...
console.log("Could not fetch the metadata from the already open changeset")
openChangeset.setData(undefined)
// ... and try again. As the cs is closed, no recursive loop can exist
await this.UploadChangeset(generateChangeXML, extraMetaTags, openChangeset)
return
console.log("Got metadata:", oldChangesetMeta, "isopen", oldChangesetMeta?.open)
if (oldChangesetMeta.open) {
// We can hopefully reuse the changeset
try {
const rewritings = await this.UploadChange(
csId,
generateChangeXML(csId, this._remappings),
)
const rewrittenTags = this.RewriteTagsOf(
extraMetaTags,
rewritings,
oldChangesetMeta,
)
await this.UpdateTags(csId, rewrittenTags)
return // We are done!
} catch (e) {
this._reportError(e, "While reusing a changeset " + openChangeset.data)
}
}
const rewritings = await this.UploadChange(
csId,
generateChangeXML(csId, this._remappings)
)
const rewrittenTags = this.RewriteTagsOf(
extraMetaTags,
rewritings,
oldChangesetMeta
)
await this.UpdateTags(csId, rewrittenTags)
} catch (e) {
if (this._reportError) {
this._reportError(
"Could not reuse changeset " +
csId +
", might be closed: " +
(e.stacktrace ?? e.status ?? "" + e)
)
}
console.warn("Could not upload, changeset is probably closed: ", e)
openChangeset.setData(undefined)
throw e
this._reportError(e, "While getting metadata from a changeset " + openChangeset.data)
}
}
// We have to open a new changeset
try {
return await this.UploadWithNew(generateChangeXML, openChangeset, extraMetaTags)
} catch (e) {
if (this._reportError) {
this._reportError(e, "While opening a new changeset")
}
if ((<XMLHttpRequest>e).status === 400) {
// This request is invalid. We simply drop the changes and hope that someone will analyze what went wrong with it in the upload; we pretend everything went fine
return
}
console.warn(
"Could not open/upload changeset due to ",
e,
"trying again with a another fresh changeset ",
)
openChangeset.setData(undefined)
throw e
}
}
/**
@ -227,7 +238,7 @@ export class ChangesetHandler {
uid: number // User ID
changes_count: number
tags: any
}
},
): ChangesetTag[] {
// Note: extraMetaTags is where all the tags are collected into
@ -346,16 +357,10 @@ export class ChangesetHandler {
console.log("Closed changeset ", changesetId)
}
private async GetChangesetMeta(csId: number): Promise<{
id: number
open: boolean
uid: number
changes_count: number
tags: any
}> {
private async GetChangesetMeta(csId: number): Promise<ChangesetMetadata> {
const url = `${this.backend}/api/0.6/changeset/${csId}`
const csData = await Utils.downloadJson(url)
return csData.elements[0]
const csData = await Utils.downloadJson<{ changeset: ChangesetMetadata }>(url)
return csData.changeset
}
/**
@ -370,7 +375,7 @@ export class ChangesetHandler {
tag.key !== undefined &&
tag.value !== undefined &&
tag.key !== "" &&
tag.value !== ""
tag.value !== "",
)
const metadata = tags.map((kv) => `<tag k="${kv.key}" v="${escapeHtml(kv.value)}"/>`)
const content = [`<osm><changeset>`, metadata, `</changeset></osm>`].join("")
@ -410,7 +415,7 @@ export class ChangesetHandler {
const csId = await this.osmConnection.put(
"changeset/create",
[`<osm><changeset>`, metadata, `</changeset></osm>`].join(""),
{ "Content-Type": "text/xml" }
{ "Content-Type": "text/xml" },
)
return Number(csId)
}
@ -420,12 +425,12 @@ export class ChangesetHandler {
*/
private async UploadChange(
changesetId: number,
changesetXML: string
changesetXML: string,
): Promise<Map<string, string>> {
const response = await this.osmConnection.post<XMLDocument>(
"changeset/" + changesetId + "/upload",
changesetXML,
{ "Content-Type": "text/xml" }
{ "Content-Type": "text/xml" },
)
const changes = this.parseUploadChangesetResponse(response)
console.log("Uploaded changeset ", changesetId)

View file

@ -7,6 +7,9 @@ import { QueryParameters } from "../Web/QueryParameters"
import Constants from "../../Models/Constants"
import { Utils } from "../../Utils"
import { Query } from "pg"
import { eliCategory } from "../../Models/RasterLayerProperties"
import { AvailableRasterLayers } from "../../Models/RasterLayers"
import MarkdownUtils from "../../Utils/MarkdownUtils"
class FeatureSwitchUtils {
/** Helper function to initialize feature switches
@ -78,7 +81,7 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
const legacyRewrite: Record<string, string | string[]> = {
"fs-userbadge": "fs-enable-login",
"fs-layers": ["fs-filter", "fs-background"],
"fs-layers": ["fs-filter", "fs-background"]
}
for (const key in legacyRewrite) {
@ -248,7 +251,18 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
this.backgroundLayerId = QueryParameters.GetQueryParameter(
"background",
layoutToUse?.defaultBackgroundId,
"The id of the background layer to start with"
["When set, load this raster layer (or a layer of this category) as background layer instead of using the default background. This is as if the user opened the background selection menu and selected the layer with the given id or category.",
"Most raster layers are based on the [editor layer index](https://github.com/osmlab/editor-layer-index)",
"#### Selecting a category",
"If one of the following values is used, this parameter will be interpreted as a _category_ instead of the id of a specific layer. The best layer of this category will be used. Supported categories are those from the editor layer index and are:",
eliCategory.map(c => "- " + c).join("\n"),
"#### Selecting a specific layer",
"One can use the [ID of an ELI-layer](./ELI-overview.md) or use one of the global, builtin layers:",
MarkdownUtils.list(AvailableRasterLayers.globalLayers.map(global =>
global.properties.id+(global.properties.best ? " ⭐" : "")
))
].join("\n\n")
)
}
}

View file

@ -9,9 +9,9 @@ import ComparingTag from "./ComparingTag"
import { FlatTag, OptimizedTag, TagsFilterClosed, TagTypes } from "./TagTypes"
export class And extends TagsFilter {
public and: TagsFilter[]
public and: ReadonlyArray<TagsFilter>
constructor(and: TagsFilter[]) {
constructor(and: ReadonlyArray<TagsFilter>) {
super()
this.and = and
if (and.some((p) => typeof p === "string")) {
@ -20,16 +20,16 @@ export class And extends TagsFilter {
}
}
public static construct(and: TagsFilter[]): TagsFilter
public static construct(and: (FlatTag | (Or & OptimizedTag))[]): TagsFilterClosed & OptimizedTag
public static construct(and: TagsFilter[]): TagsFilter {
public static construct(and: ReadonlyArray<TagsFilter>): TagsFilter
public static construct(and: ReadonlyArray<(FlatTag | (Or & OptimizedTag))>): TagsFilterClosed & OptimizedTag
public static construct(and: ReadonlyArray< TagsFilter>): TagsFilter {
if (and.length === 1) {
return and[0]
}
return new And(and)
}
private static combine(filter: string, choices: string[]): string[] {
private static combine(filter: string, choices: ReadonlyArray< string>): string[] {
const values = []
for (const or of choices) {
values.push(filter + or)
@ -447,7 +447,7 @@ export class And extends TagsFilter {
if (containedOrs.length === 1) {
newAnds.push(containedOrs[0])
} else if (containedOrs.length > 1) {
let commonValues: TagsFilter[] = containedOrs[0].or
let commonValues: TagsFilter[] = [...(containedOrs[0].or)]
for (let i = 1; i < containedOrs.length && commonValues.length > 0; i++) {
const containedOr = containedOrs[i]
commonValues = commonValues.filter((cv) =>

View file

@ -10,17 +10,17 @@ import ComparingTag from "./ComparingTag"
import { FlatTag, OptimizedTag, TagsFilterClosed, TagTypes } from "./TagTypes"
export class Or extends TagsFilter {
public or: TagsFilter[]
public or: ReadonlyArray<TagsFilter>
constructor(or: TagsFilter[]) {
constructor(or: ReadonlyArray<TagsFilter>) {
super()
this.or = or
}
public static construct(or: TagsFilter[]): TagsFilter
public static construct(or: ReadonlyArray<TagsFilter>): TagsFilter
public static construct<T extends TagsFilter>(or: [T]): T
public static construct(or: ((And & OptimizedTag) | FlatTag)[]): TagsFilterClosed & OptimizedTag
public static construct(or: TagsFilter[]): TagsFilter {
public static construct(or: ReadonlyArray<TagsFilter>): TagsFilter {
if (or.length === 1) {
return or[0]
}
@ -264,7 +264,7 @@ export class Or extends TagsFilter {
if (containedAnds.length === 1) {
newOrs.push(containedAnds[0])
} else if (containedAnds.length > 1) {
let commonValues: TagsFilter[] = containedAnds[0].and
let commonValues: TagsFilter[] = [...(containedAnds[0].and)]
for (let i = 1; i < containedAnds.length && commonValues.length > 0; i++) {
const containedAnd = containedAnds[i]
commonValues = commonValues.filter((cv) =>

View file

@ -685,7 +685,7 @@ export class TagUtils {
* TagUtils.containsEquivalents([new Tag("key","value")], [ new Tag("other_key","value")]) // => false
* TagUtils.containsEquivalents([new Tag("key","value")], [ new Tag("key","other_value")]) // => false
*/
public static containsEquivalents(guards: TagsFilter[], listToFilter: TagsFilter[]): boolean {
public static containsEquivalents(guards: ReadonlyArray<TagsFilter>, listToFilter: ReadonlyArray<TagsFilter>): boolean {
return listToFilter.some((tf) => guards.some((guard) => guard.shadows(tf)))
}
@ -741,7 +741,7 @@ export class TagUtils {
}
if (typeof json != "string") {
if (json["and"] !== undefined && json["or"] !== undefined) {
throw `${context}: Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined. Did you override a value? Perhaps use \`"=parent": { ... }\` instead of \"parent": {...}\` to trigger a replacement and not a fuse of values. The value is ${JSON.stringify(
throw `${context}: Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined. Did you override a value? Perhaps use \`"=parent": { ... }\` instead of "parent": {...}\` to trigger a replacement and not a fuse of values. The value is ${JSON.stringify(
json
)}`
}
@ -935,7 +935,7 @@ export class TagUtils {
return 0
}
private static joinL(tfs: TagsFilter[], seperator: string, toplevel: boolean) {
private static joinL(tfs: ReadonlyArray<TagsFilter>, seperator: string, toplevel: boolean) {
const joined = tfs.map((e) => TagUtils.toString(e, false)).join(seperator)
if (toplevel) {
return joined

View file

@ -104,15 +104,11 @@ export default class ThemeViewStateHashActor {
if (found.properties.id === "last_click") {
return true
}
const layer = this._state.layout.getMatchingLayer(found.properties)
console.log(
"Setting selected element based on hash",
hash,
"; found",
found,
"got matching layer",
layer.id,
""
found
)
selectedElement.setData(found)
return true

View file

@ -141,12 +141,14 @@ export default class Constants {
"help",
"help",
"home",
"key",
"invalid",
"invalid",
"link",
"location",
"location_empty",
"location_locked",
"lock",
"mastodon",
"not_found",
"note",

View file

@ -1,12 +1,12 @@
export type EliCategory =
| "photo"
| "map"
| "historicmap"
| "osmbasedmap"
| "historicphoto"
| "qa"
| "elevation"
| "other"
export const eliCategory = ["photo"
, "map"
, "historicmap"
, "osmbasedmap"
, "historicphoto"
, "qa"
, "elevation"
, "other"] as const
export type EliCategory = (typeof eliCategory)[number]
/**
* This class has grown beyond the point of only containing Raster Layers

View file

@ -5,7 +5,7 @@ import * as bingJson from "../assets/bing.json"
import { BBox } from "../Logic/BBox"
import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"
import { GeoOperations } from "../Logic/GeoOperations"
import { RasterLayerProperties } from "./RasterLayerProperties"
import { EliCategory, RasterLayerProperties } from "./RasterLayerProperties"
import { Utils } from "../Utils"
export type EditorLayerIndex = (Feature<Polygon, EditorLayerIndexProperties> & RasterLayerPolygon)[]
@ -38,7 +38,7 @@ export class AvailableRasterLayers {
<RasterLayerPolygon>{
type: "Feature",
properties,
geometry: BBox.global.asGeometry(),
geometry: BBox.global.asGeometry()
}
)
public static bing = <RasterLayerPolygon>bingJson
@ -48,18 +48,18 @@ export class AvailableRasterLayers {
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution: {
text: "OpenStreetMap",
url: "https://openStreetMap.org/copyright",
url: "https://openStreetMap.org/copyright"
},
best: true,
max_zoom: 19,
min_zoom: 0,
category: "osmbasedmap",
category: "osmbasedmap"
}
public static readonly osmCarto: RasterLayerPolygon = {
type: "Feature",
properties: AvailableRasterLayers.osmCartoProperties,
geometry: BBox.global.asGeometry(),
geometry: BBox.global.asGeometry()
}
/**
@ -192,19 +192,12 @@ export interface EditorLayerIndexProperties extends RasterLayerProperties {
/**
* A rough categorisation of different types of layers. See https://github.com/osmlab/editor-layer-index/blob/gh-pages/CONTRIBUTING.md#categories for a description of the individual categories.
*/
readonly category?:
| "photo"
| "map"
| "historicmap"
| "osmbasedmap"
| "historicphoto"
| "qa"
| "elevation"
| "other"
readonly category?: EliCategory
/**
* A URL template for imagery tiles
*/
readonly url: string
readonly url: string
readonly min_zoom?: number
readonly max_zoom?: number
/**

View file

@ -278,7 +278,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
featureSwitches: this.featureSwitches,
},
layout?.isLeftRightSensitive() ?? false,
(e) => this.reportError(e)
(e, extraMsg) => this.reportError(e, extraMsg),
)
this.historicalUserLocations = this.geolocation.historicalUserLocations
this.newFeatures = new NewGeometryFromChangesFeatureSource(
@ -650,7 +650,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
available,
category,
current.data,
skipLayers
skipLayers,
)
if (!best) {
return
@ -907,7 +907,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.selectedElement.setData(this.currentView.features?.data?.[0])
}
public async reportError(message: string | Error | XMLHttpRequest) {
public async reportError(message: string | Error | XMLHttpRequest, extramessage: string = "") {
const isTesting = this.featureSwitchIsTesting.data
console.log(
isTesting
@ -922,7 +922,17 @@ export default class ThemeViewState implements SpecialVisualizationState {
if ("" + message === "[object XMLHttpRequest]") {
const req = <XMLHttpRequest>message
message = "XMLHttpRequest with status code " + req.status + ", " + req.statusText
let body = ""
try {
body = req.responseText
} catch (e) {
// pass
}
message = "XMLHttpRequest with status code " + req.status + ", " + req.statusText + ", received: " + body
}
if (extramessage) {
message += "(" + extramessage + ")"
}
const stacktrace: string = new Error().stack

View file

@ -90,9 +90,7 @@ export default class UrlValidator extends Validator {
*
* const v = new UrlValidator()
* v.getFeedback("example.").textFor("en") // => "This is not a valid web address"
* v.isValid("https://booking.com/some-hotel.html") // => false
* v.getFeedback("https://booking.com/some-hotel.html").textFor("en").indexOf("low-quality") > 0 // => true
*
* v.getFeedback("https://booking.com/some-hotel.html").textFor("en") // => Translations.t.validation.url.spamSite.Subs({host: "booking.com"}).textFor("en")
*/
getFeedback(s: string, getCountry?: () => string): Translation | undefined {
if (
@ -128,6 +126,10 @@ export default class UrlValidator extends Validator {
return undefined
}
/**
* const v = new UrlValidator()
* v.isValid("https://booking.com/some-hotel.html") // => false
*/
isValid(str: string): boolean {
try {

View file

@ -39,6 +39,8 @@
import Gear from "../../assets/svg/Gear.svelte"
import { DesktopComputerIcon, UserCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import Relocation from "../../assets/svg/Relocation.svelte"
import LockClosed from "@babeard/svelte-heroicons/solid/LockClosed"
import Key from "@babeard/svelte-heroicons/solid/Key"
/**
* Renders a single icon.
@ -146,6 +148,10 @@
<PencilIcon class={clss} {color} />
{:else if icon === "user_circle"}
<UserCircleIcon class={clss} {color} />
{:else if icon === "lock"}
<LockClosed class={clss} {color} />
{:else if icon === "key"}
<Key class={clss} {color} />
{:else if Utils.isEmoji(icon)}
<span style={`font-size: ${emojiHeight}; line-height: ${emojiHeight}`}>
{icon}