forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
125139a672
313 changed files with 2392 additions and 19940 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue