diff --git a/Logic/Actors/SelectedElementTagsUpdater.ts b/Logic/Actors/SelectedElementTagsUpdater.ts index c5b23d0f3..8bfe511d8 100644 --- a/Logic/Actors/SelectedElementTagsUpdater.ts +++ b/Logic/Actors/SelectedElementTagsUpdater.ts @@ -3,13 +3,13 @@ */ import { UIEventSource } from "../UIEventSource" import { Changes } from "../Osm/Changes" -import { OsmObject } from "../Osm/OsmObject" import { OsmConnection } from "../Osm/OsmConnection" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import SimpleMetaTagger from "../SimpleMetaTagger" import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" import { Feature } from "geojson" import { OsmTags } from "../../Models/OsmFeature" +import OsmObjectDownloader from "../Osm/OsmObjectDownloader" export default class SelectedElementTagsUpdater { private static readonly metatags = new Set([ @@ -27,6 +27,7 @@ export default class SelectedElementTagsUpdater { changes: Changes osmConnection: OsmConnection layout: LayoutConfig + osmObjectDownloader: OsmObjectDownloader } constructor(state: { @@ -35,6 +36,7 @@ export default class SelectedElementTagsUpdater { changes: Changes osmConnection: OsmConnection layout: LayoutConfig + osmObjectDownloader: OsmObjectDownloader }) { this.state = state state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { @@ -70,8 +72,8 @@ export default class SelectedElementTagsUpdater { return } try { - const latestTags = await OsmObject.DownloadPropertiesOf(id) - if (latestTags === "deleted") { + const osmObject = await state.osmObjectDownloader.DownloadObjectAsync(id) + if (osmObject === "deleted") { console.warn("The current selected element has been deleted upstream!") const currentTagsSource = state.featureProperties.getStore(id) if (currentTagsSource.data["_deleted"] === "yes") { @@ -81,6 +83,7 @@ export default class SelectedElementTagsUpdater { currentTagsSource.ping() return } + const latestTags = osmObject.tags this.applyUpdate(latestTags, id) console.log("Updated", id) } catch (e) { diff --git a/Logic/Actors/SelectedFeatureHandler.ts b/Logic/Actors/SelectedFeatureHandler.ts index 3591bc4eb..3d30a9ca1 100644 --- a/Logic/Actors/SelectedFeatureHandler.ts +++ b/Logic/Actors/SelectedFeatureHandler.ts @@ -1,10 +1,10 @@ import { UIEventSource } from "../UIEventSource" -import { OsmObject } from "../Osm/OsmObject" import Loc from "../../Models/Loc" import { ElementStorage } from "../ElementStorage" import FeaturePipeline from "../FeatureSource/FeaturePipeline" import { GeoOperations } from "../GeoOperations" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import OsmObjectDownloader from "../Osm/OsmObjectDownloader" /** * Makes sure the hash shows the selected element and vice-versa. @@ -26,6 +26,7 @@ export default class SelectedFeatureHandler { allElements: ElementStorage locationControl: UIEventSource layoutToUse: LayoutConfig + objectDownloader: OsmObjectDownloader } constructor( @@ -36,6 +37,7 @@ export default class SelectedFeatureHandler { featurePipeline: FeaturePipeline locationControl: UIEventSource layoutToUse: LayoutConfig + objectDownloader: OsmObjectDownloader } ) { this.hash = hash @@ -65,8 +67,11 @@ export default class SelectedFeatureHandler { return } - OsmObject.DownloadObjectAsync(hash).then((obj) => { + this.state.objectDownloader.DownloadObjectAsync(hash).then((obj) => { try { + if (obj === "deleted") { + return + } console.log("Downloaded selected object from OSM-API for initial load: ", hash) const geojson = obj.asGeoJson() this.state.allElements.addOrGetElement(geojson) diff --git a/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts b/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts index 44b6e13f5..c3cc099d8 100644 --- a/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts +++ b/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts @@ -1,10 +1,11 @@ import { Changes } from "../../Osm/Changes" -import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject" +import { OsmNode, OsmRelation, OsmWay } from "../../Osm/OsmObject" import { IndexedFeatureSource, WritableFeatureSource } from "../FeatureSource" import { UIEventSource } from "../../UIEventSource" import { ChangeDescription } from "../../Osm/Actions/ChangeDescription" import { OsmId, OsmTags } from "../../../Models/OsmFeature" import { Feature } from "geojson" +import OsmObjectDownloader from "../../Osm/OsmObjectDownloader" export class NewGeometryFromChangesFeatureSource implements WritableFeatureSource { // This class name truly puts the 'Java' into 'Javascript' @@ -21,7 +22,7 @@ export class NewGeometryFromChangesFeatureSource implements WritableFeatureSourc const seenChanges = new Set() const features = this.features.data const self = this - + const backend = changes.backend changes.pendingChanges.stabilized(100).addCallbackAndRunD((changes) => { if (changes.length === 0) { return @@ -58,15 +59,20 @@ export class NewGeometryFromChangesFeatureSource implements WritableFeatureSourc } console.debug("Detected a reused point") // The 'allElementsStore' does _not_ have this point yet, so we have to create it - OsmObject.DownloadObjectAsync(change.type + "/" + change.id).then((feat) => { - console.log("Got the reused point:", feat) - for (const kv of change.tags) { - feat.tags[kv.k] = kv.v - } - const geojson = feat.asGeoJson() - self.features.data.push(geojson) - self.features.ping() - }) + new OsmObjectDownloader(backend) + .DownloadObjectAsync(change.type + "/" + change.id) + .then((feat) => { + console.log("Got the reused point:", feat) + if (feat === "deleted") { + throw "Panic: snapping to a point, but this point has been deleted in the meantime" + } + for (const kv of change.tags) { + feat.tags[kv.k] = kv.v + } + const geojson = feat.asGeoJson() + self.features.data.push(geojson) + self.features.ping() + }) continue } else if (change.id < 0 && change.changes === undefined) { // The geometry is not described - not a new point diff --git a/Logic/FeatureSource/Sources/OsmFeatureSource.ts b/Logic/FeatureSource/Sources/OsmFeatureSource.ts index 91a819e2b..0c8949f7c 100644 --- a/Logic/FeatureSource/Sources/OsmFeatureSource.ts +++ b/Logic/FeatureSource/Sources/OsmFeatureSource.ts @@ -4,9 +4,9 @@ import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" import { Tiles } from "../../../Models/TileRange" import { BBox } from "../../BBox" import { TagsFilter } from "../../Tags/TagsFilter" -import { OsmObject } from "../../Osm/OsmObject" import { Feature } from "geojson" import FeatureSourceMerger from "../Sources/FeatureSourceMerger" +import OsmObjectDownloader from "../../Osm/OsmObjectDownloader" /** * If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile' @@ -101,7 +101,17 @@ export default class OsmFeatureSource extends FeatureSourceMerger { // This member is missing. We redownload the entire relation instead console.debug("Fetching incomplete relation " + feature.properties.id) - return (await OsmObject.DownloadObjectAsync(feature.properties.id)).asGeoJson() + const dfeature = await new OsmObjectDownloader(this._backend).DownloadObjectAsync( + feature.properties.id + ) + if (dfeature === "deleted") { + console.warn( + "This relation has been deleted in the meantime: ", + feature.properties.id + ) + return + } + return dfeature.asGeoJson() } return feature } @@ -149,6 +159,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger { for (let i = 0; i < features.length; i++) { features[i] = await this.patchIncompleteRelations(features[i], osmJson) } + features = Utils.NoNull(features) features.forEach((f) => { f.properties["_backend"] = this._backend }) diff --git a/Logic/Osm/Actions/DeleteAction.ts b/Logic/Osm/Actions/DeleteAction.ts index 9ab5fab49..97a865056 100644 --- a/Logic/Osm/Actions/DeleteAction.ts +++ b/Logic/Osm/Actions/DeleteAction.ts @@ -8,6 +8,7 @@ import { And } from "../../Tags/And" import { Tag } from "../../Tags/Tag" import { OsmId } from "../../../Models/OsmFeature" import { Utils } from "../../../Utils" +import OsmObjectDownloader from "../OsmObjectDownloader"; export default class DeleteAction extends OsmChangeAction { private readonly _softDeletionTags: TagsFilter @@ -71,8 +72,12 @@ export default class DeleteAction extends OsmChangeAction { changes: Changes, object?: OsmObject ): Promise { - const osmObject = object ?? (await OsmObject.DownloadObjectAsync(this._id)) + const osmObject = object ?? (await new OsmObjectDownloader(changes.backend, changes).DownloadObjectAsync(this._id)) + if(osmObject === "deleted"){ + // already deleted in the meantime - no more changes necessary + return [] + } if (this._hardDelete) { return [ { diff --git a/Logic/Osm/Actions/RelationSplitHandler.ts b/Logic/Osm/Actions/RelationSplitHandler.ts index 1fd6749b8..3cdc6cc43 100644 --- a/Logic/Osm/Actions/RelationSplitHandler.ts +++ b/Logic/Osm/Actions/RelationSplitHandler.ts @@ -1,7 +1,8 @@ import OsmChangeAction from "./OsmChangeAction" import { Changes } from "../Changes" import { ChangeDescription } from "./ChangeDescription" -import { OsmObject, OsmRelation, OsmWay } from "../OsmObject" +import { OsmRelation, OsmWay } from "../OsmObject" +import OsmObjectDownloader from "../OsmObjectDownloader" export interface RelationSplitInput { relation: OsmRelation @@ -14,11 +15,13 @@ export interface RelationSplitInput { abstract class AbstractRelationSplitHandler extends OsmChangeAction { protected readonly _input: RelationSplitInput protected readonly _theme: string + protected readonly _objectDownloader: OsmObjectDownloader - constructor(input: RelationSplitInput, theme: string) { + constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) { super("relation/" + input.relation.id, false) this._input = input this._theme = theme + this._objectDownloader = objectDownloader } /** @@ -33,7 +36,9 @@ abstract class AbstractRelationSplitHandler extends OsmChangeAction { return member.ref } if (member.type === "way") { - const osmWay = await OsmObject.DownloadObjectAsync("way/" + member.ref) + const osmWay = ( + await this._objectDownloader.DownloadObjectAsync("way/" + member.ref) + ) const nodes = osmWay.nodes if (first) { return nodes[0] @@ -52,26 +57,30 @@ abstract class AbstractRelationSplitHandler extends OsmChangeAction { * When a way is split and this way is part of a relation, the relation should be updated too to have the new segment if relevant. */ export default class RelationSplitHandler extends AbstractRelationSplitHandler { - constructor(input: RelationSplitInput, theme: string) { - super(input, theme) + constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) { + super(input, theme, objectDownloader) } async CreateChangeDescriptions(changes: Changes): Promise { if (this._input.relation.tags["type"] === "restriction") { // This is a turn restriction - return new TurnRestrictionRSH(this._input, this._theme).CreateChangeDescriptions( - changes - ) + return new TurnRestrictionRSH( + this._input, + this._theme, + this._objectDownloader + ).CreateChangeDescriptions(changes) } - return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions( - changes - ) + return new InPlaceReplacedmentRTSH( + this._input, + this._theme, + this._objectDownloader + ).CreateChangeDescriptions(changes) } } export class TurnRestrictionRSH extends AbstractRelationSplitHandler { - constructor(input: RelationSplitInput, theme: string) { - super(input, theme) + constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) { + super(input, theme, objectDownloader) } public async CreateChangeDescriptions(changes: Changes): Promise { @@ -91,9 +100,11 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler { if (selfMember.role === "via") { // A via way can be replaced in place - return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions( - changes - ) + return new InPlaceReplacedmentRTSH( + this._input, + this._theme, + this._objectDownloader + ).CreateChangeDescriptions(changes) } // We have to keep only the way with a common point with the rest of the relation @@ -166,8 +177,8 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler { * Note that the feature might appear multiple times. */ export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { - constructor(input: RelationSplitInput, theme: string) { - super(input, theme) + constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) { + super(input, theme, objectDownloader) } async CreateChangeDescriptions(changes: Changes): Promise { diff --git a/Logic/Osm/Actions/SplitAction.ts b/Logic/Osm/Actions/SplitAction.ts index 6c8166220..7b15b57ee 100644 --- a/Logic/Osm/Actions/SplitAction.ts +++ b/Logic/Osm/Actions/SplitAction.ts @@ -5,6 +5,7 @@ import OsmChangeAction from "./OsmChangeAction" import { ChangeDescription } from "./ChangeDescription" import RelationSplitHandler from "./RelationSplitHandler" import { Feature, LineString } from "geojson" +import OsmObjectDownloader from "../OsmObjectDownloader" interface SplitInfo { originalIndex?: number // or negative for new elements @@ -61,7 +62,9 @@ export default class SplitAction extends OsmChangeAction { } async CreateChangeDescriptions(changes: Changes): Promise { - const originalElement = await OsmObject.DownloadObjectAsync(this.wayId) + const originalElement = ( + await new OsmObjectDownloader(changes.backend, changes).DownloadObjectAsync(this.wayId) + ) const originalNodes = originalElement.nodes // First, calculate the splitpoints and remove points close to one another @@ -172,7 +175,8 @@ export default class SplitAction extends OsmChangeAction { // At last, we still have to check that we aren't part of a relation... // At least, the order of the ways is identical, so we can keep the same roles - const relations = await OsmObject.DownloadReferencingRelations(this.wayId) + const downloader = new OsmObjectDownloader(changes.backend, changes) + const relations = await downloader.DownloadReferencingRelations(this.wayId) for (const relation of relations) { const changDescrs = await new RelationSplitHandler( { @@ -182,7 +186,8 @@ export default class SplitAction extends OsmChangeAction { allWaysNodesInOrder: allWaysNodesInOrder, originalWayId: originalElement.id, }, - this._meta.theme + this._meta.theme, + downloader ).CreateChangeDescriptions(changes) changeDescription.push(...changDescrs) } diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index b7580ad77..b6dc34d9a 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -12,6 +12,7 @@ import { GeoOperations } from "../GeoOperations" import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler" import { OsmConnection } from "./OsmConnection" import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" +import OsmObjectDownloader from "./OsmObjectDownloader" /** * Handles all changes made to OSM. @@ -23,10 +24,10 @@ export class Changes { public readonly allChanges = new UIEventSource(undefined) public readonly state: { allElements?: IndexedFeatureSource; osmConnection: OsmConnection } public readonly extraComment: UIEventSource = new UIEventSource(undefined) - + public readonly backend: string private readonly historicalUserLocations?: FeatureSource private _nextId: number = -1 // Newly assigned ID's are negative - private readonly isUploading = new UIEventSource(false) + public readonly isUploading = new UIEventSource(false) private readonly previouslyCreated: OsmObject[] = [] private readonly _leftRightSensitive: boolean private readonly _changesetHandler: ChangesetHandler @@ -47,6 +48,7 @@ export class Changes { // If a pending change contains a negative ID, we save that this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id) ?? [])) this.state = state + this.backend = state.osmConnection.Backend() this._changesetHandler = new ChangesetHandler( state.dryRun, state.osmConnection, @@ -149,274 +151,6 @@ export class Changes { this.allChanges.ping() } - private calculateDistanceToChanges( - change: OsmChangeAction, - changeDescriptions: ChangeDescription[] - ) { - const locations = this.historicalUserLocations?.features?.data - if (locations === undefined) { - // No state loaded or no locations -> we can't calculate... - return - } - if (!change.trackStatistics) { - // Probably irrelevant, such as a new helper node - return - } - - const now = new Date() - const recentLocationPoints = locations - .filter((feat) => feat.geometry.type === "Point") - .filter((feat) => { - const visitTime = new Date( - ((feat.properties)).date - ) - // In seconds - const diff = (now.getTime() - visitTime.getTime()) / 1000 - return diff < Constants.nearbyVisitTime - }) - if (recentLocationPoints.length === 0) { - // Probably no GPS enabled/no fix - return - } - - // The applicable points, contain information in their properties about location, time and GPS accuracy - // They are all GeoLocationPointProperties - // We walk every change and determine the closest distance possible - // Only if the change itself does _not_ contain any coordinates, we fall back and search the original feature in the state - - const changedObjectCoordinates: [number, number][] = [] - - { - const feature = this.state.allElements?.featuresById?.data.get(change.mainObjectId) - if (feature !== undefined) { - changedObjectCoordinates.push(GeoOperations.centerpointCoordinates(feature)) - } - } - - for (const changeDescription of changeDescriptions) { - const chng: - | { lat: number; lon: number } - | { coordinates: [number, number][] } - | { members } = changeDescription.changes - if (chng === undefined) { - continue - } - if (chng["lat"] !== undefined) { - changedObjectCoordinates.push([chng["lat"], chng["lon"]]) - } - if (chng["coordinates"] !== undefined) { - changedObjectCoordinates.push(...chng["coordinates"]) - } - } - - return Math.min( - ...changedObjectCoordinates.map((coor) => - Math.min( - ...recentLocationPoints.map((gpsPoint) => { - const otherCoor = GeoOperations.centerpointCoordinates(gpsPoint) - return GeoOperations.distanceBetween(coor, otherCoor) - }) - ) - ) - ) - } - - /** - * UPload the selected changes to OSM. - * Returns 'true' if successfull and if they can be removed - */ - private async flushSelectChanges( - pending: ChangeDescription[], - openChangeset: UIEventSource - ): Promise { - const self = this - const neededIds = Changes.GetNeededIds(pending) - - const osmObjects = Utils.NoNull( - await Promise.all( - neededIds.map(async (id) => - OsmObject.DownloadObjectAsync(id).catch((e) => { - console.error( - "Could not download OSM-object", - id, - " dropping it from the changes (" + e + ")" - ) - pending = pending.filter((ch) => ch.type + "/" + ch.id !== id) - return undefined - }) - ) - ) - ) - - if (this._leftRightSensitive) { - osmObjects.forEach((obj) => SimpleMetaTagger.removeBothTagging(obj.tags)) - } - - console.log("Got the fresh objects!", osmObjects, "pending: ", pending) - if (pending.length == 0) { - console.log("No pending changes...") - return true - } - - const perType = Array.from( - Utils.Hist( - pending - .filter( - (descr) => - descr.meta.changeType !== undefined && descr.meta.changeType !== null - ) - .map((descr) => descr.meta.changeType) - ), - ([key, count]) => ({ - key: key, - value: count, - 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, - })) - - const distances = Utils.NoNull(pending.map((descr) => descr.meta.distanceToObject)) - distances.sort((a, b) => a - b) - const perBinCount = Constants.distanceToChangeObjectBins.map((_) => 0) - - let j = 0 - const maxDistances = Constants.distanceToChangeObjectBins - for (let i = 0; i < maxDistances.length; i++) { - const maxDistance = maxDistances[i] - // distances is sorted in ascending order, so as soon as one is to big, all the resting elements will be bigger too - while (j < distances.length && distances[j] < maxDistance) { - perBinCount[i]++ - j++ - } - } - - const perBinMessage = Utils.NoNull( - perBinCount.map((count, i) => { - if (count === 0) { - return undefined - } - const maxD = maxDistances[i] - let key = `change_within_${maxD}m` - if (maxD === Number.MAX_VALUE) { - key = `change_over_${maxDistances[i - 1]}m` - } - return { - key, - value: count, - aggregate: true, - } - }) - ) - - // This method is only called with changedescriptions for this theme - const theme = pending[0].meta.theme - let comment = "Adding data with #MapComplete for theme #" + theme - if (this.extraComment.data !== undefined) { - comment += "\n\n" + this.extraComment.data - } - - const metatags: ChangesetTag[] = [ - { - key: "comment", - value: comment, - }, - { - key: "theme", - value: theme, - }, - ...perType, - ...motivations, - ...perBinMessage, - ] - - await this._changesetHandler.UploadChangeset( - (csId, remappings) => { - if (remappings.size > 0) { - console.log("Rewriting pending changes from", pending, "with", remappings) - pending = pending.map((ch) => ChangeDescriptionTools.rewriteIds(ch, remappings)) - console.log("Result is", pending) - } - const changes: { - newObjects: OsmObject[] - modifiedObjects: OsmObject[] - deletedObjects: OsmObject[] - } = self.CreateChangesetObjects(pending, osmObjects) - return Changes.createChangesetFor("" + csId, changes) - }, - metatags, - openChangeset - ) - - console.log("Upload successfull!") - return true - } - - private async flushChangesAsync(): Promise { - const self = this - try { - // At last, we build the changeset and upload - const pending = self.pendingChanges.data - - const pendingPerTheme = new Map() - for (const changeDescription of pending) { - const theme = changeDescription.meta.theme - if (!pendingPerTheme.has(theme)) { - pendingPerTheme.set(theme, []) - } - pendingPerTheme.get(theme).push(changeDescription) - } - - const successes = await Promise.all( - Array.from(pendingPerTheme, async ([theme, pendingChanges]) => { - try { - const openChangeset = this.state.osmConnection - .GetPreference("current-open-changeset-" + theme) - .sync( - (str) => { - const n = Number(str) - if (isNaN(n)) { - return undefined - } - return n - }, - [], - (n) => "" + n - ) - console.log( - "Using current-open-changeset-" + - theme + - " from the preferences, got " + - openChangeset.data - ) - - return await self.flushSelectChanges(pendingChanges, openChangeset) - } catch (e) { - console.error("Could not upload some changes:", e) - return false - } - }) - ) - - if (!successes.some((s) => s == false)) { - // All changes successfull, we clear the data! - this.pendingChanges.setData([]) - } - } catch (e) { - console.error( - "Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", - e - ) - self.pendingChanges.setData([]) - } finally { - self.isUploading.setData(false) - } - } - public CreateChangesetObjects( changes: ChangeDescription[], downloadedOsmObjects: OsmObject[] @@ -584,4 +318,285 @@ export class Changes { ) return result } + + private calculateDistanceToChanges( + change: OsmChangeAction, + changeDescriptions: ChangeDescription[] + ) { + const locations = this.historicalUserLocations?.features?.data + if (locations === undefined) { + // No state loaded or no locations -> we can't calculate... + return + } + if (!change.trackStatistics) { + // Probably irrelevant, such as a new helper node + return + } + + const now = new Date() + const recentLocationPoints = locations + .filter((feat) => feat.geometry.type === "Point") + .filter((feat) => { + const visitTime = new Date( + ((feat.properties)).date + ) + // In seconds + const diff = (now.getTime() - visitTime.getTime()) / 1000 + return diff < Constants.nearbyVisitTime + }) + if (recentLocationPoints.length === 0) { + // Probably no GPS enabled/no fix + return + } + + // The applicable points, contain information in their properties about location, time and GPS accuracy + // They are all GeoLocationPointProperties + // We walk every change and determine the closest distance possible + // Only if the change itself does _not_ contain any coordinates, we fall back and search the original feature in the state + + const changedObjectCoordinates: [number, number][] = [] + + { + const feature = this.state.allElements?.featuresById?.data.get(change.mainObjectId) + if (feature !== undefined) { + changedObjectCoordinates.push(GeoOperations.centerpointCoordinates(feature)) + } + } + + for (const changeDescription of changeDescriptions) { + const chng: + | { lat: number; lon: number } + | { coordinates: [number, number][] } + | { members } = changeDescription.changes + if (chng === undefined) { + continue + } + if (chng["lat"] !== undefined) { + changedObjectCoordinates.push([chng["lat"], chng["lon"]]) + } + if (chng["coordinates"] !== undefined) { + changedObjectCoordinates.push(...chng["coordinates"]) + } + } + + return Math.min( + ...changedObjectCoordinates.map((coor) => + Math.min( + ...recentLocationPoints.map((gpsPoint) => { + const otherCoor = GeoOperations.centerpointCoordinates(gpsPoint) + return GeoOperations.distanceBetween(coor, otherCoor) + }) + ) + ) + ) + } + + /** + * Upload the selected changes to OSM. + * Returns 'true' if successful and if they can be removed + */ + private async flushSelectChanges( + pending: ChangeDescription[], + openChangeset: UIEventSource + ): Promise { + const self = this + 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(async (id) => { + try { + const osmObj = await downloader.DownloadObjectAsync(id) + return { id, osmObj } + } catch (e) { + console.error( + "Could not download OSM-object", + id, + " dropping it from the changes (" + e + ")" + ) + return undefined + } + }) + ) + + osmObjects = Utils.NoNull(osmObjects) + + for (const { osmObj, id } of osmObjects) { + if (osmObj === "deleted") { + pending = pending.filter((ch) => ch.type + "/" + ch.id !== id) + } + } + + const objects = osmObjects + .filter((obj) => obj.osmObj !== "deleted") + .map((obj) => obj.osmObj) + + if (this._leftRightSensitive) { + objects.forEach((obj) => SimpleMetaTagger.removeBothTagging(obj.tags)) + } + + console.log("Got the fresh objects!", objects, "pending: ", pending) + if (pending.length == 0) { + console.log("No pending changes...") + return true + } + + const perType = Array.from( + Utils.Hist( + pending + .filter( + (descr) => + descr.meta.changeType !== undefined && descr.meta.changeType !== null + ) + .map((descr) => descr.meta.changeType) + ), + ([key, count]) => ({ + key: key, + value: count, + 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, + })) + + const distances = Utils.NoNull(pending.map((descr) => descr.meta.distanceToObject)) + distances.sort((a, b) => a - b) + const perBinCount = Constants.distanceToChangeObjectBins.map((_) => 0) + + let j = 0 + const maxDistances = Constants.distanceToChangeObjectBins + for (let i = 0; i < maxDistances.length; i++) { + const maxDistance = maxDistances[i] + // distances is sorted in ascending order, so as soon as one is to big, all the resting elements will be bigger too + while (j < distances.length && distances[j] < maxDistance) { + perBinCount[i]++ + j++ + } + } + + const perBinMessage = Utils.NoNull( + perBinCount.map((count, i) => { + if (count === 0) { + return undefined + } + const maxD = maxDistances[i] + let key = `change_within_${maxD}m` + if (maxD === Number.MAX_VALUE) { + key = `change_over_${maxDistances[i - 1]}m` + } + return { + key, + value: count, + aggregate: true, + } + }) + ) + + // This method is only called with changedescriptions for this theme + const theme = pending[0].meta.theme + let comment = "Adding data with #MapComplete for theme #" + theme + if (this.extraComment.data !== undefined) { + comment += "\n\n" + this.extraComment.data + } + + const metatags: ChangesetTag[] = [ + { + key: "comment", + value: comment, + }, + { + key: "theme", + value: theme, + }, + ...perType, + ...motivations, + ...perBinMessage, + ] + + await this._changesetHandler.UploadChangeset( + (csId, remappings) => { + if (remappings.size > 0) { + console.log("Rewriting pending changes from", pending, "with", remappings) + pending = pending.map((ch) => ChangeDescriptionTools.rewriteIds(ch, remappings)) + console.log("Result is", pending) + } + const changes: { + newObjects: OsmObject[] + modifiedObjects: OsmObject[] + deletedObjects: OsmObject[] + } = self.CreateChangesetObjects(pending, objects) + return Changes.createChangesetFor("" + csId, changes) + }, + metatags, + openChangeset + ) + + console.log("Upload successfull!") + return true + } + + private async flushChangesAsync(): Promise { + const self = this + try { + // At last, we build the changeset and upload + const pending = self.pendingChanges.data + + const pendingPerTheme = new Map() + for (const changeDescription of pending) { + const theme = changeDescription.meta.theme + if (!pendingPerTheme.has(theme)) { + pendingPerTheme.set(theme, []) + } + pendingPerTheme.get(theme).push(changeDescription) + } + + const successes = await Promise.all( + Array.from(pendingPerTheme, async ([theme, pendingChanges]) => { + try { + const openChangeset = this.state.osmConnection + .GetPreference("current-open-changeset-" + theme) + .sync( + (str) => { + const n = Number(str) + if (isNaN(n)) { + return undefined + } + return n + }, + [], + (n) => "" + n + ) + console.log( + "Using current-open-changeset-" + + theme + + " from the preferences, got " + + openChangeset.data + ) + + return await self.flushSelectChanges(pendingChanges, openChangeset) + } catch (e) { + console.error("Could not upload some changes:", e) + return false + } + }) + ) + + if (!successes.some((s) => s == false)) { + // All changes successfull, we clear the data! + this.pendingChanges.setData([]) + } + } catch (e) { + console.error( + "Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", + e + ) + self.pendingChanges.setData([]) + } finally { + self.isUploading.setData(false) + } + } } diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index 745daa645..ee9f3510c 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -82,7 +82,6 @@ export class OsmConnection { OsmConnection.oauth_configs[options.osmConfiguration ?? "osm"] ?? OsmConnection.oauth_configs.osm console.debug("Using backend", this._oauth_config.url) - OsmObject.SetBackendUrl(this._oauth_config.url + "/") this._iframeMode = Utils.runningFromConsole ? false : window !== window.top this.userDetails = new UIEventSource( diff --git a/Logic/Osm/OsmObject.ts b/Logic/Osm/OsmObject.ts index 11996a92c..3367db4e8 100644 --- a/Logic/Osm/OsmObject.ts +++ b/Logic/Osm/OsmObject.ts @@ -1,17 +1,13 @@ import { Utils } from "../../Utils" import polygon_features from "../../assets/polygon-features.json" -import { Store, UIEventSource } from "../UIEventSource" -import { BBox } from "../BBox" import OsmToGeoJson from "osmtogeojson" -import { NodeId, OsmFeature, OsmId, OsmTags, RelationId, WayId } from "../../Models/OsmFeature" +import { OsmFeature, OsmId, OsmTags, WayId } from "../../Models/OsmFeature" import { Feature, LineString, Polygon } from "geojson" export abstract class OsmObject { private static defaultBackend = "https://www.openstreetmap.org/" protected static backendURL = OsmObject.defaultBackend private static polygonFeatures = OsmObject.constructPolygonFeatures() - private static objectCache = new Map>() - private static historyCache = new Map>() type: "node" | "way" | "relation" id: number /** @@ -31,190 +27,6 @@ export abstract class OsmObject { } } - public static SetBackendUrl(url: string) { - if (!url.endsWith("/")) { - throw "Backend URL must end with a '/'" - } - if (!url.startsWith("http")) { - throw "Backend URL must begin with http" - } - this.backendURL = url - } - - public static DownloadObject(id: NodeId, forceRefresh?: boolean): Store - public static DownloadObject(id: RelationId, forceRefresh?: boolean): Store - public static DownloadObject(id: WayId, forceRefresh?: boolean): Store - public static DownloadObject(id: string, forceRefresh: boolean = false): Store { - let src: UIEventSource - if (OsmObject.objectCache.has(id)) { - src = OsmObject.objectCache.get(id) - if (forceRefresh) { - src.setData(undefined) - } else { - return src - } - } else { - src = UIEventSource.FromPromise(OsmObject.DownloadObjectAsync(id)) - } - - OsmObject.objectCache.set(id, src) - return src - } - - static async DownloadPropertiesOf(id: string): Promise { - const splitted = id.split("/") - const idN = Number(splitted[1]) - if (idN < 0) { - return undefined - } - - const url = `${OsmObject.backendURL}api/0.6/${id}` - const rawData = await Utils.downloadJsonCachedAdvanced(url, 1000) - if (rawData["error"] !== undefined && rawData["statuscode"] === 410) { - return "deleted" - } - // Tags is undefined if the element does not have any tags - return rawData["content"].elements[0].tags ?? {} - } - - static async DownloadObjectAsync( - id: NodeId, - maxCacheAgeInSecs?: number - ): Promise - static async DownloadObjectAsync( - id: WayId, - maxCacheAgeInSecs?: number - ): Promise - static async DownloadObjectAsync( - id: RelationId, - maxCacheAgeInSecs?: number - ): Promise - static async DownloadObjectAsync( - id: OsmId, - maxCacheAgeInSecs?: number - ): Promise - static async DownloadObjectAsync( - id: string, - maxCacheAgeInSecs?: number - ): Promise - static async DownloadObjectAsync( - id: string, - maxCacheAgeInSecs?: number - ): Promise { - const splitted = id.split("/") - const type = splitted[0] - const idN = Number(splitted[1]) - if (idN < 0) { - return undefined - } - - const full = !id.startsWith("node") ? "/full" : "" - const url = `${OsmObject.backendURL}api/0.6/${id}${full}` - const rawData = await Utils.downloadJsonCached(url, (maxCacheAgeInSecs ?? 10) * 1000) - if (rawData === undefined) { - return undefined - } - // A full query might contain more then just the requested object (e.g. nodes that are part of a way, where we only want the way) - const parsed = OsmObject.ParseObjects(rawData.elements) - // Lets fetch the object we need - for (const osmObject of parsed) { - if (osmObject.type !== type) { - continue - } - if (osmObject.id !== idN) { - continue - } - // Found the one! - return osmObject - } - throw "PANIC: requested object is not part of the response" - } - - /** - * Downloads the ways that are using this node. - * Beware: their geometry will be incomplete! - */ - public static DownloadReferencingWays(id: string): Promise { - return Utils.downloadJsonCached( - `${OsmObject.backendURL}api/0.6/${id}/ways`, - 60 * 1000 - ).then((data) => { - return data.elements.map((wayInfo) => { - const way = new OsmWay(wayInfo.id) - way.LoadData(wayInfo) - return way - }) - }) - } - - /** - * Downloads the relations that are using this feature. - * Beware: their geometry will be incomplete! - */ - public static async DownloadReferencingRelations(id: string): Promise { - const data = await Utils.downloadJsonCached( - `${OsmObject.backendURL}api/0.6/${id}/relations`, - 60 * 1000 - ) - return data.elements.map((wayInfo) => { - const rel = new OsmRelation(wayInfo.id) - rel.LoadData(wayInfo) - rel.SaveExtraData(wayInfo, undefined) - return rel - }) - } - - public static DownloadHistory(id: NodeId): UIEventSource - public static DownloadHistory(id: WayId): UIEventSource - public static DownloadHistory(id: RelationId): UIEventSource - - public static DownloadHistory(id: OsmId): UIEventSource - public static DownloadHistory(id: string): UIEventSource { - if (OsmObject.historyCache.has(id)) { - return OsmObject.historyCache.get(id) - } - const splitted = id.split("/") - const type = splitted[0] - const idN = Number(splitted[1]) - const src = new UIEventSource([]) - OsmObject.historyCache.set(id, src) - Utils.downloadJsonCached( - `${OsmObject.backendURL}api/0.6/${type}/${idN}/history`, - 10 * 60 * 1000 - ).then((data) => { - const elements: any[] = data.elements - const osmObjects: OsmObject[] = [] - for (const element of elements) { - let osmObject: OsmObject = null - element.nodes = [] - switch (type) { - case "node": - osmObject = new OsmNode(idN) - break - case "way": - osmObject = new OsmWay(idN) - break - case "relation": - osmObject = new OsmRelation(idN) - break - } - osmObject?.LoadData(element) - osmObject?.SaveExtraData(element, []) - osmObjects.push(osmObject) - } - src.setData(osmObjects) - }) - return src - } - - // bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds) - public static async LoadArea(bbox: BBox): Promise { - const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` - const data = await Utils.downloadJson(url) - const elements: any[] = data.elements - return OsmObject.ParseObjects(elements) - } - public static ParseObjects(elements: any[]): OsmObject[] { const objects: OsmObject[] = [] const allNodes: Map = new Map() @@ -357,12 +169,16 @@ export abstract class OsmObject { return 'version="' + this.version + '"' } - private LoadData(element: any): void { - this.tags = element.tags ?? this.tags - this.version = element.version - this.timestamp = element.timestamp + protected LoadData(element: any): void { + if (element === undefined) { + return + } + this.tags = element?.tags ?? this.tags const tgs = this.tags - if (element.tags === undefined) { + tgs["id"] = (this.type + "/" + this.id) + this.version = element?.version + this.timestamp = element?.timestamp + if (element?.tags === undefined) { // Simple node which is part of a way - not important return } @@ -371,7 +187,6 @@ export abstract class OsmObject { tgs["_last_edit:changeset"] = element.changeset tgs["_last_edit:timestamp"] = element.timestamp tgs["_version_number"] = element.version - tgs["id"] = (this.type + "/" + this.id) } } @@ -379,8 +194,9 @@ export class OsmNode extends OsmObject { lat: number lon: number - constructor(id: number) { + constructor(id: number, extraData?) { super("node", id) + this.LoadData(extraData) } ChangesetXML(changesetId: string): string { @@ -431,8 +247,9 @@ export class OsmWay extends OsmObject { lat: number lon: number - constructor(id: number) { + constructor(id: number, wayInfo?) { super("way", id) + this.LoadData(wayInfo) } centerpoint(): [number, number] { @@ -535,8 +352,9 @@ export class OsmRelation extends OsmObject { private geojson = undefined - constructor(id: number) { + constructor(id: number, extraInfo?: any) { super("relation", id) + this.LoadData(extraInfo) } centerpoint(): [number, number] { diff --git a/Logic/Osm/OsmObjectDownloader.ts b/Logic/Osm/OsmObjectDownloader.ts new file mode 100644 index 000000000..3ae7d214c --- /dev/null +++ b/Logic/Osm/OsmObjectDownloader.ts @@ -0,0 +1,152 @@ +import { Utils } from "../../Utils" +import { OsmNode, OsmObject, OsmRelation, OsmWay } from "./OsmObject" +import { NodeId, OsmId, RelationId, WayId } from "../../Models/OsmFeature" +import { Store, UIEventSource } from "../UIEventSource" +import { ChangeDescription } from "./Actions/ChangeDescription" + +/** + * The OSM-Object downloader downloads the latest version of the object, but applies 'pendingchanges' to them, + * so that we always have a consistent view + */ +export default class OsmObjectDownloader { + private readonly _changes?: { + readonly pendingChanges: UIEventSource + readonly isUploading: Store + } + private readonly backend: string + private historyCache = new Map>() + + constructor( + backend: string = "https://openstreetmap.org", + changes?: { + readonly pendingChanges: UIEventSource + readonly isUploading: Store + } + ) { + this._changes = changes + if (!backend.endsWith("/")) { + backend += "/" + } + if (!backend.startsWith("http")) { + throw "Backend URL must begin with http" + } + this.backend = backend + } + + async DownloadObjectAsync(id: NodeId, maxCacheAgeInSecs?: number): Promise + async DownloadObjectAsync(id: WayId, maxCacheAgeInSecs?: number): Promise + async DownloadObjectAsync( + id: RelationId, + maxCacheAgeInSecs?: number + ): Promise + async DownloadObjectAsync(id: OsmId, maxCacheAgeInSecs?: number): Promise + async DownloadObjectAsync( + id: string, + maxCacheAgeInSecs?: number + ): Promise + async DownloadObjectAsync( + id: string, + maxCacheAgeInSecs?: number + ): Promise { + const splitted = id.split("/") + const type = splitted[0] + const idN = Number(splitted[1]) + if (idN < 0) { + throw "Invalid request: cannot download OsmObject " + id + ", it has a negative id" + } + + const full = !id.startsWith("node") ? "/full" : "" + const url = `${this.backend}api/0.6/${id}${full}` + const rawData = await Utils.downloadJsonCachedAdvanced( + url, + (maxCacheAgeInSecs ?? 10) * 1000 + ) + if (rawData["error"] !== undefined && rawData["statuscode"] === 410) { + return "deleted" + } + // A full query might contain more then just the requested object (e.g. nodes that are part of a way, where we only want the way) + const parsed = OsmObject.ParseObjects(rawData["content"].elements) + // Lets fetch the object we need + for (const osmObject of parsed) { + if (osmObject.type !== type) { + continue + } + if (osmObject.id !== idN) { + continue + } + // Found the one! + return osmObject + } + throw "PANIC: requested object is not part of the response" + } + + public DownloadHistory(id: NodeId): UIEventSource + + public DownloadHistory(id: WayId): UIEventSource + + public DownloadHistory(id: RelationId): UIEventSource + + public DownloadHistory(id: OsmId): UIEventSource + + public DownloadHistory(id: string): UIEventSource { + if (this.historyCache.has(id)) { + return this.historyCache.get(id) + } + const splitted = id.split("/") + const type = splitted[0] + const idN = Number(splitted[1]) + const src = new UIEventSource([]) + this.historyCache.set(id, src) + Utils.downloadJsonCached( + `${this.backend}api/0.6/${type}/${idN}/history`, + 10 * 60 * 1000 + ).then((data) => { + const elements: any[] = data.elements + const osmObjects: OsmObject[] = [] + for (const element of elements) { + let osmObject: OsmObject = null + element.nodes = [] + switch (type) { + case "node": + osmObject = new OsmNode(idN, element) + break + case "way": + osmObject = new OsmWay(idN, element) + break + case "relation": + osmObject = new OsmRelation(idN, element) + break + } + osmObject?.SaveExtraData(element, []) + osmObjects.push(osmObject) + } + src.setData(osmObjects) + }) + return src + } + + /** + * Downloads the ways that are using this node. + * Beware: their geometry will be incomplete! + */ + public async DownloadReferencingWays(id: string): Promise { + const data = await Utils.downloadJsonCached(`${this.backend}api/0.6/${id}/ways`, 60 * 1000) + return data.elements.map((wayInfo) => new OsmWay(wayInfo.id, wayInfo)) + } + + /** + * Downloads the relations that are using this feature. + * Beware: their geometry will be incomplete! + */ + public async DownloadReferencingRelations(id: string): Promise { + const data = await Utils.downloadJsonCached( + `${this.backend}api/0.6/${id}/relations`, + 60 * 1000 + ) + return data.elements.map((wayInfo) => { + const rel = new OsmRelation(wayInfo.id, wayInfo) + rel.SaveExtraData(wayInfo, undefined) + return rel + }) + } +} diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index 34e36d7cb..a71201e47 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -14,12 +14,14 @@ import { OsmObject } from "./Osm/OsmObject" import { OsmTags } from "../Models/OsmFeature" import { UIEventSource } from "./UIEventSource" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +import OsmObjectDownloader from "./Osm/OsmObjectDownloader" /** * All elements that are needed to perform metatagging */ export interface MetataggingState { layout: LayoutConfig + osmObjectDownloader: OsmObjectDownloader } export abstract class SimpleMetaTagger { @@ -97,7 +99,7 @@ export class ReferencingWaysMetaTagger extends SimpleMetaTagger { } Utils.AddLazyPropertyAsync(feature.properties, "_referencing_ways", async () => { - const referencingWays = await OsmObject.DownloadReferencingWays(id) + const referencingWays = await state.osmObjectDownloader.DownloadReferencingWays(id) const wayIds = referencingWays.map((w) => "way/" + w.id) wayIds.sort() return wayIds.join(";") diff --git a/Models/ThemeViewState.ts b/Models/ThemeViewState.ts index 5850f13ec..3ac0d7a2c 100644 --- a/Models/ThemeViewState.ts +++ b/Models/ThemeViewState.ts @@ -42,6 +42,7 @@ import { MenuState } from "./MenuState" import MetaTagging from "../Logic/MetaTagging" import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator" import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource" +import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"; /** * @@ -64,6 +65,7 @@ export default class ThemeViewState implements SpecialVisualizationState { readonly osmConnection: OsmConnection readonly selectedElement: UIEventSource readonly mapProperties: MapProperties & ExportableMap + readonly osmObjectDownloader: OsmObjectDownloader readonly dataIsLoading: Store readonly guistate: MenuState @@ -213,6 +215,8 @@ export default class ThemeViewState implements SpecialVisualizationState { this.layout )) + this.osmObjectDownloader = new OsmObjectDownloader(this.osmConnection.Backend(), this.changes) + this.initActors() this.drawSpecialLayers(lastClick) this.initHotkeys() diff --git a/UI/BigComponents/ActionButtons.ts b/UI/BigComponents/ActionButtons.ts index 8cd9fbccd..e001ae7ae 100644 --- a/UI/BigComponents/ActionButtons.ts +++ b/UI/BigComponents/ActionButtons.ts @@ -1,9 +1,5 @@ import Combine from "../Base/Combine" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import { Store } from "../../Logic/UIEventSource" -import { BBox } from "../../Logic/BBox" -import Loc from "../../Models/Loc" -import { OsmConnection } from "../../Logic/Osm/OsmConnection" import Translations from "../i18n/Translations" import { SubtleButton } from "../Base/SubtleButton" import Svg from "../../Svg" @@ -11,7 +7,7 @@ import { Utils } from "../../Utils" import { MapillaryLink } from "./MapillaryLink" import { OpenIdEditor, OpenJosm } from "./CopyrightPanel" import Toggle from "../Input/Toggle" -import { DefaultGuiState } from "../DefaultGuiState" +import { SpecialVisualizationState } from "../SpecialVisualization" export class BackToThemeOverview extends Toggle { constructor( @@ -35,14 +31,7 @@ export class BackToThemeOverview extends Toggle { } export class ActionButtons extends Combine { - constructor(state: { - readonly layoutToUse: LayoutConfig - readonly currentBounds: Store - readonly locationControl: Store - readonly osmConnection: OsmConnection - readonly featureSwitchMoreQuests: Store - readonly defaultGuiState: DefaultGuiState - }) { + constructor(state:SpecialVisualizationState) { const imgSize = "h-6 w-6" const iconStyle = "height: 1.5rem; width: 1.5rem" const t = Translations.t.general.attribution @@ -76,7 +65,7 @@ export class ActionButtons extends Combine { }), new OpenIdEditor(state, iconStyle), new MapillaryLink(state, iconStyle), - new OpenJosm(state, iconStyle).SetClass("hidden-on-mobile"), + new OpenJosm(state.osmConnection,state.mapProperties.bounds, iconStyle).SetClass("hidden-on-mobile"), ]) this.SetClass("block w-full link-no-underline") } diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts deleted file mode 100644 index 6c25cd00e..000000000 --- a/UI/Input/LocationInput.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { ReadonlyInputElement } from "./InputElement" -import Loc from "../../Models/Loc" -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import Combine from "../Base/Combine" -import Svg from "../../Svg" -import { GeoOperations } from "../../Logic/GeoOperations" -import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import { BBox } from "../../Logic/BBox" -import { FixedUiElement } from "../Base/FixedUiElement" -import BaseUIElement from "../BaseUIElement" -import matchpoint from "../../assets/layers/matchpoint/matchpoint.json" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import FilteredLayer from "../../Models/FilteredLayer" -import { RelationId, WayId } from "../../Models/OsmFeature" -import { Feature, LineString, Polygon } from "geojson" -import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject" - -export default class LocationInput - extends BaseUIElement - implements ReadonlyInputElement, MinimapObj -{ - private static readonly matchLayer = new LayerConfig( - matchpoint, - "LocationInput.matchpoint", - true - ) - - public readonly snappedOnto: UIEventSource = - new UIEventSource(undefined) - public readonly _matching_layer: LayerConfig - public readonly leafletMap: UIEventSource - public readonly bounds - public readonly location - private readonly _centerLocation: UIEventSource - private readonly mapBackground: UIEventSource - /** - * The features to which the input should be snapped - * @private - */ - private readonly _snapTo: Store< - (Feature & { properties: { id: WayId } })[] - > - /** - * The features to which the input should be snapped without cleanup of relations and memberships - * Used for rendering - * @private - */ - private readonly _snapToRaw: Store - private readonly _value: Store - private readonly _snappedPoint: Store - private readonly _maxSnapDistance: number - private readonly _snappedPointTags: any - private readonly _bounds: UIEventSource - private readonly map: BaseUIElement & MinimapObj - private readonly clickLocation: UIEventSource - private readonly _minZoom: number - private readonly _state: { - readonly filteredLayers: Store - readonly backgroundLayer: UIEventSource - readonly layoutToUse: LayoutConfig - readonly selectedElement: UIEventSource - readonly allElements: ElementStorage - } - - /** - * Given a list of geojson-features, will prepare these features to be snappable: - * - points are removed - * - LineStrings are passed as-is - * - Multipolygons are decomposed into their member ways by downloading them - * - * @private - */ - private static async prepareSnapOnto( - features: Feature[] - ): Promise<(Feature & { properties: { id: WayId } })[]> { - const linesAndPolygon: Feature[] = ( - features.filter((f) => f.geometry.type !== "Point") - ) - // Clean the features: multipolygons are split into their it's members - const linestrings: (Feature & { properties: { id: WayId } })[] = [] - for (const feature of linesAndPolygon) { - if (feature.properties.id.startsWith("way")) { - // A normal way - we continue - linestrings.push(feature) - continue - } - - // We have a multipolygon, thus: a relation - // Download the members - const relation = await OsmObject.DownloadObjectAsync( - feature.properties.id, - 60 * 60 - ) - const members: OsmWay[] = await Promise.all( - relation.members - .filter((m) => m.type === "way") - .map((m) => OsmObject.DownloadObjectAsync(("way/" + m.ref), 60 * 60)) - ) - linestrings.push(...members.map((m) => m.asGeoJson())) - } - return linestrings - } - - constructor(options?: { - minZoom?: number - mapBackground?: UIEventSource - snapTo?: UIEventSource - renderLayerForSnappedPoint?: LayerConfig - maxSnapDistance?: number - snappedPointTags?: any - requiresSnapping?: boolean - centerLocation?: UIEventSource - bounds?: UIEventSource - state?: { - readonly filteredLayers: Store - readonly backgroundLayer: UIEventSource - readonly layoutToUse: LayoutConfig - readonly selectedElement: UIEventSource - readonly allElements: ElementStorage - } - }) { - super() - this._snapToRaw = options?.snapTo?.map((feats) => - feats.filter((f) => f.feature.geometry.type !== "Point") - ) - this._snapTo = options?.snapTo - ?.bind((features) => - UIEventSource.FromPromise( - LocationInput.prepareSnapOnto(features.map((f) => f.feature)) - ) - ) - ?.map((f) => f ?? []) - this._maxSnapDistance = options?.maxSnapDistance - this._centerLocation = - options?.centerLocation ?? - new UIEventSource({ - lat: 0, - lon: 0, - zoom: 0, - }) - this._snappedPointTags = options?.snappedPointTags - this._bounds = options?.bounds - this._minZoom = options?.minZoom - this._state = options?.state - const self = this - if (this._snapTo === undefined) { - this._value = this._centerLocation - } else { - this._matching_layer = options?.renderLayerForSnappedPoint ?? LocationInput.matchLayer - - // Calculate the location of the point based by snapping it onto a way - // As a side-effect, the actual snapped-onto way (if any) is saved into 'snappedOnto' - this._snappedPoint = this._centerLocation.map( - (loc) => { - if (loc === undefined) { - return undefined - } - - // We reproject the location onto every 'snap-to-feature' and select the closest - - let min = undefined - let matchedWay: Feature & { properties: { id: WayId } } = - undefined - for (const feature of self._snapTo.data ?? []) { - try { - const nearestPointOnLine = GeoOperations.nearestPoint(feature, [ - loc.lon, - loc.lat, - ]) - if (min === undefined) { - min = { ...nearestPointOnLine } - matchedWay = feature - continue - } - - if (min.properties.dist > nearestPointOnLine.properties.dist) { - min = { ...nearestPointOnLine } - matchedWay = feature - } - } catch (e) { - console.log( - "Snapping to a nearest point failed for ", - feature, - "due to ", - e - ) - } - } - - if (min === undefined || min.properties.dist * 1000 > self._maxSnapDistance) { - if (options?.requiresSnapping) { - return undefined - } else { - // No match found - the original coordinates are returned as is - return { - type: "Feature", - properties: options?.snappedPointTags ?? min.properties, - geometry: { type: "Point", coordinates: [loc.lon, loc.lat] }, - } - } - } - min.properties = options?.snappedPointTags ?? min.properties - min.properties = { - ...min.properties, - _referencing_ways: JSON.stringify([matchedWay.properties.id]), - } - self.snappedOnto.setData(matchedWay) - return min - }, - [this._snapTo] - ) - - this._value = this._snappedPoint.map((f) => { - const [lon, lat] = f.geometry.coordinates - return { - lon: lon, - lat: lat, - zoom: undefined, - } - }) - } - this.mapBackground = - options?.mapBackground ?? - this._state?.backgroundLayer ?? - new UIEventSource(AvailableBaseLayers.osmCarto) - this.SetClass("block h-full") - - this.clickLocation = new UIEventSource(undefined) - this.map = Minimap.createMiniMap({ - location: this._centerLocation, - background: this.mapBackground, - attribution: this.mapBackground !== this._state?.backgroundLayer, - lastClickLocation: this.clickLocation, - bounds: this._bounds, - addLayerControl: true, - }) - this.leafletMap = this.map.leafletMap - this.location = this.map.location - } - - GetValue(): Store { - return this._value - } - - IsValid(t: Loc): boolean { - return t !== undefined - } - - installBounds(factor: number | BBox, showRange?: boolean): void { - this.map.installBounds(factor, showRange) - } - - protected InnerConstructElement(): HTMLElement { - try { - const self = this - const hasMoved = new UIEventSource(false) - const startLocation = { ...this._centerLocation.data } - this._centerLocation.addCallbackD((newLocation) => { - const f = 100000 - const diff = - Math.abs(newLocation.lon * f - startLocation.lon * f) + - Math.abs(newLocation.lat * f - startLocation.lat * f) - if (diff < 1) { - return - } - hasMoved.setData(true) - return true - }) - this.clickLocation.addCallbackAndRunD((location) => - this._centerLocation.setData(location) - ) - if (this._snapToRaw !== undefined) { - // Show the lines to snap to - new ShowDataMultiLayer({ - features: new StaticFeatureSource(this._snapToRaw), - zoomToFeatures: false, - leafletMap: this.map.leafletMap, - layers: this._state.filteredLayers, - }) - // Show the central point - const matchPoint = this._snappedPoint.map((loc) => { - if (loc === undefined) { - return [] - } - return [loc] - }) - - // The 'matchlayer' is the layer which shows the target location - new ShowDataLayer({ - features: new StaticFeatureSource(matchPoint), - zoomToFeatures: false, - leafletMap: this.map.leafletMap, - layerToShow: this._matching_layer, - state: this._state, - selectedElement: this._state.selectedElement, - }) - } - this.mapBackground.map( - (layer) => { - const leaflet = this.map.leafletMap.data - if (leaflet === undefined || layer === undefined) { - return - } - - leaflet.setMaxZoom(layer.max_zoom) - leaflet.setMinZoom(self._minZoom ?? layer.max_zoom - 2) - leaflet.setZoom(layer.max_zoom - 1) - }, - [this.map.leafletMap] - ) - - return new Combine([ - new Combine([ - Svg.move_arrows_ui() - .SetClass("block relative pointer-events-none") - .SetStyle("left: -2.5rem; top: -2.5rem; width: 5rem; height: 5rem"), - ]) - .SetClass("block w-0 h-0 z-10 relative") - .SetStyle( - "background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%; opacity: 0.5" - ), - - this.map.SetClass("z-0 relative block w-full h-full bg-gray-100"), - ]).ConstructElement() - } catch (e) { - console.error("Could not generate LocationInputElement:", e) - return new FixedUiElement("Constructing a locationInput failed due to" + e) - .SetClass("alert") - .ConstructElement() - } - } -} diff --git a/UI/Popup/AddNewPoint/AddNewPoint.svelte b/UI/Popup/AddNewPoint/AddNewPoint.svelte index fef8dcf20..9b60da791 100644 --- a/UI/Popup/AddNewPoint/AddNewPoint.svelte +++ b/UI/Popup/AddNewPoint/AddNewPoint.svelte @@ -21,10 +21,9 @@ import LoginButton from "../../Base/LoginButton.svelte"; import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"; import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"; - import { OsmObject } from "../../../Logic/Osm/OsmObject"; + import { OsmWay } from "../../../Logic/Osm/OsmObject"; import { Tag } from "../../../Logic/Tags/Tag"; import type { WayId } from "../../../Models/OsmFeature"; - import { TagUtils } from "../../../Logic/Tags/TagUtils"; import Loading from "../../Base/Loading.svelte"; export let coordinate: { lon: number, lat: number }; @@ -75,12 +74,19 @@ const tags: Tag[] = selectedPreset.preset.tags; console.log("Creating new point at", location, "snapped to", snapTo, "with tags", tags); - const snapToWay = snapTo === undefined ? undefined : await OsmObject.DownloadObjectAsync(snapTo, 0); + let snapToWay: undefined | OsmWay = undefined + if(snapTo !== undefined){ + const downloaded = await state.osmObjectDownloader.DownloadObjectAsync(snapTo, 0); + if(downloaded !== "deleted"){ + snapToWay = downloaded + } + } - const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, { + const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, + { theme: state.layout?.id ?? "unkown", changeType: "create", - snapOnto: snapToWay + snapOnto: snapToWay }); await state.changes.applyAction(newElementAction); // The 'changes' should have created a new point, which added this into the 'featureProperties' diff --git a/UI/Popup/DeleteWizard.ts b/UI/Popup/DeleteWizard.ts index 2cd325c6f..3ef4bb639 100644 --- a/UI/Popup/DeleteWizard.ts +++ b/UI/Popup/DeleteWizard.ts @@ -11,7 +11,6 @@ import { Translation } from "../i18n/Translation" import BaseUIElement from "../BaseUIElement" import Constants from "../../Models/Constants" import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig" -import { OsmObject } from "../../Logic/Osm/OsmObject" import { OsmConnection } from "../../Logic/Osm/OsmConnection" import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction" import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" @@ -20,12 +19,12 @@ import { RadioButton } from "../Input/RadioButton" import { FixedInputElement } from "../Input/FixedInputElement" import Title from "../Base/Title" import { SubstitutedTranslation } from "../SubstitutedTranslation" -import TagRenderingQuestion from "./TagRenderingQuestion" import { OsmId, OsmTags } from "../../Models/OsmFeature" import { LoginToggle } from "./LoginButton" import { SpecialVisualizationState } from "../SpecialVisualization" -import SvelteUIElement from "../Base/SvelteUIElement"; -import TagHint from "./TagHint.svelte"; +import SvelteUIElement from "../Base/SvelteUIElement" +import TagHint from "./TagHint.svelte" +import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader" export default class DeleteWizard extends Toggle { /** @@ -53,6 +52,7 @@ export default class DeleteWizard extends Toggle { const deleteAbility = new DeleteabilityChecker( id, state.osmConnection, + state.osmObjectDownloader, options.neededChangesets ) @@ -227,7 +227,10 @@ export default class DeleteWizard extends Toggle { // This is a retagging, not a deletion of any kind return new Combine([ t.explanations.retagNoOtherThemes, - new SvelteUIElement(TagHint, {osmConnection: state.osmConnection, tags: retag}) + new SvelteUIElement(TagHint, { + osmConnection: state.osmConnection, + tags: retag, + }), ]) } @@ -285,11 +288,18 @@ export default class DeleteWizard extends Toggle { class DeleteabilityChecker { public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean; reason: Translation }> + private readonly objectDownloader: OsmObjectDownloader private readonly _id: OsmId private readonly _allowDeletionAtChangesetCount: number private readonly _osmConnection: OsmConnection - constructor(id: OsmId, osmConnection: OsmConnection, allowDeletionAtChangesetCount?: number) { + constructor( + id: OsmId, + osmConnection: OsmConnection, + objectDownloader: OsmObjectDownloader, + allowDeletionAtChangesetCount?: number + ) { + this.objectDownloader = objectDownloader this._id = id this._osmConnection = osmConnection this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE @@ -366,11 +376,13 @@ class DeleteabilityChecker { if (allByMyself.data === null && useTheInternet) { // We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above - const hist = OsmObject.DownloadHistory(id).map((versions) => - versions.map((version) => - Number(version.tags["_last_edit:contributor:uid"]) + const hist = this.objectDownloader + .DownloadHistory(id) + .map((versions) => + versions.map((version) => + Number(version.tags["_last_edit:contributor:uid"]) + ) ) - ) hist.addCallbackAndRunD((hist) => previousEditors.setData(hist)) } @@ -406,11 +418,11 @@ class DeleteabilityChecker { } // All right! We have arrived at a point that we should query OSM again to check that the point isn't a part of ways or relations - OsmObject.DownloadReferencingRelations(id).then((rels) => { + this.objectDownloader.DownloadReferencingRelations(id).then((rels) => { hasRelations.setData(rels.length > 0) }) - OsmObject.DownloadReferencingWays(id).then((ways) => { + this.objectDownloader.DownloadReferencingWays(id).then((ways) => { hasWays.setData(ways.length > 0) }) return true // unregister to only run once diff --git a/UI/Popup/MoveWizard.ts b/UI/Popup/MoveWizard.ts index 0fba077ce..294529d7f 100644 --- a/UI/Popup/MoveWizard.ts +++ b/UI/Popup/MoveWizard.ts @@ -233,13 +233,13 @@ export default class MoveWizard extends Toggle { } else if (id.startsWith("relation")) { moveDisallowedReason.setData(t.isRelation) } else if (id.indexOf("-") < 0) { - OsmObject.DownloadReferencingWays(id).then((referencing) => { + state.osmObjectDownloader.DownloadReferencingWays(id).then((referencing) => { if (referencing.length > 0) { console.log("Got a referencing way, move not allowed") moveDisallowedReason.setData(t.partOfAWay) } }) - OsmObject.DownloadReferencingRelations(id).then((partOf) => { + state.osmObjectDownloader.DownloadReferencingRelations(id).then((partOf) => { if (partOf.length > 0) { moveDisallowedReason.setData(t.partOfRelation) } diff --git a/UI/Popup/SplitRoadWizard.ts b/UI/Popup/SplitRoadWizard.ts index bf3ab9197..9abed2cbb 100644 --- a/UI/Popup/SplitRoadWizard.ts +++ b/UI/Popup/SplitRoadWizard.ts @@ -12,13 +12,13 @@ import { VariableUiElement } from "../Base/VariableUIElement" import { LoginToggle } from "./LoginButton" import SvelteUIElement from "../Base/SvelteUIElement" import WaySplitMap from "../BigComponents/WaySplitMap.svelte" -import { OsmObject } from "../../Logic/Osm/OsmObject" import { Feature, Point } from "geojson" import { WayId } from "../../Models/OsmFeature" import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { Changes } from "../../Logic/Osm/Changes" import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader" export default class SplitRoadWizard extends Combine { public dialogIsOpened: UIEventSource @@ -34,6 +34,7 @@ export default class SplitRoadWizard extends Combine { state: { layout?: LayoutConfig osmConnection?: OsmConnection + osmObjectDownloader?: OsmObjectDownloader changes?: Changes indexedFeatures?: IndexedFeatureSource selectedElement?: UIEventSource @@ -52,7 +53,15 @@ export default class SplitRoadWizard extends Combine { const leafletMap = new UIEventSource(undefined) function initMap() { - SplitRoadWizard.setupMapComponent(id, splitPoints).then((mapComponent) => + ;(async function ( + id: WayId, + splitPoints: UIEventSource + ): Promise { + return new SvelteUIElement(WaySplitMap, { + osmWay: await state.osmObjectDownloader.DownloadObjectAsync(id), + splitPoints, + }) + })(id, splitPoints).then((mapComponent) => leafletMap.setData(mapComponent.SetClass("w-full h-80")) ) } @@ -132,15 +141,4 @@ export default class SplitRoadWizard extends Combine { self.ScrollIntoView() }) } - - private static async setupMapComponent( - id: WayId, - splitPoints: UIEventSource - ): Promise { - const osmWay = await OsmObject.DownloadObjectAsync(id) - return new SvelteUIElement(WaySplitMap, { - osmWay, - splitPoints, - }) - } } diff --git a/UI/SpecialVisualization.ts b/UI/SpecialVisualization.ts index c30da739c..9e64d16ac 100644 --- a/UI/SpecialVisualization.ts +++ b/UI/SpecialVisualization.ts @@ -13,6 +13,7 @@ import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexe import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; import { MenuState } from "../Models/MenuState"; +import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"; /** * The state needed to render a special Visualisation. @@ -39,6 +40,7 @@ export interface SpecialVisualizationState { readonly featureSwitchUserbadge: Store readonly featureSwitchIsTesting: Store readonly changes: Changes + readonly osmObjectDownloader: OsmObjectDownloader /** * State of the main map */ diff --git a/scripts/CycleHighwayFix.ts b/scripts/CycleHighwayFix.ts index 637e13c28..246f7064f 100644 --- a/scripts/CycleHighwayFix.ts +++ b/scripts/CycleHighwayFix.ts @@ -1,6 +1,7 @@ import ScriptUtils from "./ScriptUtils" import { appendFileSync, readFileSync, writeFileSync } from "fs" import { OsmObject } from "../Logic/Osm/OsmObject" +import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"; ScriptUtils.fixUtils() @@ -17,7 +18,7 @@ const ids = JSON.parse(readFileSync("export.geojson", "utf-8")).features.map( ) console.log(ids) ids.map((id) => - OsmObject.DownloadReferencingRelations(id).then((relations) => { + new OsmObjectDownloader().DownloadReferencingRelations(id).then((relations) => { console.log(relations) const changeparts = relations .filter( diff --git a/test/Logic/OSM/Actions/RelationSplitHandler.spec.ts b/test/Logic/OSM/Actions/RelationSplitHandler.spec.ts index 7392ffaa0..d73d729e7 100644 --- a/test/Logic/OSM/Actions/RelationSplitHandler.spec.ts +++ b/test/Logic/OSM/Actions/RelationSplitHandler.spec.ts @@ -1,11 +1,14 @@ import { Utils } from "../../../../Utils" -import { OsmObject, OsmRelation } from "../../../../Logic/Osm/OsmObject" +import { OsmRelation } from "../../../../Logic/Osm/OsmObject" import { InPlaceReplacedmentRTSH, TurnRestrictionRSH, } from "../../../../Logic/Osm/Actions/RelationSplitHandler" import { Changes } from "../../../../Logic/Osm/Changes" import { describe, expect, it } from "vitest" +import OsmObjectDownloader from "../../../../Logic/Osm/OsmObjectDownloader" +import { ImmutableStore } from "../../../../Logic/UIEventSource" +import { OsmConnection } from "../../../../Logic/Osm/OsmConnection" describe("RelationSplitHandler", () => { Utils.injectJsonDownloadForTests("https://www.openstreetmap.org/api/0.6/node/1124134958/ways", { @@ -624,8 +627,9 @@ describe("RelationSplitHandler", () => { it("should split all cycling relation (split 295132739)", async () => { // Lets mimic a split action of https://www.openstreetmap.org/way/295132739 + const downloader = new OsmObjectDownloader() const relation: OsmRelation = ( - await OsmObject.DownloadObjectAsync("relation/9572808") + await downloader.DownloadObjectAsync("relation/9572808") ) const originalNodeIds = [ 5273988967, 170497153, 1507524582, 4524321710, 170497155, 170497157, 170497158, @@ -645,9 +649,13 @@ describe("RelationSplitHandler", () => { originalNodes: originalNodeIds, allWaysNodesInOrder: withSplit, }, - "no-theme" + "no-theme", + downloader ) - const changeDescription = await splitter.CreateChangeDescriptions(new Changes()) + const changeDescription = await splitter.CreateChangeDescriptions(new Changes({ + dryRun: new ImmutableStore(false), + osmConnection: new OsmConnection() + })) const allIds = changeDescription[0].changes["members"].map((m) => m.ref).join(",") const expected = "687866206,295132739,-1,690497698" // "didn't find the expected order of ids in the relation to test" @@ -655,8 +663,9 @@ describe("RelationSplitHandler", () => { }) it("should split turn restrictions (split of https://www.openstreetmap.org/way/143298912)", async () => { + const downloader = new OsmObjectDownloader() const relation: OsmRelation = ( - await OsmObject.DownloadObjectAsync("relation/4374576") + await downloader.DownloadObjectAsync("relation/4374576") ) const originalNodeIds = [ 1407529979, 1974988033, 3250129361, 1634435395, 8493044168, 875668688, 1634435396, @@ -695,9 +704,13 @@ describe("RelationSplitHandler", () => { originalNodes: originalNodeIds, allWaysNodesInOrder: withSplit, }, - "no-theme" + "no-theme", + downloader ) - const changeDescription = await splitter.CreateChangeDescriptions(new Changes()) + const changeDescription = await splitter.CreateChangeDescriptions(new Changes({ + dryRun: new ImmutableStore(false), + osmConnection: new OsmConnection() + })) const allIds = changeDescription[0].changes["members"] .map((m) => m.type + "/" + m.ref + "-->" + m.role) .join(",") @@ -713,9 +726,15 @@ describe("RelationSplitHandler", () => { originalNodes: originalNodeIds, allWaysNodesInOrder: withSplit, }, - "no-theme" + "no-theme", + downloader + ) + const changesReverse = await splitterReverse.CreateChangeDescriptions( + new Changes({ + dryRun: new ImmutableStore(false), + osmConnection: new OsmConnection(), + }) ) - const changesReverse = await splitterReverse.CreateChangeDescriptions(new Changes()) expect(changesReverse.length).toEqual(0) }) }) diff --git a/test/Logic/OSM/OsmObject.spec.ts b/test/Logic/OSM/OsmObject.spec.ts index b619d41d8..5cf3fcc3f 100644 --- a/test/Logic/OSM/OsmObject.spec.ts +++ b/test/Logic/OSM/OsmObject.spec.ts @@ -3,6 +3,7 @@ import { Utils } from "../../../Utils" import ScriptUtils from "../../../scripts/ScriptUtils" import { readFileSync } from "fs" import { describe, expect, it } from "vitest" +import OsmObjectDownloader from "../../../Logic/Osm/OsmObjectDownloader" describe("OsmObject", () => { describe("download referencing ways", () => { @@ -79,7 +80,8 @@ describe("OsmObject", () => { ) it("should download referencing ways", async () => { - const ways = await OsmObject.DownloadReferencingWays("node/1124134958") + const downloader = new OsmObjectDownloader() + const ways = await downloader.DownloadReferencingWays("node/1124134958") expect(ways).toBeDefined() expect(ways).toHaveLength(4) }) @@ -90,7 +92,7 @@ describe("OsmObject", () => { "https://www.openstreetmap.org/api/0.6/relation/5759328/full", JSON.parse(readFileSync("./test/data/relation_5759328.json", { encoding: "utf-8" })) ) - const r = await OsmObject.DownloadObjectAsync("relation/5759328").then((x) => x) + const r = await new OsmObjectDownloader().DownloadObjectAsync("relation/5759328").then((x) => x) const geojson = r.asGeoJson() expect(geojson.geometry.type).toBe("MultiPolygon") })