diff --git a/assets/layers/questions/questions.json b/assets/layers/questions/questions.json index 59bcd3a06..00dfea69f 100644 --- a/assets/layers/questions/questions.json +++ b/assets/layers/questions/questions.json @@ -137,7 +137,7 @@ { "id": "minimap", "labels": [ - "ignore_docs" + "ignore_docs" ], "description": "Shows a small map with the feature. Added by default to every popup", "render": { diff --git a/package.json b/package.json index 401565028..147adddd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapcomplete", - "version": "0.44.7", + "version": "0.44.8", "repository": "https://github.com/pietervdvn/MapComplete", "description": "A small website to edit OSM easily", "bugs": "https://github.com/pietervdvn/MapComplete/issues", @@ -19,20 +19,23 @@ "#": "This client-id is registered by 'MapComplete' on OpenStreetMap.org", "oauth_client_id": "K93H1d8ve7p-tVLE1ZwsQ4lAFLQk8INx5vfTLMu5DWk", "oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg", - "url": "https://www.openstreetmap.org" + "url": "https://www.openstreetmap.org", + "api_url": "https://api.openstreetmap.org" + }, + "disabled:oauth_credentials": { + "##": "DEV", + "#": "This client-id is registered by 'MapComplete' on https://master.apis.dev.openstreetmap.org/", + "oauth_client_id": "BAPPMRuojjFsY__0APmScr1hbebYj1GlDbV6S5TsUbo", + "oauth_secret": "Lq1UKYAySRfQfwgFctGvlGrfxbGHQwggheE52HIGrO8", + "url": "https://master.apis.dev.openstreetmap.org", + "api_url": "https://master.apis.dev.openstreetmap.org" }, "mvt_layer_server": "https://cache.mapcomplete.org/public.{type}_{layer}/{z}/{x}/{y}.pbf", "#summary_server": "Should be the endpoint; appending status.json should work", "summary_server": "https://cache.mapcomplete.org/", "geoip_server": "https://ipinfo.mapcomplete.org/", "error_server": "https://report.mapcomplete.org/report", - "disabled:oauth_credentials": { - "##": "DEV", - "#": "This client-id is registered by 'MapComplete' on https://master.apis.dev.openstreetmap.org/", - "oauth_client_id": "BAPPMRuojjFsY__0APmScr1hbebYj1GlDbV6S5TsUbo", - "oauth_secret": "Lq1UKYAySRfQfwgFctGvlGrfxbGHQwggheE52HIGrO8", - "url": "https://master.apis.dev.openstreetmap.org" - }, + "api_keys": { "#": "Various API-keys for various services. Feel free to reuse those in another MapComplete-hosted version", "imgur": "7070e7167f0a25a", diff --git a/scripts/handleErrors.ts b/scripts/handleErrors.ts index 0b9e4dea6..74fade996 100644 --- a/scripts/handleErrors.ts +++ b/scripts/handleErrors.ts @@ -21,6 +21,9 @@ class HandleErrors extends Script { const path = args[0] const lines = readFileSync(path, "utf8").split("\n") + let createdChangesets = new Set() + let refusedFiles: Set = new Set() + for (const line of lines) { if (!line?.trim()) { continue @@ -41,7 +44,8 @@ class HandleErrors extends Script { } } = JSON.parse(line) const e = parsed.message - if (e.layout !== "grb") { + if (e.layout === "grb") { + console.log("Filtering on GRB!") continue } console.log(e.username, e.layout, e.message, parsed.date) @@ -52,43 +56,48 @@ class HandleErrors extends Script { const neededIds = Changes.GetNeededIds(e.pendingChanges) // We _do not_ pass in the Changes object itself - we want the data from OSM directly in order to apply the changes - let osmObjects: { id: string, osmObj: OsmObject | "deleted" }[] = await Promise.all<{ + const osmObjects: { id: string, osmObj: OsmObject | "deleted" }[] = await Promise.all<{ id: string; osmObj: OsmObject | "deleted" }>( - neededIds.map(async id => ({ id, osmObj: await downloader.DownloadObjectAsync(id) })), + neededIds.map(async id => ({ id, osmObj: await downloader.DownloadObjectAsync(id) })) ) const objects = osmObjects .filter((obj) => obj.osmObj !== "deleted") .map((obj) => obj.osmObj) - const perType = Array.from( - Utils.Hist( - e.pendingChanges - .filter( - (descr) => - descr.meta.changeType !== undefined && descr.meta.changeType !== null, - ) - .map((descr) => descr.meta.changeType), - ), - ([key, count]) => ({ - key: key, - value: count, - aggregate: true, - }), - ) + const { toUpload, refused } = Changes.fragmentChanges(e.pendingChanges, objects) + + const changes: { newObjects: OsmObject[] modifiedObjects: OsmObject[] deletedObjects: OsmObject[] } = new Changes({ dryRun: new ImmutableStore(true), - osmConnection, - }).CreateChangesetObjects(e.pendingChanges, objects) + osmConnection + }).CreateChangesetObjects(toUpload, objects) + + const changeset = Changes.createChangesetFor("", changes) + const path = "error_changeset_" + parsed.index + "_" + e.layout + "_" + e.username + ".osc" + if(changeset === ""){ + console.log("Changes for "+parsed.index+": empty changeset, not creating a file for it") + }else if (createdChangesets.has(changeset)) { + console.log("Changeset " + parsed.index + " is identical to previously seen changeset, not writing to file") + } else { + writeFileSync(path, changeset, "utf8") + createdChangesets.add(changeset) + } + const refusedContent = JSON.stringify(refused) + if (refusedFiles.has(refusedContent)) { + console.log("Refused changes for " + parsed.index + " is identical to previously seen changeset, not writing to file") + } else { + writeFileSync(path + ".refused.json", refusedContent, "utf8") + refusedFiles.add(refusedContent) + } + console.log("Written", path, "with " + e.pendingChanges.length + " changes") - const changeset = Changes.createChangesetFor("" + parsed.index, changes) - writeFileSync("error_changeset_"+parsed.index+".osc", changeset, "utf8") } catch (e) { console.log("Parsing line failed:", e) } diff --git a/src/Logic/ImageProviders/ImageUploadManager.ts b/src/Logic/ImageProviders/ImageUploadManager.ts index 2abef273d..a1885224c 100644 --- a/src/Logic/ImageProviders/ImageUploadManager.ts +++ b/src/Logic/ImageProviders/ImageUploadManager.ts @@ -128,6 +128,9 @@ export class ImageUploadManager { targetKey, tags?.data?.["_orig_theme"] ) + + + if (!action) { return } @@ -169,13 +172,13 @@ export class ImageUploadManager { return undefined } } - console.log("Uploading done, creating action for", featureId) + console.log("Uploading image done, creating action for", featureId) key = targetKey ?? key + this.increaseCountFor(this._uploadFinished, featureId) const action = new LinkImageAction(featureId, key, value, properties, { theme: theme ?? this._layout.id, changeType: "add-image", }) - this.increaseCountFor(this._uploadFinished, featureId) return action } diff --git a/src/Logic/MetaTagging.ts b/src/Logic/MetaTagging.ts index 043f27c07..634d18759 100644 --- a/src/Logic/MetaTagging.ts +++ b/src/Logic/MetaTagging.ts @@ -82,10 +82,9 @@ export default class MetaTagging { this.updateCurrentSelectedElement() let lastUpdateMoment = new Date() const tags = state?.featureProperties?.getStore(feature.properties.id) - console.log("Binding an updater to", feature) let updateCount = 0 tags?.addCallbackD(() => { - console.log( + console.debug( "Received an update! Re-calculating the metatags, timediff:", new Date().getTime() - lastUpdateMoment.getTime() ) diff --git a/src/Logic/Osm/Actions/ChangeDescription.ts b/src/Logic/Osm/Actions/ChangeDescription.ts index 844ba01b4..fe34e623e 100644 --- a/src/Logic/Osm/Actions/ChangeDescription.ts +++ b/src/Logic/Osm/Actions/ChangeDescription.ts @@ -136,7 +136,7 @@ export class ChangeDescriptionTools { mappings: Map ): ChangeDescription { const key = change.type + "/" + change.id - +console.log("Checking remapping ", change.id, mappings, change) const wayHasChangedNode = ((change.changes ?? {})["nodes"] ?? []).some((id) => mappings.has("node/" + id) ) @@ -199,4 +199,11 @@ export class ChangeDescriptionTools { return r.asGeoJson().geometry } } + + public static rewriteAllIds( + changes: ChangeDescription[], + mappings: Map + ): ChangeDescription[] { + return changes.map(c =>ChangeDescriptionTools.rewriteIds(c, mappings)) + } } diff --git a/src/Logic/Osm/AuthConfig.ts b/src/Logic/Osm/AuthConfig.ts index b3c2330e2..11d543f80 100644 --- a/src/Logic/Osm/AuthConfig.ts +++ b/src/Logic/Osm/AuthConfig.ts @@ -3,4 +3,5 @@ export interface AuthConfig { oauth_client_id: string oauth_secret: string url: string + api_url: string } diff --git a/src/Logic/Osm/Changes.ts b/src/Logic/Osm/Changes.ts index 804f35b76..df9909204 100644 --- a/src/Logic/Osm/Changes.ts +++ b/src/Logic/Osm/Changes.ts @@ -53,7 +53,7 @@ export class Changes { featureSwitches?: FeatureSwitchState }, leftRightSensitive: boolean = false, - reportError?: (string: string | Error) => void, + reportError?: (string: string | Error) => 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) => this._reportError(e) ) this.historicalUserLocations = state.historicalUserLocations @@ -82,7 +82,7 @@ export class Changes { modifiedObjects: OsmObject[] newObjects: OsmObject[] deletedObjects: OsmObject[] - }, + } ): string { const changedElements = allChanges.modifiedObjects ?? [] const newElements = allChanges.newObjects ?? [] @@ -120,6 +120,7 @@ export class Changes { }) return items } + const metatagsDocs: { key?: string value?: string @@ -132,46 +133,46 @@ export class Changes { [ { key: "comment", - docs: "The changeset comment. Will be a fixed string, mentioning the theme", + docs: "The changeset comment. Will be a fixed string, mentioning the theme" }, { key: "theme", - docs: "The name of the theme that was used to create this change. ", + docs: "The name of the theme that was used to create this change. " }, { key: "source", value: "survey", - docs: "The contributor had their geolocation enabled while making changes", + docs: "The contributor had their geolocation enabled while making changes" }, { key: "change_within_{distance}", - docs: "If the contributor enabled their geolocation, this will hint how far away they were from the objects they edited. This gives an indication of proximity and if they truly surveyed or were armchair-mapping", + docs: "If the contributor enabled their geolocation, this will hint how far away they were from the objects they edited. This gives an indication of proximity and if they truly surveyed or were armchair-mapping" }, { key: "change_over_{distance}", - docs: "If the contributor enabled their geolocation, this will hint how far away they were from the objects they edited. If they were over 5000m away, the might have been armchair-mapping", + docs: "If the contributor enabled their geolocation, this will hint how far away they were from the objects they edited. If they were over 5000m away, the might have been armchair-mapping" }, { key: "created_by", value: "MapComplete ", - docs: "The piece of software used to create this changeset; will always start with MapComplete, followed by the version number", + docs: "The piece of software used to create this changeset; will always start with MapComplete, followed by the version number" }, { key: "locale", value: "en|nl|de|...", - docs: "The code of the language that the contributor used MapComplete in. Hints what language the user speaks.", + docs: "The code of the language that the contributor used MapComplete in. Hints what language the user speaks." }, { key: "host", value: "https://mapcomplete.org/", - docs: "The URL that the contributor used to make changes. One can see the used instance with this", + docs: "The URL that the contributor used to make changes. One can see the used instance with this" }, { key: "imagery", - 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)", - }, + 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"), @@ -193,15 +194,15 @@ export class Changes { metatagsDocs.map(({ key, value, docs, source, changeType, specialMotivation }) => [ key ?? changeType?.join(", ") ?? "", value, - [ + [ docs, specialMotivation ? "This might give a reason per modified node or way" - : "", + : "" ].join("\n"), - source, - ]), - ), + source + ]) + ) ].join("\n\n") } @@ -247,11 +248,13 @@ export class Changes { public async applyAction(action: OsmChangeAction): Promise { const changeDescriptions = await action.Perform(this) - changeDescriptions[0].meta.distanceToObject = this.calculateDistanceToChanges( + const remapped = ChangeDescriptionTools.rewriteAllIds(changeDescriptions, this._changesetHandler._remappings) + + remapped[0].meta.distanceToObject = this.calculateDistanceToChanges( action, - changeDescriptions, + remapped ) - this.applyChanges(changeDescriptions) + this.applyChanges(remapped) } public applyChanges(changes: ChangeDescription[]) { @@ -263,14 +266,30 @@ export class Changes { public CreateChangesetObjects( changes: ChangeDescription[], - downloadedOsmObjects: OsmObject[], + downloadedOsmObjects: OsmObject[] ): { newObjects: OsmObject[] modifiedObjects: OsmObject[] deletedObjects: OsmObject[] } { - const objects: Map = new Map() + /** + * This is a rather complicated method which does a lot of stuff. + * + * Our main important data is `state` and `objects` which will determine what is returned. + * First init all those states, then we actually apply the changes. + * At last, we sort them for easy handling, which is rather boring + */ + + // ------------------ INIT ------------------------- + + /** + * Keeps track of every object what actually happened with it + */ const states: Map = new Map() + /** + * Keeps track of the _new_ state of the objects, how they should end up on the database + */ + const objects: Map = new Map() for (const o of downloadedOsmObjects) { objects.set(o.type + "/" + o.id, o) @@ -282,6 +301,8 @@ export class Changes { states.set(o.type + "/" + o.id, "unchanged") } + // -------------- APPLY INTERMEDIATE CHANGES ----------------- + for (const change of changes) { let changed = false const id = change.type + "/" + change.id @@ -293,7 +314,7 @@ export class Changes { } if (change.changes === undefined) { // This object is a change to a newly created object. However, we have not seen the creation changedescription yet! - throw "Not a creation of the object" + throw "Not a creation of the object: " + JSON.stringify(change) } // This is a new object that should be created states.set(id, "created") @@ -399,10 +420,12 @@ export class Changes { } } + + // ----------------- SORT OBJECTS ------------------- const result = { newObjects: [], modifiedObjects: [], - deletedObjects: [], + deletedObjects: [] } objects.forEach((v, id) => { @@ -425,14 +448,14 @@ export class Changes { result.modifiedObjects.length, "modified;", result.deletedObjects, - "deleted", + "deleted" ) return result } private calculateDistanceToChanges( change: OsmChangeAction, - changeDescriptions: ChangeDescription[], + changeDescriptions: ChangeDescription[] ) { const locations = this.historicalUserLocations?.features?.data if (locations === undefined) { @@ -452,7 +475,7 @@ export class Changes { .filter((feat) => feat.geometry.type === "Point") .filter((feat) => { const visitTime = new Date( - ((feat.properties)).date, + ((feat.properties)).date ) // In seconds const diff = (now.getTime() - visitTime.getTime()) / 1000 @@ -499,9 +522,9 @@ export class Changes { ...recentLocationPoints.map((gpsPoint) => { const otherCoor = GeoOperations.centerpointCoordinates(gpsPoint) return GeoOperations.distanceBetween(coor, otherCoor) - }), - ), - ), + }) + ) + ) ) } @@ -538,19 +561,50 @@ export class Changes { } } + public static fragmentChanges(pending: ChangeDescription[], objects: OsmObject[]): { + refused: ChangeDescription[]; + toUpload: ChangeDescription[] + } { + const refused: ChangeDescription[] = [] + const toUpload: ChangeDescription[] = [] + + // All ids which have an 'update' + const createdIds = + new Set(pending.filter(cd => cd.type === "node" && cd.changes !== undefined).map(cd => cd.id)) + pending.forEach(c => { + if (c.id < 0) { + if (createdIds.has(c.id)) { + toUpload.push(c) + } else { + reportError(`Got an orphaned change. The 'creation'-change description for ${c.type}/${c.id} got lost. Permanently dropping this change:`+JSON.stringify(c)) + } + return + } + const matchFound = !!objects.find(o => o.id === c.id && o.type === c.type) + if (matchFound) { + toUpload.push(c) + } else { + refused.push(c) + } + }) + + return {refused, toUpload} + + } + /** * Upload the selected changes to OSM. This is typically called with changes for a single theme * @return pending changes which could not be uploaded for some reason; undefined or empty array if successful */ private async flushSelectChanges( pending: ChangeDescription[], - openChangeset: UIEventSource, + openChangeset: UIEventSource ): Promise { 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 const downloader = new OsmObjectDownloader(this.backend, undefined) let osmObjects = await Promise.all<{ id: string; osmObj: OsmObject | "deleted" }>( - neededIds.map(id => this.getOsmObject(id, downloader)), + neededIds.map(id => this.getOsmObject(id, downloader)) ) osmObjects = Utils.NoNull(osmObjects) @@ -579,21 +633,21 @@ export class Changes { 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, - }), + aggregate: true + }) ) const motivations = pending .filter((descr) => descr.meta.specialMotivation !== undefined) .map((descr) => ({ key: descr.meta.changeType + ":" + descr.type + "/" + descr.id, - value: descr.meta.specialMotivation, + value: descr.meta.specialMotivation })) const distances = Utils.NoNull(pending.map((descr) => descr.meta.distanceToObject)) @@ -624,9 +678,9 @@ export class Changes { return { key, value: count, - aggregate: true, + aggregate: true } - }), + }) ) // This method is only called with changedescriptions for this theme @@ -639,32 +693,21 @@ export class Changes { const metatags: ChangesetTag[] = [ { key: "comment", - value: comment, + value: comment }, { key: "theme", - value: theme, + value: theme }, ...perType, ...motivations, - ...perBinMessage, + ...perBinMessage ] - const refused: ChangeDescription[] = [] - let toUpload: ChangeDescription[] = [] - pending.forEach(c => { - if (c.id < 0) { - toUpload.push(c) - return - } - const matchFound = !!objects.find(o => o.id === c.id && o.type === c.type) - if (matchFound) { - toUpload.push(c) - } else { - refused.push(c) - } - }) + let {toUpload, refused} = Changes.fragmentChanges( + pending, objects + ) await this._changesetHandler.UploadChangeset( (csId, remappings) => { @@ -681,10 +724,10 @@ export class Changes { return Changes.createChangesetFor("" + csId, changes) }, metatags, - openChangeset, + openChangeset ) - console.log("Upload successful! Refused changes are",refused) + console.log("Upload successful! Refused changes are", refused) return refused } @@ -708,14 +751,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, + openChangeset.data ) const refused = await self.flushSelectChanges(pendingChanges, openChangeset) @@ -730,7 +773,7 @@ export class Changes { this.errors.ping() return pendingChanges } - }), + }) ) // We keep all the refused changes to try them again @@ -738,7 +781,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() diff --git a/src/Logic/Osm/ChangesetHandler.ts b/src/Logic/Osm/ChangesetHandler.ts index f66a4d0fa..315a8a960 100644 --- a/src/Logic/Osm/ChangesetHandler.ts +++ b/src/Logic/Osm/ChangesetHandler.ts @@ -94,6 +94,7 @@ export class ChangesetHandler { return hasChange } + /** * The full logic to upload a change to one or more elements. * @@ -187,7 +188,7 @@ export class ChangesetHandler { await this.UpdateTags(csId, rewrittenTags) } catch (e) { if (this._reportError) { - this._reportError("Could not reuse changeset "+csId+", might be closed: " + (e.stacktrace ?? ("" + e))) + this._reportError("Could not reuse changeset " + csId + ", might be closed: " + (e.stacktrace ?? ("" + e))) } console.warn("Could not upload, changeset is probably closed: ", e) openChangeset.setData(undefined) @@ -236,7 +237,7 @@ export class ChangesetHandler { if (newMetaTag === undefined) { extraMetaTags.push({ key: key, - value: oldCsTags[key], + value: oldCsTags[key] }) continue } @@ -373,11 +374,11 @@ export class ChangesetHandler { ["locale", Locale.language.data], ["host", `${window.location.origin}${window.location.pathname}`], ["source", 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 })) } @@ -409,7 +410,7 @@ export class ChangesetHandler { changesetId: number, changesetXML: string ): Promise> { - const response = await this.osmConnection.post( + const response = await this.osmConnection.post( "changeset/" + changesetId + "/upload", changesetXML, { "Content-Type": "text/xml" } diff --git a/src/Logic/Osm/OsmConnection.ts b/src/Logic/Osm/OsmConnection.ts index 246e1a1a5..71b2869f9 100644 --- a/src/Logic/Osm/OsmConnection.ts +++ b/src/Logic/Osm/OsmConnection.ts @@ -214,7 +214,7 @@ export class OsmConnection { this.auth.xhr( { method: "GET", - path: "/api/0.6/user/details", + path: "/api/0.6/user/details" }, (err, details: XMLDocument) => { if (err != null) { @@ -326,9 +326,9 @@ export class OsmConnection { method, 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 { @@ -339,7 +339,7 @@ export class OsmConnection { }) } - public async post( + public async post( path: string, content?: string, header?: Record, @@ -408,7 +408,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 ) @@ -449,7 +449,7 @@ export class OsmConnection { file: gpx, description: options.description, tags: options.labels?.join(",") ?? "", - visibility: options.visibility, + visibility: options.visibility } if (!contents.description) { @@ -457,9 +457,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" @@ -467,7 +467,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] } @@ -478,7 +478,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) @@ -499,9 +499,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 { @@ -516,7 +516,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") @@ -534,6 +534,7 @@ export class OsmConnection { : window.location.protocol + "//" + window.location.host + "/land.html", singlepage: true, // We always use 'singlePage', it is the most stable - including in PWA auto: true, + apiUrl: this._oauth_config.api_url ?? this._oauth_config.url }) } diff --git a/src/Logic/Osm/OsmObject.ts b/src/Logic/Osm/OsmObject.ts index 03048f408..6782f4495 100644 --- a/src/Logic/Osm/OsmObject.ts +++ b/src/Logic/Osm/OsmObject.ts @@ -3,10 +3,11 @@ import polygon_features from "../../assets/polygon-features.json" import { OsmFeature, OsmId, OsmTags, WayId } from "../../Models/OsmFeature" import OsmToGeoJson from "osmtogeojson" import { Feature, LineString, Polygon } from "geojson" +import Constants from "../../Models/Constants" export abstract class OsmObject { private static defaultBackend = "https://api.openstreetmap.org/" - protected static backendURL = OsmObject.defaultBackend + private static backendURL = Constants.osmAuthConfig.url ?? OsmObject.defaultBackend public static polygonFeatures = OsmObject.constructPolygonFeatures() type: "node" | "way" | "relation" id: number diff --git a/src/Logic/SimpleMetaTagger.ts b/src/Logic/SimpleMetaTagger.ts index 84d4358d8..389d0f0dc 100644 --- a/src/Logic/SimpleMetaTagger.ts +++ b/src/Logic/SimpleMetaTagger.ts @@ -635,6 +635,9 @@ export default class SimpleMetaTaggers { isLazy: true, }, (feature: Feature, layer: LayerConfig, tagsStore: UIEventSource) => { + if(tagsStore === undefined){ + return + } Utils.AddLazyPropertyAsync(feature.properties, "_currency", async () => { // Wait until _country is actually set const tags = await tagsStore.AsPromise((tags) => !!tags._country) diff --git a/src/Logic/State/FeatureSwitchState.ts b/src/Logic/State/FeatureSwitchState.ts index ec0dedbd1..0487ff54b 100644 --- a/src/Logic/State/FeatureSwitchState.ts +++ b/src/Logic/State/FeatureSwitchState.ts @@ -179,7 +179,8 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches { let testingDefaultValue = false if ( !Utils.runningFromConsole && - (location.hostname === "localhost" || location.hostname === "127.0.0.1") + (location.hostname === "localhost" || location.hostname === "127.0.0.1") && + !Constants.osmAuthConfig.url.startsWith("https://master.apis.dev.openstreetmap.org") ) { testingDefaultValue = true } diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 32b8b9745..26b608e76 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -158,7 +158,7 @@ export default class ThemeViewState implements SpecialVisualizationState { this.featureSwitches = new FeatureSwitchState(layout) this.guistate = new MenuState( this.featureSwitches.featureSwitchWelcomeMessage.data, - layout.id + layout.id, ) this.map = new UIEventSource(undefined) const geolocationState = new GeoLocationState() @@ -174,14 +174,14 @@ export default class ThemeViewState implements SpecialVisualizationState { oauth_token: QueryParameters.GetQueryParameter( "oauth_token", undefined, - "Used to complete the login" + "Used to complete the login", ), }) this.userRelatedState = new UserRelatedState( this.osmConnection, layout, this.featureSwitches, - this.mapProperties + this.mapProperties, ) this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => { this.mapProperties.allowRotating.setData(fixated !== "yes") @@ -192,13 +192,13 @@ export default class ThemeViewState implements SpecialVisualizationState { geolocationState, this.selectedElement, this.mapProperties, - this.userRelatedState.gpsLocationHistoryRetentionTime + this.userRelatedState.gpsLocationHistoryRetentionTime, ) this.geolocationControl = new GeolocationControlState(this.geolocation, this.mapProperties) this.availableLayers = AvailableRasterLayers.layersAvailableAt( this.mapProperties.location, - this.osmConnection.isLoggedIn + this.osmConnection.isLoggedIn, ) const self = this @@ -206,7 +206,7 @@ export default class ThemeViewState implements SpecialVisualizationState { this.osmConnection, layout.layers, layout.id, - this.featureSwitches.featureSwitchLayerDefault + this.featureSwitches.featureSwitchLayerDefault, ) { @@ -215,7 +215,7 @@ export default class ThemeViewState implements SpecialVisualizationState { const isDisplayed = QueryParameters.GetBooleanQueryParameter( "overlay-" + rasterInfo.id, rasterInfo.defaultState ?? true, - "Whether or not overlay layer " + rasterInfo.id + " is shown" + "Whether or not overlay layer " + rasterInfo.id + " is shown", ) const state = { isDisplayed } overlayLayerStates.set(rasterInfo.id, state) @@ -240,7 +240,7 @@ export default class ThemeViewState implements SpecialVisualizationState { this.osmConnection.Backend(), (id) => self.layerState.filteredLayers.get(id).isDisplayed, mvtAvailableLayers, - this.fullNodeDatabase + this.fullNodeDatabase, ) let currentViewIndex = 0 @@ -258,7 +258,7 @@ export default class ThemeViewState implements SpecialVisualizationState { id: "current_view_" + currentViewIndex, }), ] - }) + }), ) this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds) @@ -276,19 +276,19 @@ export default class ThemeViewState implements SpecialVisualizationState { featureSwitches: this.featureSwitches, }, layout?.isLeftRightSensitive() ?? false, - (e) => this.reportError(e) + (e) => this.reportError(e), ) this.historicalUserLocations = this.geolocation.historicalUserLocations this.newFeatures = new NewGeometryFromChangesFeatureSource( this.changes, layoutSource, - this.featureProperties + this.featureProperties, ) layoutSource.addSource(this.newFeatures) const perLayer = new PerLayerFeatureSourceSplitter( Array.from(this.layerState.filteredLayers.values()).filter( - (l) => l.layerDef?.source !== null + (l) => l.layerDef?.source !== null, ), new ChangeGeometryApplicator(this.indexedFeatures, this.changes), { @@ -299,10 +299,10 @@ export default class ThemeViewState implements SpecialVisualizationState { "Got ", features.length, "leftover features, such as", - features[0].properties + features[0].properties, ) }, - } + }, ) this.perLayer = perLayer.perLayer } @@ -342,12 +342,12 @@ export default class ThemeViewState implements SpecialVisualizationState { this.lastClickObject = new LastClickFeatureSource( this.layout, this.mapProperties.lastClickLocation, - this.userRelatedState.addNewFeatureMode + this.userRelatedState.addNewFeatureMode, ) this.osmObjectDownloader = new OsmObjectDownloader( this.osmConnection.Backend(), - this.changes + this.changes, ) this.perLayerFiltered = this.showNormalDataOn(this.map) @@ -358,7 +358,7 @@ export default class ThemeViewState implements SpecialVisualizationState { currentZoom: this.mapProperties.zoom, layerState: this.layerState, bounds: this.visualFeedbackViewportBounds, - } + }, ) this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView this.imageUploadManager = new ImageUploadManager( @@ -366,12 +366,12 @@ export default class ThemeViewState implements SpecialVisualizationState { Imgur.singleton, this.featureProperties, this.osmConnection, - this.changes + this.changes, ) this.favourites = new FavouritesFeatureSource(this) this.featureSummary = this.setupSummaryLayer( - new LayerConfig(summaryLayer, "summaryLayer", true) + new LayerConfig(summaryLayer, "summaryLayer", true), ) this.toCacheSavers = this.initSaveToLocalStorage() this.initActors() @@ -411,7 +411,7 @@ export default class ThemeViewState implements SpecialVisualizationState { LayoutSource.fromCacheZoomLevel, fs, this.featureProperties, - fs.layer.layerDef.maxAgeOfCache + fs.layer.layerDef.maxAgeOfCache, ) toLocalStorage.set(layerId, storage) }) @@ -424,7 +424,7 @@ export default class ThemeViewState implements SpecialVisualizationState { const doShowLayer = this.mapProperties.zoom.map( (z) => (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), - [fs.layer.isDisplayed] + [fs.layer.isDisplayed], ) if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) { @@ -441,7 +441,7 @@ export default class ThemeViewState implements SpecialVisualizationState { fs.layer, fs, (id) => this.featureProperties.getStore(id), - this.layerState.globalFilters + this.layerState.globalFilters, ) filteringFeatureSource.set(layerName, filtered) @@ -469,7 +469,7 @@ export default class ThemeViewState implements SpecialVisualizationState { public showCurrentLocationOn(map: Store): ShowDataLayer { const id = "gps_location" const flayerGps = this.layerState.filteredLayers.get(id) - if(flayerGps === undefined){ + if (flayerGps === undefined) { return } const features = this.geolocation.currentUserLocation @@ -585,7 +585,7 @@ export default class ThemeViewState implements SpecialVisualizationState { return } this.selectClosestAtCenter(0) - } + }, ) for (let i = 1; i < 9; i++) { @@ -603,7 +603,7 @@ export default class ThemeViewState implements SpecialVisualizationState { onUp: true, }, doc, - () => this.selectClosestAtCenter(i - 1) + () => this.selectClosestAtCenter(i - 1), ) } @@ -620,7 +620,7 @@ export default class ThemeViewState implements SpecialVisualizationState { if (this.featureSwitches.featureSwitchBackgroundSelection.data) { this.guistate.backgroundLayerSelectionIsOpened.setData(true) } - } + }, ) Hotkeys.RegisterHotkey( { @@ -632,14 +632,14 @@ export default class ThemeViewState implements SpecialVisualizationState { if (this.featureSwitches.featureSwitchFilter.data) { this.guistate.openFilterView() } - } + }, ) Hotkeys.RegisterHotkey( { shift: "O" }, Translations.t.hotkeyDocumentation.selectMapnik, () => { this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto) - } + }, ) const setLayerCategory = (category: EliCategory) => { const available = this.availableLayers.data @@ -647,7 +647,7 @@ export default class ThemeViewState implements SpecialVisualizationState { const best = RasterLayerUtils.SelectBestLayerAccordingTo( available, category, - current.data + current.data, ) console.log("Best layer for category", category, "is", best.properties.id) current.setData(best) @@ -656,26 +656,26 @@ export default class ThemeViewState implements SpecialVisualizationState { Hotkeys.RegisterHotkey( { nomod: "O" }, Translations.t.hotkeyDocumentation.selectOsmbasedmap, - () => setLayerCategory("osmbasedmap") + () => setLayerCategory("osmbasedmap"), ) Hotkeys.RegisterHotkey( { nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, - () => setLayerCategory("map") + () => setLayerCategory("map"), ) Hotkeys.RegisterHotkey( { nomod: "P" }, Translations.t.hotkeyDocumentation.selectAerial, - () => setLayerCategory("photo") + () => setLayerCategory("photo"), ) Hotkeys.RegisterHotkey( { nomod: "L" }, Translations.t.hotkeyDocumentation.geolocate, () => { this.geolocationControl.handleClick() - } + }, ) return true }) @@ -687,7 +687,7 @@ export default class ThemeViewState implements SpecialVisualizationState { Translations.t.hotkeyDocumentation.translationMode, () => { Locale.showLinkToWeblate.setData(!Locale.showLinkToWeblate.data) - } + }, ) } @@ -698,7 +698,7 @@ export default class ThemeViewState implements SpecialVisualizationState { const normalLayers = this.layout.layers.filter( (l) => Constants.priviliged_layers.indexOf(l.id) < 0 && - !l.id.startsWith("note_import") + !l.id.startsWith("note_import"), ) const maxzoom = Math.min(...normalLayers.map((l) => l.minzoom)) @@ -706,7 +706,7 @@ export default class ThemeViewState implements SpecialVisualizationState { (l) => Constants.priviliged_layers.indexOf(l.id) < 0 && l.source.geojsonSource === undefined && - l.doCount + l.doCount, ) const summaryTileSource = new SummaryTileSource( Constants.SummaryServer, @@ -715,7 +715,7 @@ export default class ThemeViewState implements SpecialVisualizationState { this.mapProperties, { isActive: this.mapProperties.zoom.map((z) => z < maxzoom), - } + }, ) const src = new SummaryTileSourceRewriter(summaryTileSource, this.layerState.filteredLayers) @@ -737,12 +737,12 @@ export default class ThemeViewState implements SpecialVisualizationState { gps_location_history: this.geolocation.historicalUserLocations, gps_track: this.geolocation.historicalUserLocationsTrack, selected_element: new StaticFeatureSource( - this.selectedElement.map((f) => (f === undefined ? empty : [f])) + this.selectedElement.map((f) => (f === undefined ? empty : [f])), ), range: new StaticFeatureSource( this.mapProperties.maxbounds.map((bbox) => - bbox === undefined ? empty : [bbox.asGeoJson({ id: "range" })] - ) + bbox === undefined ? empty : [bbox.asGeoJson({ id: "range" })], + ), ), current_view: this.currentView, favourite: this.favourites, @@ -757,7 +757,7 @@ export default class ThemeViewState implements SpecialVisualizationState { ShowDataLayer.showRange( this.map, new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]), - this.featureSwitches.featureSwitchIsTesting + this.featureSwitches.featureSwitchIsTesting, ) } const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view") @@ -771,7 +771,7 @@ export default class ThemeViewState implements SpecialVisualizationState { currentViewLayer, this.layout, this.osmObjectDownloader, - this.featureProperties + this.featureProperties, ) }) } @@ -815,20 +815,20 @@ export default class ThemeViewState implements SpecialVisualizationState { const lastClickLayerConfig = new LayerConfig( last_click_layerconfig, - "last_click" + "last_click", ) const lastClickFiltered = lastClickLayerConfig.isShown === undefined ? specialLayers.last_click : specialLayers.last_click.features.mapD((fs) => - fs.filter((f) => { - const matches = lastClickLayerConfig.isShown.matchesProperties( - f.properties - ) - console.debug("LastClick ", f, "matches", matches) - return matches - }) - ) + fs.filter((f) => { + const matches = lastClickLayerConfig.isShown.matchesProperties( + f.properties, + ) + console.debug("LastClick ", f, "matches", matches) + return matches + }), + ) new ShowDataLayer(this.map, { features: new StaticFeatureSource(lastClickFiltered), layer: lastClickLayerConfig, @@ -873,7 +873,7 @@ export default class ThemeViewState implements SpecialVisualizationState { this.mapProperties.rasterLayer, this.availableLayers, this.featureSwitches.backgroundLayerId, - this.userRelatedState.preferredBackgroundLayer + this.userRelatedState.preferredBackgroundLayer, ) } @@ -883,29 +883,37 @@ export default class ThemeViewState implements SpecialVisualizationState { } public async reportError(message: string | Error | XMLHttpRequest) { - console.log(">>> Reporting error to", Constants.ErrorReportServer, message) + const isTesting = this.featureSwitchIsTesting.data + console.log(isTesting ? ">>> _Not_ reporting error to report server as testmode is on" : ">>> Reporting error to", Constants.ErrorReportServer, message) + if (isTesting) { + return + } if ("" + message === "[object XMLHttpRequest]") { const req = message message = "XMLHttpRequest with status code " + req.status + ", " + req.statusText } - let stacktrace: string = new Error().stack + const stacktrace: string = new Error().stack - await fetch(Constants.ErrorReportServer, { - method: "POST", - body: JSON.stringify({ - stacktrace, - message: "" + message, - layout: this.layout.id, - version: Constants.vNumber, - language: this.userRelatedState.language.data, - username: this.osmConnection.userDetails.data?.name, - userid: this.osmConnection.userDetails.data?.uid, - pendingChanges: this.changes.pendingChanges.data, - previousChanges: this.changes.allChanges.data, - changeRewrites: Utils.MapToObj(this.changes._changesetHandler._remappings) - }), - }) + try { + await fetch(Constants.ErrorReportServer, { + method: "POST", + body: JSON.stringify({ + stacktrace, + message: "" + message, + layout: this.layout.id, + version: Constants.vNumber, + language: this.userRelatedState.language.data, + username: this.osmConnection.userDetails.data?.name, + userid: this.osmConnection.userDetails.data?.uid, + pendingChanges: this.changes.pendingChanges.data, + previousChanges: this.changes.allChanges.data, + changeRewrites: Utils.MapToObj(this.changes._changesetHandler._remappings), + }), + }) + } catch (e) { + console.error("Could not upload an error report") + } } } diff --git a/src/UI/Map/MaplibreMap.svelte b/src/UI/Map/MaplibreMap.svelte index 882398f51..36f54c43a 100644 --- a/src/UI/Map/MaplibreMap.svelte +++ b/src/UI/Map/MaplibreMap.svelte @@ -70,7 +70,7 @@ try{ _map?.remove() }catch (e) { - console.log("Could not remove map due to", e) + console.debug("Could not remove map due to", e) } if(autorecovery){ requestAnimationFrame(() => { diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index c709b20bd..feefe1ab6 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -299,6 +299,11 @@
Testmode
+ {#if state.osmConnection.Backend().startsWith("https://master.apis.dev.openstreetmap.org")} +
+ Testserver +
+ {/if}
Faking a user (Testmode)