forked from MapComplete/MapComplete
		
	Refactoring: move download functionality for OsmObjects into a new object
This commit is contained in:
		
							parent
							
								
									8eb2c68f79
								
							
						
					
					
						commit
						1f9aacfb29
					
				
					 23 changed files with 633 additions and 901 deletions
				
			
		|  | @ -3,13 +3,13 @@ | ||||||
|  */ |  */ | ||||||
| import { UIEventSource } from "../UIEventSource" | import { UIEventSource } from "../UIEventSource" | ||||||
| import { Changes } from "../Osm/Changes" | import { Changes } from "../Osm/Changes" | ||||||
| import { OsmObject } from "../Osm/OsmObject" |  | ||||||
| import { OsmConnection } from "../Osm/OsmConnection" | import { OsmConnection } from "../Osm/OsmConnection" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import SimpleMetaTagger from "../SimpleMetaTagger" | import SimpleMetaTagger from "../SimpleMetaTagger" | ||||||
| import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" | import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
| import { OsmTags } from "../../Models/OsmFeature" | import { OsmTags } from "../../Models/OsmFeature" | ||||||
|  | import OsmObjectDownloader from "../Osm/OsmObjectDownloader" | ||||||
| 
 | 
 | ||||||
| export default class SelectedElementTagsUpdater { | export default class SelectedElementTagsUpdater { | ||||||
|     private static readonly metatags = new Set([ |     private static readonly metatags = new Set([ | ||||||
|  | @ -27,6 +27,7 @@ export default class SelectedElementTagsUpdater { | ||||||
|         changes: Changes |         changes: Changes | ||||||
|         osmConnection: OsmConnection |         osmConnection: OsmConnection | ||||||
|         layout: LayoutConfig |         layout: LayoutConfig | ||||||
|  |         osmObjectDownloader: OsmObjectDownloader | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     constructor(state: { |     constructor(state: { | ||||||
|  | @ -35,6 +36,7 @@ export default class SelectedElementTagsUpdater { | ||||||
|         changes: Changes |         changes: Changes | ||||||
|         osmConnection: OsmConnection |         osmConnection: OsmConnection | ||||||
|         layout: LayoutConfig |         layout: LayoutConfig | ||||||
|  |         osmObjectDownloader: OsmObjectDownloader | ||||||
|     }) { |     }) { | ||||||
|         this.state = state |         this.state = state | ||||||
|         state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { |         state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { | ||||||
|  | @ -70,8 +72,8 @@ export default class SelectedElementTagsUpdater { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             try { |             try { | ||||||
|                 const latestTags = await OsmObject.DownloadPropertiesOf(id) |                 const osmObject = await state.osmObjectDownloader.DownloadObjectAsync(id) | ||||||
|                 if (latestTags === "deleted") { |                 if (osmObject === "deleted") { | ||||||
|                     console.warn("The current selected element has been deleted upstream!") |                     console.warn("The current selected element has been deleted upstream!") | ||||||
|                     const currentTagsSource = state.featureProperties.getStore(id) |                     const currentTagsSource = state.featureProperties.getStore(id) | ||||||
|                     if (currentTagsSource.data["_deleted"] === "yes") { |                     if (currentTagsSource.data["_deleted"] === "yes") { | ||||||
|  | @ -81,6 +83,7 @@ export default class SelectedElementTagsUpdater { | ||||||
|                     currentTagsSource.ping() |                     currentTagsSource.ping() | ||||||
|                     return |                     return | ||||||
|                 } |                 } | ||||||
|  |                 const latestTags = osmObject.tags | ||||||
|                 this.applyUpdate(latestTags, id) |                 this.applyUpdate(latestTags, id) | ||||||
|                 console.log("Updated", id) |                 console.log("Updated", id) | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|  |  | ||||||
|  | @ -1,10 +1,10 @@ | ||||||
| import { UIEventSource } from "../UIEventSource" | import { UIEventSource } from "../UIEventSource" | ||||||
| import { OsmObject } from "../Osm/OsmObject" |  | ||||||
| import Loc from "../../Models/Loc" | import Loc from "../../Models/Loc" | ||||||
| import { ElementStorage } from "../ElementStorage" | import { ElementStorage } from "../ElementStorage" | ||||||
| import FeaturePipeline from "../FeatureSource/FeaturePipeline" | import FeaturePipeline from "../FeatureSource/FeaturePipeline" | ||||||
| import { GeoOperations } from "../GeoOperations" | import { GeoOperations } from "../GeoOperations" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
|  | import OsmObjectDownloader from "../Osm/OsmObjectDownloader" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Makes sure the hash shows the selected element and vice-versa. |  * Makes sure the hash shows the selected element and vice-versa. | ||||||
|  | @ -26,6 +26,7 @@ export default class SelectedFeatureHandler { | ||||||
|         allElements: ElementStorage |         allElements: ElementStorage | ||||||
|         locationControl: UIEventSource<Loc> |         locationControl: UIEventSource<Loc> | ||||||
|         layoutToUse: LayoutConfig |         layoutToUse: LayoutConfig | ||||||
|  |         objectDownloader: OsmObjectDownloader | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|  | @ -36,6 +37,7 @@ export default class SelectedFeatureHandler { | ||||||
|             featurePipeline: FeaturePipeline |             featurePipeline: FeaturePipeline | ||||||
|             locationControl: UIEventSource<Loc> |             locationControl: UIEventSource<Loc> | ||||||
|             layoutToUse: LayoutConfig |             layoutToUse: LayoutConfig | ||||||
|  |             objectDownloader: OsmObjectDownloader | ||||||
|         } |         } | ||||||
|     ) { |     ) { | ||||||
|         this.hash = hash |         this.hash = hash | ||||||
|  | @ -65,8 +67,11 @@ export default class SelectedFeatureHandler { | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         OsmObject.DownloadObjectAsync(hash).then((obj) => { |         this.state.objectDownloader.DownloadObjectAsync(hash).then((obj) => { | ||||||
|             try { |             try { | ||||||
|  |                 if (obj === "deleted") { | ||||||
|  |                     return | ||||||
|  |                 } | ||||||
|                 console.log("Downloaded selected object from OSM-API for initial load: ", hash) |                 console.log("Downloaded selected object from OSM-API for initial load: ", hash) | ||||||
|                 const geojson = obj.asGeoJson() |                 const geojson = obj.asGeoJson() | ||||||
|                 this.state.allElements.addOrGetElement(geojson) |                 this.state.allElements.addOrGetElement(geojson) | ||||||
|  |  | ||||||
|  | @ -1,10 +1,11 @@ | ||||||
| import { Changes } from "../../Osm/Changes" | 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 { IndexedFeatureSource, WritableFeatureSource } from "../FeatureSource" | ||||||
| import { UIEventSource } from "../../UIEventSource" | import { UIEventSource } from "../../UIEventSource" | ||||||
| import { ChangeDescription } from "../../Osm/Actions/ChangeDescription" | import { ChangeDescription } from "../../Osm/Actions/ChangeDescription" | ||||||
| import { OsmId, OsmTags } from "../../../Models/OsmFeature" | import { OsmId, OsmTags } from "../../../Models/OsmFeature" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
|  | import OsmObjectDownloader from "../../Osm/OsmObjectDownloader" | ||||||
| 
 | 
 | ||||||
| export class NewGeometryFromChangesFeatureSource implements WritableFeatureSource { | export class NewGeometryFromChangesFeatureSource implements WritableFeatureSource { | ||||||
|     // This class name truly puts the 'Java' into 'Javascript'
 |     // This class name truly puts the 'Java' into 'Javascript'
 | ||||||
|  | @ -21,7 +22,7 @@ export class NewGeometryFromChangesFeatureSource implements WritableFeatureSourc | ||||||
|         const seenChanges = new Set<ChangeDescription>() |         const seenChanges = new Set<ChangeDescription>() | ||||||
|         const features = this.features.data |         const features = this.features.data | ||||||
|         const self = this |         const self = this | ||||||
| 
 |         const backend = changes.backend | ||||||
|         changes.pendingChanges.stabilized(100).addCallbackAndRunD((changes) => { |         changes.pendingChanges.stabilized(100).addCallbackAndRunD((changes) => { | ||||||
|             if (changes.length === 0) { |             if (changes.length === 0) { | ||||||
|                 return |                 return | ||||||
|  | @ -58,15 +59,20 @@ export class NewGeometryFromChangesFeatureSource implements WritableFeatureSourc | ||||||
|                     } |                     } | ||||||
|                     console.debug("Detected a reused point") |                     console.debug("Detected a reused point") | ||||||
|                     // The 'allElementsStore' does _not_ have this point yet, so we have to create it
 |                     // The 'allElementsStore' does _not_ have this point yet, so we have to create it
 | ||||||
|                     OsmObject.DownloadObjectAsync(change.type + "/" + change.id).then((feat) => { |                     new OsmObjectDownloader(backend) | ||||||
|                         console.log("Got the reused point:", feat) |                         .DownloadObjectAsync(change.type + "/" + change.id) | ||||||
|                         for (const kv of change.tags) { |                         .then((feat) => { | ||||||
|                             feat.tags[kv.k] = kv.v |                             console.log("Got the reused point:", feat) | ||||||
|                         } |                             if (feat === "deleted") { | ||||||
|                         const geojson = feat.asGeoJson() |                                 throw "Panic: snapping to a point, but this point has been deleted in the meantime" | ||||||
|                         self.features.data.push(geojson) |                             } | ||||||
|                         self.features.ping() |                             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 |                     continue | ||||||
|                 } else if (change.id < 0 && change.changes === undefined) { |                 } else if (change.id < 0 && change.changes === undefined) { | ||||||
|                     // The geometry is not described - not a new point
 |                     // The geometry is not described - not a new point
 | ||||||
|  |  | ||||||
|  | @ -4,9 +4,9 @@ import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" | ||||||
| import { Tiles } from "../../../Models/TileRange" | import { Tiles } from "../../../Models/TileRange" | ||||||
| import { BBox } from "../../BBox" | import { BBox } from "../../BBox" | ||||||
| import { TagsFilter } from "../../Tags/TagsFilter" | import { TagsFilter } from "../../Tags/TagsFilter" | ||||||
| import { OsmObject } from "../../Osm/OsmObject" |  | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
| import FeatureSourceMerger from "../Sources/FeatureSourceMerger" | 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' |  * 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
 |             // This member is missing. We redownload the entire relation instead
 | ||||||
|             console.debug("Fetching incomplete relation " + feature.properties.id) |             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 |         return feature | ||||||
|     } |     } | ||||||
|  | @ -149,6 +159,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger { | ||||||
|                 for (let i = 0; i < features.length; i++) { |                 for (let i = 0; i < features.length; i++) { | ||||||
|                     features[i] = await this.patchIncompleteRelations(features[i], osmJson) |                     features[i] = await this.patchIncompleteRelations(features[i], osmJson) | ||||||
|                 } |                 } | ||||||
|  |                 features = Utils.NoNull(features) | ||||||
|                 features.forEach((f) => { |                 features.forEach((f) => { | ||||||
|                     f.properties["_backend"] = this._backend |                     f.properties["_backend"] = this._backend | ||||||
|                 }) |                 }) | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import { And } from "../../Tags/And" | ||||||
| import { Tag } from "../../Tags/Tag" | import { Tag } from "../../Tags/Tag" | ||||||
| import { OsmId } from "../../../Models/OsmFeature" | import { OsmId } from "../../../Models/OsmFeature" | ||||||
| import { Utils } from "../../../Utils" | import { Utils } from "../../../Utils" | ||||||
|  | import OsmObjectDownloader from "../OsmObjectDownloader"; | ||||||
| 
 | 
 | ||||||
| export default class DeleteAction extends OsmChangeAction { | export default class DeleteAction extends OsmChangeAction { | ||||||
|     private readonly _softDeletionTags: TagsFilter |     private readonly _softDeletionTags: TagsFilter | ||||||
|  | @ -71,8 +72,12 @@ export default class DeleteAction extends OsmChangeAction { | ||||||
|         changes: Changes, |         changes: Changes, | ||||||
|         object?: OsmObject |         object?: OsmObject | ||||||
|     ): Promise<ChangeDescription[]> { |     ): Promise<ChangeDescription[]> { | ||||||
|         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) { |         if (this._hardDelete) { | ||||||
|             return [ |             return [ | ||||||
|                 { |                 { | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| import OsmChangeAction from "./OsmChangeAction" | import OsmChangeAction from "./OsmChangeAction" | ||||||
| import { Changes } from "../Changes" | import { Changes } from "../Changes" | ||||||
| import { ChangeDescription } from "./ChangeDescription" | import { ChangeDescription } from "./ChangeDescription" | ||||||
| import { OsmObject, OsmRelation, OsmWay } from "../OsmObject" | import { OsmRelation, OsmWay } from "../OsmObject" | ||||||
|  | import OsmObjectDownloader from "../OsmObjectDownloader" | ||||||
| 
 | 
 | ||||||
| export interface RelationSplitInput { | export interface RelationSplitInput { | ||||||
|     relation: OsmRelation |     relation: OsmRelation | ||||||
|  | @ -14,11 +15,13 @@ export interface RelationSplitInput { | ||||||
| abstract class AbstractRelationSplitHandler extends OsmChangeAction { | abstract class AbstractRelationSplitHandler extends OsmChangeAction { | ||||||
|     protected readonly _input: RelationSplitInput |     protected readonly _input: RelationSplitInput | ||||||
|     protected readonly _theme: string |     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) |         super("relation/" + input.relation.id, false) | ||||||
|         this._input = input |         this._input = input | ||||||
|         this._theme = theme |         this._theme = theme | ||||||
|  |         this._objectDownloader = objectDownloader | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -33,7 +36,9 @@ abstract class AbstractRelationSplitHandler extends OsmChangeAction { | ||||||
|             return member.ref |             return member.ref | ||||||
|         } |         } | ||||||
|         if (member.type === "way") { |         if (member.type === "way") { | ||||||
|             const osmWay = <OsmWay>await OsmObject.DownloadObjectAsync("way/" + member.ref) |             const osmWay = <OsmWay>( | ||||||
|  |                 await this._objectDownloader.DownloadObjectAsync("way/" + member.ref) | ||||||
|  |             ) | ||||||
|             const nodes = osmWay.nodes |             const nodes = osmWay.nodes | ||||||
|             if (first) { |             if (first) { | ||||||
|                 return nodes[0] |                 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. |  * 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 { | export default class RelationSplitHandler extends AbstractRelationSplitHandler { | ||||||
|     constructor(input: RelationSplitInput, theme: string) { |     constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) { | ||||||
|         super(input, theme) |         super(input, theme, objectDownloader) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
|         if (this._input.relation.tags["type"] === "restriction") { |         if (this._input.relation.tags["type"] === "restriction") { | ||||||
|             // This is a turn restriction
 |             // This is a turn restriction
 | ||||||
|             return new TurnRestrictionRSH(this._input, this._theme).CreateChangeDescriptions( |             return new TurnRestrictionRSH( | ||||||
|                 changes |                 this._input, | ||||||
|             ) |                 this._theme, | ||||||
|  |                 this._objectDownloader | ||||||
|  |             ).CreateChangeDescriptions(changes) | ||||||
|         } |         } | ||||||
|         return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions( |         return new InPlaceReplacedmentRTSH( | ||||||
|             changes |             this._input, | ||||||
|         ) |             this._theme, | ||||||
|  |             this._objectDownloader | ||||||
|  |         ).CreateChangeDescriptions(changes) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class TurnRestrictionRSH extends AbstractRelationSplitHandler { | export class TurnRestrictionRSH extends AbstractRelationSplitHandler { | ||||||
|     constructor(input: RelationSplitInput, theme: string) { |     constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) { | ||||||
|         super(input, theme) |         super(input, theme, objectDownloader) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
|  | @ -91,9 +100,11 @@ export class TurnRestrictionRSH extends AbstractRelationSplitHandler { | ||||||
| 
 | 
 | ||||||
|         if (selfMember.role === "via") { |         if (selfMember.role === "via") { | ||||||
|             // A via way can be replaced in place
 |             // A via way can be replaced in place
 | ||||||
|             return new InPlaceReplacedmentRTSH(this._input, this._theme).CreateChangeDescriptions( |             return new InPlaceReplacedmentRTSH( | ||||||
|                 changes |                 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
 |         // 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. |  * Note that the feature might appear multiple times. | ||||||
|  */ |  */ | ||||||
| export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { | export class InPlaceReplacedmentRTSH extends AbstractRelationSplitHandler { | ||||||
|     constructor(input: RelationSplitInput, theme: string) { |     constructor(input: RelationSplitInput, theme: string, objectDownloader: OsmObjectDownloader) { | ||||||
|         super(input, theme) |         super(input, theme, objectDownloader) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import OsmChangeAction from "./OsmChangeAction" | ||||||
| import { ChangeDescription } from "./ChangeDescription" | import { ChangeDescription } from "./ChangeDescription" | ||||||
| import RelationSplitHandler from "./RelationSplitHandler" | import RelationSplitHandler from "./RelationSplitHandler" | ||||||
| import { Feature, LineString } from "geojson" | import { Feature, LineString } from "geojson" | ||||||
|  | import OsmObjectDownloader from "../OsmObjectDownloader" | ||||||
| 
 | 
 | ||||||
| interface SplitInfo { | interface SplitInfo { | ||||||
|     originalIndex?: number // or negative for new elements
 |     originalIndex?: number // or negative for new elements
 | ||||||
|  | @ -61,7 +62,9 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { |     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||||
|         const originalElement = <OsmWay>await OsmObject.DownloadObjectAsync(this.wayId) |         const originalElement = <OsmWay>( | ||||||
|  |             await new OsmObjectDownloader(changes.backend, changes).DownloadObjectAsync(this.wayId) | ||||||
|  |         ) | ||||||
|         const originalNodes = originalElement.nodes |         const originalNodes = originalElement.nodes | ||||||
| 
 | 
 | ||||||
|         // First, calculate the splitpoints and remove points close to one another
 |         // 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 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
 |         // 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) { |         for (const relation of relations) { | ||||||
|             const changDescrs = await new RelationSplitHandler( |             const changDescrs = await new RelationSplitHandler( | ||||||
|                 { |                 { | ||||||
|  | @ -182,7 +186,8 @@ export default class SplitAction extends OsmChangeAction { | ||||||
|                     allWaysNodesInOrder: allWaysNodesInOrder, |                     allWaysNodesInOrder: allWaysNodesInOrder, | ||||||
|                     originalWayId: originalElement.id, |                     originalWayId: originalElement.id, | ||||||
|                 }, |                 }, | ||||||
|                 this._meta.theme |                 this._meta.theme, | ||||||
|  |                 downloader | ||||||
|             ).CreateChangeDescriptions(changes) |             ).CreateChangeDescriptions(changes) | ||||||
|             changeDescription.push(...changDescrs) |             changeDescription.push(...changDescrs) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ import { GeoOperations } from "../GeoOperations" | ||||||
| import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler" | import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler" | ||||||
| import { OsmConnection } from "./OsmConnection" | import { OsmConnection } from "./OsmConnection" | ||||||
| import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" | import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" | ||||||
|  | import OsmObjectDownloader from "./OsmObjectDownloader" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Handles all changes made to OSM. |  * Handles all changes made to OSM. | ||||||
|  | @ -23,10 +24,10 @@ export class Changes { | ||||||
|     public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined) |     public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined) | ||||||
|     public readonly state: { allElements?: IndexedFeatureSource; osmConnection: OsmConnection } |     public readonly state: { allElements?: IndexedFeatureSource; osmConnection: OsmConnection } | ||||||
|     public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined) |     public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined) | ||||||
| 
 |     public readonly backend: string | ||||||
|     private readonly historicalUserLocations?: FeatureSource |     private readonly historicalUserLocations?: FeatureSource | ||||||
|     private _nextId: number = -1 // Newly assigned ID's are negative
 |     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 previouslyCreated: OsmObject[] = [] | ||||||
|     private readonly _leftRightSensitive: boolean |     private readonly _leftRightSensitive: boolean | ||||||
|     private readonly _changesetHandler: ChangesetHandler |     private readonly _changesetHandler: ChangesetHandler | ||||||
|  | @ -47,6 +48,7 @@ export class Changes { | ||||||
|         // If a pending change contains a negative ID, we save that
 |         // If a pending change contains a negative ID, we save that
 | ||||||
|         this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id) ?? [])) |         this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id) ?? [])) | ||||||
|         this.state = state |         this.state = state | ||||||
|  |         this.backend = state.osmConnection.Backend() | ||||||
|         this._changesetHandler = new ChangesetHandler( |         this._changesetHandler = new ChangesetHandler( | ||||||
|             state.dryRun, |             state.dryRun, | ||||||
|             state.osmConnection, |             state.osmConnection, | ||||||
|  | @ -149,274 +151,6 @@ export class Changes { | ||||||
|         this.allChanges.ping() |         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( |  | ||||||
|                     (<GeoLocationPointProperties>(<any>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<number> |  | ||||||
|     ): Promise<boolean> { |  | ||||||
|         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<void> { |  | ||||||
|         const self = this |  | ||||||
|         try { |  | ||||||
|             // At last, we build the changeset and upload
 |  | ||||||
|             const pending = self.pendingChanges.data |  | ||||||
| 
 |  | ||||||
|             const pendingPerTheme = new Map<string, ChangeDescription[]>() |  | ||||||
|             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( |     public CreateChangesetObjects( | ||||||
|         changes: ChangeDescription[], |         changes: ChangeDescription[], | ||||||
|         downloadedOsmObjects: OsmObject[] |         downloadedOsmObjects: OsmObject[] | ||||||
|  | @ -584,4 +318,285 @@ export class Changes { | ||||||
|         ) |         ) | ||||||
|         return result |         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( | ||||||
|  |                     (<GeoLocationPointProperties>(<any>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<number> | ||||||
|  |     ): Promise<boolean> { | ||||||
|  |         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) => <OsmObject>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<void> { | ||||||
|  |         const self = this | ||||||
|  |         try { | ||||||
|  |             // At last, we build the changeset and upload
 | ||||||
|  |             const pending = self.pendingChanges.data | ||||||
|  | 
 | ||||||
|  |             const pendingPerTheme = new Map<string, ChangeDescription[]>() | ||||||
|  |             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) | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -82,7 +82,6 @@ export class OsmConnection { | ||||||
|             OsmConnection.oauth_configs[options.osmConfiguration ?? "osm"] ?? |             OsmConnection.oauth_configs[options.osmConfiguration ?? "osm"] ?? | ||||||
|             OsmConnection.oauth_configs.osm |             OsmConnection.oauth_configs.osm | ||||||
|         console.debug("Using backend", this._oauth_config.url) |         console.debug("Using backend", this._oauth_config.url) | ||||||
|         OsmObject.SetBackendUrl(this._oauth_config.url + "/") |  | ||||||
|         this._iframeMode = Utils.runningFromConsole ? false : window !== window.top |         this._iframeMode = Utils.runningFromConsole ? false : window !== window.top | ||||||
| 
 | 
 | ||||||
|         this.userDetails = new UIEventSource<UserDetails>( |         this.userDetails = new UIEventSource<UserDetails>( | ||||||
|  |  | ||||||
|  | @ -1,17 +1,13 @@ | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import polygon_features from "../../assets/polygon-features.json" | import polygon_features from "../../assets/polygon-features.json" | ||||||
| import { Store, UIEventSource } from "../UIEventSource" |  | ||||||
| import { BBox } from "../BBox" |  | ||||||
| import OsmToGeoJson from "osmtogeojson" | 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" | import { Feature, LineString, Polygon } from "geojson" | ||||||
| 
 | 
 | ||||||
| export abstract class OsmObject { | export abstract class OsmObject { | ||||||
|     private static defaultBackend = "https://www.openstreetmap.org/" |     private static defaultBackend = "https://www.openstreetmap.org/" | ||||||
|     protected static backendURL = OsmObject.defaultBackend |     protected static backendURL = OsmObject.defaultBackend | ||||||
|     private static polygonFeatures = OsmObject.constructPolygonFeatures() |     private static polygonFeatures = OsmObject.constructPolygonFeatures() | ||||||
|     private static objectCache = new Map<string, UIEventSource<OsmObject>>() |  | ||||||
|     private static historyCache = new Map<string, UIEventSource<OsmObject[]>>() |  | ||||||
|     type: "node" | "way" | "relation" |     type: "node" | "way" | "relation" | ||||||
|     id: number |     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<OsmNode> |  | ||||||
|     public static DownloadObject(id: RelationId, forceRefresh?: boolean): Store<OsmRelation> |  | ||||||
|     public static DownloadObject(id: WayId, forceRefresh?: boolean): Store<OsmWay> |  | ||||||
|     public static DownloadObject(id: string, forceRefresh: boolean = false): Store<OsmObject> { |  | ||||||
|         let src: UIEventSource<OsmObject> |  | ||||||
|         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<OsmTags | "deleted"> { |  | ||||||
|         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<OsmNode | undefined> |  | ||||||
|     static async DownloadObjectAsync( |  | ||||||
|         id: WayId, |  | ||||||
|         maxCacheAgeInSecs?: number |  | ||||||
|     ): Promise<OsmWay | undefined> |  | ||||||
|     static async DownloadObjectAsync( |  | ||||||
|         id: RelationId, |  | ||||||
|         maxCacheAgeInSecs?: number |  | ||||||
|     ): Promise<OsmRelation | undefined> |  | ||||||
|     static async DownloadObjectAsync( |  | ||||||
|         id: OsmId, |  | ||||||
|         maxCacheAgeInSecs?: number |  | ||||||
|     ): Promise<OsmObject | undefined> |  | ||||||
|     static async DownloadObjectAsync( |  | ||||||
|         id: string, |  | ||||||
|         maxCacheAgeInSecs?: number |  | ||||||
|     ): Promise<OsmObject | undefined> |  | ||||||
|     static async DownloadObjectAsync( |  | ||||||
|         id: string, |  | ||||||
|         maxCacheAgeInSecs?: number |  | ||||||
|     ): Promise<OsmObject | undefined> { |  | ||||||
|         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<OsmWay[]> { |  | ||||||
|         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<OsmRelation[]> { |  | ||||||
|         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<OsmNode[]> |  | ||||||
|     public static DownloadHistory(id: WayId): UIEventSource<OsmWay[]> |  | ||||||
|     public static DownloadHistory(id: RelationId): UIEventSource<OsmRelation[]> |  | ||||||
| 
 |  | ||||||
|     public static DownloadHistory(id: OsmId): UIEventSource<OsmObject[]> |  | ||||||
|     public static DownloadHistory(id: string): UIEventSource<OsmObject[]> { |  | ||||||
|         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[]>([]) |  | ||||||
|         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<OsmObject[]> { |  | ||||||
|         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[] { |     public static ParseObjects(elements: any[]): OsmObject[] { | ||||||
|         const objects: OsmObject[] = [] |         const objects: OsmObject[] = [] | ||||||
|         const allNodes: Map<number, OsmNode> = new Map<number, OsmNode>() |         const allNodes: Map<number, OsmNode> = new Map<number, OsmNode>() | ||||||
|  | @ -357,12 +169,16 @@ export abstract class OsmObject { | ||||||
|         return 'version="' + this.version + '"' |         return 'version="' + this.version + '"' | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private LoadData(element: any): void { |     protected LoadData(element: any): void { | ||||||
|         this.tags = element.tags ?? this.tags |         if (element === undefined) { | ||||||
|         this.version = element.version |             return | ||||||
|         this.timestamp = element.timestamp |         } | ||||||
|  |         this.tags = element?.tags ?? this.tags | ||||||
|         const tgs = this.tags |         const tgs = this.tags | ||||||
|         if (element.tags === undefined) { |         tgs["id"] = <OsmId>(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
 |             // Simple node which is part of a way - not important
 | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|  | @ -371,7 +187,6 @@ export abstract class OsmObject { | ||||||
|         tgs["_last_edit:changeset"] = element.changeset |         tgs["_last_edit:changeset"] = element.changeset | ||||||
|         tgs["_last_edit:timestamp"] = element.timestamp |         tgs["_last_edit:timestamp"] = element.timestamp | ||||||
|         tgs["_version_number"] = element.version |         tgs["_version_number"] = element.version | ||||||
|         tgs["id"] = <OsmId>(this.type + "/" + this.id) |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -379,8 +194,9 @@ export class OsmNode extends OsmObject { | ||||||
|     lat: number |     lat: number | ||||||
|     lon: number |     lon: number | ||||||
| 
 | 
 | ||||||
|     constructor(id: number) { |     constructor(id: number, extraData?) { | ||||||
|         super("node", id) |         super("node", id) | ||||||
|  |         this.LoadData(extraData) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     ChangesetXML(changesetId: string): string { |     ChangesetXML(changesetId: string): string { | ||||||
|  | @ -431,8 +247,9 @@ export class OsmWay extends OsmObject { | ||||||
|     lat: number |     lat: number | ||||||
|     lon: number |     lon: number | ||||||
| 
 | 
 | ||||||
|     constructor(id: number) { |     constructor(id: number, wayInfo?) { | ||||||
|         super("way", id) |         super("way", id) | ||||||
|  |         this.LoadData(wayInfo) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     centerpoint(): [number, number] { |     centerpoint(): [number, number] { | ||||||
|  | @ -535,8 +352,9 @@ export class OsmRelation extends OsmObject { | ||||||
| 
 | 
 | ||||||
|     private geojson = undefined |     private geojson = undefined | ||||||
| 
 | 
 | ||||||
|     constructor(id: number) { |     constructor(id: number, extraInfo?: any) { | ||||||
|         super("relation", id) |         super("relation", id) | ||||||
|  |         this.LoadData(extraInfo) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     centerpoint(): [number, number] { |     centerpoint(): [number, number] { | ||||||
|  |  | ||||||
							
								
								
									
										152
									
								
								Logic/Osm/OsmObjectDownloader.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								Logic/Osm/OsmObjectDownloader.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<ChangeDescription[]> | ||||||
|  |         readonly isUploading: Store<boolean> | ||||||
|  |     } | ||||||
|  |     private readonly backend: string | ||||||
|  |     private historyCache = new Map<string, UIEventSource<OsmObject[]>>() | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         backend: string = "https://openstreetmap.org", | ||||||
|  |         changes?: { | ||||||
|  |             readonly pendingChanges: UIEventSource<ChangeDescription[]> | ||||||
|  |             readonly isUploading: Store<boolean> | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         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<OsmNode | "deleted"> | ||||||
|  |     async DownloadObjectAsync(id: WayId, maxCacheAgeInSecs?: number): Promise<OsmWay | "deleted"> | ||||||
|  |     async DownloadObjectAsync( | ||||||
|  |         id: RelationId, | ||||||
|  |         maxCacheAgeInSecs?: number | ||||||
|  |     ): Promise<OsmRelation | undefined> | ||||||
|  |     async DownloadObjectAsync(id: OsmId, maxCacheAgeInSecs?: number): Promise<OsmObject | "deleted"> | ||||||
|  |     async DownloadObjectAsync( | ||||||
|  |         id: string, | ||||||
|  |         maxCacheAgeInSecs?: number | ||||||
|  |     ): Promise<OsmObject | "deleted"> | ||||||
|  |     async DownloadObjectAsync( | ||||||
|  |         id: string, | ||||||
|  |         maxCacheAgeInSecs?: number | ||||||
|  |     ): Promise<OsmObject | "deleted"> { | ||||||
|  |         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<OsmNode[]> | ||||||
|  | 
 | ||||||
|  |     public DownloadHistory(id: WayId): UIEventSource<OsmWay[]> | ||||||
|  | 
 | ||||||
|  |     public DownloadHistory(id: RelationId): UIEventSource<OsmRelation[]> | ||||||
|  | 
 | ||||||
|  |     public DownloadHistory(id: OsmId): UIEventSource<OsmObject[]> | ||||||
|  | 
 | ||||||
|  |     public DownloadHistory(id: string): UIEventSource<OsmObject[]> { | ||||||
|  |         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<OsmObject[]>([]) | ||||||
|  |         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<OsmWay[]> { | ||||||
|  |         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<OsmRelation[]> { | ||||||
|  |         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 | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -14,12 +14,14 @@ import { OsmObject } from "./Osm/OsmObject" | ||||||
| import { OsmTags } from "../Models/OsmFeature" | import { OsmTags } from "../Models/OsmFeature" | ||||||
| import { UIEventSource } from "./UIEventSource" | import { UIEventSource } from "./UIEventSource" | ||||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | ||||||
|  | import OsmObjectDownloader from "./Osm/OsmObjectDownloader" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * All elements that are needed to perform metatagging |  * All elements that are needed to perform metatagging | ||||||
|  */ |  */ | ||||||
| export interface MetataggingState { | export interface MetataggingState { | ||||||
|     layout: LayoutConfig |     layout: LayoutConfig | ||||||
|  |     osmObjectDownloader: OsmObjectDownloader | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export abstract class SimpleMetaTagger { | export abstract class SimpleMetaTagger { | ||||||
|  | @ -97,7 +99,7 @@ export class ReferencingWaysMetaTagger extends SimpleMetaTagger { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         Utils.AddLazyPropertyAsync(feature.properties, "_referencing_ways", async () => { |         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) |             const wayIds = referencingWays.map((w) => "way/" + w.id) | ||||||
|             wayIds.sort() |             wayIds.sort() | ||||||
|             return wayIds.join(";") |             return wayIds.join(";") | ||||||
|  |  | ||||||
|  | @ -42,6 +42,7 @@ import { MenuState } from "./MenuState" | ||||||
| import MetaTagging from "../Logic/MetaTagging" | import MetaTagging from "../Logic/MetaTagging" | ||||||
| import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator" | import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator" | ||||||
| import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource" | 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 osmConnection: OsmConnection | ||||||
|     readonly selectedElement: UIEventSource<Feature> |     readonly selectedElement: UIEventSource<Feature> | ||||||
|     readonly mapProperties: MapProperties & ExportableMap |     readonly mapProperties: MapProperties & ExportableMap | ||||||
|  |     readonly osmObjectDownloader: OsmObjectDownloader | ||||||
| 
 | 
 | ||||||
|     readonly dataIsLoading: Store<boolean> |     readonly dataIsLoading: Store<boolean> | ||||||
|     readonly guistate: MenuState |     readonly guistate: MenuState | ||||||
|  | @ -213,6 +215,8 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|             this.layout |             this.layout | ||||||
|         )) |         )) | ||||||
| 
 | 
 | ||||||
|  |         this.osmObjectDownloader = new OsmObjectDownloader(this.osmConnection.Backend(), this.changes) | ||||||
|  | 
 | ||||||
|         this.initActors() |         this.initActors() | ||||||
|         this.drawSpecialLayers(lastClick) |         this.drawSpecialLayers(lastClick) | ||||||
|         this.initHotkeys() |         this.initHotkeys() | ||||||
|  |  | ||||||
|  | @ -1,9 +1,5 @@ | ||||||
| import Combine from "../Base/Combine" | import Combine from "../Base/Combine" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" |  | ||||||
| import { Store } from "../../Logic/UIEventSource" | 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 Translations from "../i18n/Translations" | ||||||
| import { SubtleButton } from "../Base/SubtleButton" | import { SubtleButton } from "../Base/SubtleButton" | ||||||
| import Svg from "../../Svg" | import Svg from "../../Svg" | ||||||
|  | @ -11,7 +7,7 @@ import { Utils } from "../../Utils" | ||||||
| import { MapillaryLink } from "./MapillaryLink" | import { MapillaryLink } from "./MapillaryLink" | ||||||
| import { OpenIdEditor, OpenJosm } from "./CopyrightPanel" | import { OpenIdEditor, OpenJosm } from "./CopyrightPanel" | ||||||
| import Toggle from "../Input/Toggle" | import Toggle from "../Input/Toggle" | ||||||
| import { DefaultGuiState } from "../DefaultGuiState" | import { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
| 
 | 
 | ||||||
| export class BackToThemeOverview extends Toggle { | export class BackToThemeOverview extends Toggle { | ||||||
|     constructor( |     constructor( | ||||||
|  | @ -35,14 +31,7 @@ export class BackToThemeOverview extends Toggle { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class ActionButtons extends Combine { | export class ActionButtons extends Combine { | ||||||
|     constructor(state: { |     constructor(state:SpecialVisualizationState) { | ||||||
|         readonly layoutToUse: LayoutConfig |  | ||||||
|         readonly currentBounds: Store<BBox> |  | ||||||
|         readonly locationControl: Store<Loc> |  | ||||||
|         readonly osmConnection: OsmConnection |  | ||||||
|         readonly featureSwitchMoreQuests: Store<boolean> |  | ||||||
|         readonly defaultGuiState: DefaultGuiState |  | ||||||
|     }) { |  | ||||||
|         const imgSize = "h-6 w-6" |         const imgSize = "h-6 w-6" | ||||||
|         const iconStyle = "height: 1.5rem; width: 1.5rem" |         const iconStyle = "height: 1.5rem; width: 1.5rem" | ||||||
|         const t = Translations.t.general.attribution |         const t = Translations.t.general.attribution | ||||||
|  | @ -76,7 +65,7 @@ export class ActionButtons extends Combine { | ||||||
|             }), |             }), | ||||||
|             new OpenIdEditor(state, iconStyle), |             new OpenIdEditor(state, iconStyle), | ||||||
|             new MapillaryLink(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") |         this.SetClass("block w-full link-no-underline") | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -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<Loc>, MinimapObj |  | ||||||
| { |  | ||||||
|     private static readonly matchLayer = new LayerConfig( |  | ||||||
|         matchpoint, |  | ||||||
|         "LocationInput.matchpoint", |  | ||||||
|         true |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     public readonly snappedOnto: UIEventSource<Feature & { properties: { id: WayId } }> = |  | ||||||
|         new UIEventSource(undefined) |  | ||||||
|     public readonly _matching_layer: LayerConfig |  | ||||||
|     public readonly leafletMap: UIEventSource<any> |  | ||||||
|     public readonly bounds |  | ||||||
|     public readonly location |  | ||||||
|     private readonly _centerLocation: UIEventSource<Loc> |  | ||||||
|     private readonly mapBackground: UIEventSource<BaseLayer> |  | ||||||
|     /** |  | ||||||
|      * The features to which the input should be snapped |  | ||||||
|      * @private |  | ||||||
|      */ |  | ||||||
|     private readonly _snapTo: Store< |  | ||||||
|         (Feature<LineString | Polygon> & { 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<Feature[]> |  | ||||||
|     private readonly _value: Store<Loc> |  | ||||||
|     private readonly _snappedPoint: Store<any> |  | ||||||
|     private readonly _maxSnapDistance: number |  | ||||||
|     private readonly _snappedPointTags: any |  | ||||||
|     private readonly _bounds: UIEventSource<BBox> |  | ||||||
|     private readonly map: BaseUIElement & MinimapObj |  | ||||||
|     private readonly clickLocation: UIEventSource<Loc> |  | ||||||
|     private readonly _minZoom: number |  | ||||||
|     private readonly _state: { |  | ||||||
|         readonly filteredLayers: Store<FilteredLayer[]> |  | ||||||
|         readonly backgroundLayer: UIEventSource<BaseLayer> |  | ||||||
|         readonly layoutToUse: LayoutConfig |  | ||||||
|         readonly selectedElement: UIEventSource<any> |  | ||||||
|         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<LineString | Polygon> & { properties: { id: WayId } })[]> { |  | ||||||
|         const linesAndPolygon: Feature<LineString | Polygon>[] = <any>( |  | ||||||
|             features.filter((f) => f.geometry.type !== "Point") |  | ||||||
|         ) |  | ||||||
|         // Clean the features: multipolygons are split into their it's members
 |  | ||||||
|         const linestrings: (Feature<LineString | Polygon> & { properties: { id: WayId } })[] = [] |  | ||||||
|         for (const feature of linesAndPolygon) { |  | ||||||
|             if (feature.properties.id.startsWith("way")) { |  | ||||||
|                 // A normal way - we continue
 |  | ||||||
|                 linestrings.push(<any>feature) |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // We have a multipolygon, thus: a relation
 |  | ||||||
|             // Download the members
 |  | ||||||
|             const relation = await OsmObject.DownloadObjectAsync( |  | ||||||
|                 <RelationId>feature.properties.id, |  | ||||||
|                 60 * 60 |  | ||||||
|             ) |  | ||||||
|             const members: OsmWay[] = await Promise.all( |  | ||||||
|                 relation.members |  | ||||||
|                     .filter((m) => m.type === "way") |  | ||||||
|                     .map((m) => OsmObject.DownloadObjectAsync(<WayId>("way/" + m.ref), 60 * 60)) |  | ||||||
|             ) |  | ||||||
|             linestrings.push(...members.map((m) => m.asGeoJson())) |  | ||||||
|         } |  | ||||||
|         return linestrings |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     constructor(options?: { |  | ||||||
|         minZoom?: number |  | ||||||
|         mapBackground?: UIEventSource<BaseLayer> |  | ||||||
|         snapTo?: UIEventSource<Feature[]> |  | ||||||
|         renderLayerForSnappedPoint?: LayerConfig |  | ||||||
|         maxSnapDistance?: number |  | ||||||
|         snappedPointTags?: any |  | ||||||
|         requiresSnapping?: boolean |  | ||||||
|         centerLocation?: UIEventSource<Loc> |  | ||||||
|         bounds?: UIEventSource<BBox> |  | ||||||
|         state?: { |  | ||||||
|             readonly filteredLayers: Store<FilteredLayer[]> |  | ||||||
|             readonly backgroundLayer: UIEventSource<BaseLayer> |  | ||||||
|             readonly layoutToUse: LayoutConfig |  | ||||||
|             readonly selectedElement: UIEventSource<any> |  | ||||||
|             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<Loc>({ |  | ||||||
|                 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<LineString | Polygon> & { 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(<any>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<BaseLayer>(AvailableBaseLayers.osmCarto) |  | ||||||
|         this.SetClass("block h-full") |  | ||||||
| 
 |  | ||||||
|         this.clickLocation = new UIEventSource<Loc>(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<Loc> { |  | ||||||
|         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() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -21,10 +21,9 @@ | ||||||
|   import LoginButton from "../../Base/LoginButton.svelte"; |   import LoginButton from "../../Base/LoginButton.svelte"; | ||||||
|   import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"; |   import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"; | ||||||
|   import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"; |   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 { Tag } from "../../../Logic/Tags/Tag"; | ||||||
|   import type { WayId } from "../../../Models/OsmFeature"; |   import type { WayId } from "../../../Models/OsmFeature"; | ||||||
|   import { TagUtils } from "../../../Logic/Tags/TagUtils"; |  | ||||||
|   import Loading from "../../Base/Loading.svelte"; |   import Loading from "../../Base/Loading.svelte"; | ||||||
| 
 | 
 | ||||||
|   export let coordinate: { lon: number, lat: number }; |   export let coordinate: { lon: number, lat: number }; | ||||||
|  | @ -75,9 +74,16 @@ | ||||||
|     const tags: Tag[] = selectedPreset.preset.tags; |     const tags: Tag[] = selectedPreset.preset.tags; | ||||||
|     console.log("Creating new point at", location, "snapped to", snapTo, "with tags", 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", |       theme: state.layout?.id ?? "unkown", | ||||||
|       changeType: "create", |       changeType: "create", | ||||||
|       snapOnto: snapToWay  |       snapOnto: snapToWay  | ||||||
|  |  | ||||||
|  | @ -11,7 +11,6 @@ import { Translation } from "../i18n/Translation" | ||||||
| import BaseUIElement from "../BaseUIElement" | import BaseUIElement from "../BaseUIElement" | ||||||
| import Constants from "../../Models/Constants" | import Constants from "../../Models/Constants" | ||||||
| import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig" | import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig" | ||||||
| import { OsmObject } from "../../Logic/Osm/OsmObject" |  | ||||||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" | import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||||
| import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction" | import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction" | ||||||
| import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" | import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" | ||||||
|  | @ -20,12 +19,12 @@ import { RadioButton } from "../Input/RadioButton" | ||||||
| import { FixedInputElement } from "../Input/FixedInputElement" | import { FixedInputElement } from "../Input/FixedInputElement" | ||||||
| import Title from "../Base/Title" | import Title from "../Base/Title" | ||||||
| import { SubstitutedTranslation } from "../SubstitutedTranslation" | import { SubstitutedTranslation } from "../SubstitutedTranslation" | ||||||
| import TagRenderingQuestion from "./TagRenderingQuestion" |  | ||||||
| import { OsmId, OsmTags } from "../../Models/OsmFeature" | import { OsmId, OsmTags } from "../../Models/OsmFeature" | ||||||
| import { LoginToggle } from "./LoginButton" | import { LoginToggle } from "./LoginButton" | ||||||
| import { SpecialVisualizationState } from "../SpecialVisualization" | import { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
| import SvelteUIElement from "../Base/SvelteUIElement"; | import SvelteUIElement from "../Base/SvelteUIElement" | ||||||
| import TagHint from "./TagHint.svelte"; | import TagHint from "./TagHint.svelte" | ||||||
|  | import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader" | ||||||
| 
 | 
 | ||||||
| export default class DeleteWizard extends Toggle { | export default class DeleteWizard extends Toggle { | ||||||
|     /** |     /** | ||||||
|  | @ -53,6 +52,7 @@ export default class DeleteWizard extends Toggle { | ||||||
|         const deleteAbility = new DeleteabilityChecker( |         const deleteAbility = new DeleteabilityChecker( | ||||||
|             id, |             id, | ||||||
|             state.osmConnection, |             state.osmConnection, | ||||||
|  |             state.osmObjectDownloader, | ||||||
|             options.neededChangesets |             options.neededChangesets | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  | @ -227,7 +227,10 @@ export default class DeleteWizard extends Toggle { | ||||||
|                         // This is a retagging, not a deletion of any kind
 |                         // This is a retagging, not a deletion of any kind
 | ||||||
|                         return new Combine([ |                         return new Combine([ | ||||||
|                             t.explanations.retagNoOtherThemes, |                             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 { | class DeleteabilityChecker { | ||||||
|     public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean; reason: Translation }> |     public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean; reason: Translation }> | ||||||
|  |     private readonly objectDownloader: OsmObjectDownloader | ||||||
|     private readonly _id: OsmId |     private readonly _id: OsmId | ||||||
|     private readonly _allowDeletionAtChangesetCount: number |     private readonly _allowDeletionAtChangesetCount: number | ||||||
|     private readonly _osmConnection: OsmConnection |     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._id = id | ||||||
|         this._osmConnection = osmConnection |         this._osmConnection = osmConnection | ||||||
|         this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE |         this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE | ||||||
|  | @ -366,11 +376,13 @@ class DeleteabilityChecker { | ||||||
| 
 | 
 | ||||||
|                 if (allByMyself.data === null && useTheInternet) { |                 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
 |                     // 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) => |                     const hist = this.objectDownloader | ||||||
|                         versions.map((version) => |                         .DownloadHistory(id) | ||||||
|                             Number(version.tags["_last_edit:contributor:uid"]) |                         .map((versions) => | ||||||
|  |                             versions.map((version) => | ||||||
|  |                                 Number(version.tags["_last_edit:contributor:uid"]) | ||||||
|  |                             ) | ||||||
|                         ) |                         ) | ||||||
|                     ) |  | ||||||
|                     hist.addCallbackAndRunD((hist) => previousEditors.setData(hist)) |                     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
 |             // 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) |                 hasRelations.setData(rels.length > 0) | ||||||
|             }) |             }) | ||||||
| 
 | 
 | ||||||
|             OsmObject.DownloadReferencingWays(id).then((ways) => { |             this.objectDownloader.DownloadReferencingWays(id).then((ways) => { | ||||||
|                 hasWays.setData(ways.length > 0) |                 hasWays.setData(ways.length > 0) | ||||||
|             }) |             }) | ||||||
|             return true // unregister to only run once
 |             return true // unregister to only run once
 | ||||||
|  |  | ||||||
|  | @ -233,13 +233,13 @@ export default class MoveWizard extends Toggle { | ||||||
|         } else if (id.startsWith("relation")) { |         } else if (id.startsWith("relation")) { | ||||||
|             moveDisallowedReason.setData(t.isRelation) |             moveDisallowedReason.setData(t.isRelation) | ||||||
|         } else if (id.indexOf("-") < 0) { |         } else if (id.indexOf("-") < 0) { | ||||||
|             OsmObject.DownloadReferencingWays(id).then((referencing) => { |             state.osmObjectDownloader.DownloadReferencingWays(id).then((referencing) => { | ||||||
|                 if (referencing.length > 0) { |                 if (referencing.length > 0) { | ||||||
|                     console.log("Got a referencing way, move not allowed") |                     console.log("Got a referencing way, move not allowed") | ||||||
|                     moveDisallowedReason.setData(t.partOfAWay) |                     moveDisallowedReason.setData(t.partOfAWay) | ||||||
|                 } |                 } | ||||||
|             }) |             }) | ||||||
|             OsmObject.DownloadReferencingRelations(id).then((partOf) => { |             state.osmObjectDownloader.DownloadReferencingRelations(id).then((partOf) => { | ||||||
|                 if (partOf.length > 0) { |                 if (partOf.length > 0) { | ||||||
|                     moveDisallowedReason.setData(t.partOfRelation) |                     moveDisallowedReason.setData(t.partOfRelation) | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  | @ -12,13 +12,13 @@ import { VariableUiElement } from "../Base/VariableUIElement" | ||||||
| import { LoginToggle } from "./LoginButton" | import { LoginToggle } from "./LoginButton" | ||||||
| import SvelteUIElement from "../Base/SvelteUIElement" | import SvelteUIElement from "../Base/SvelteUIElement" | ||||||
| import WaySplitMap from "../BigComponents/WaySplitMap.svelte" | import WaySplitMap from "../BigComponents/WaySplitMap.svelte" | ||||||
| import { OsmObject } from "../../Logic/Osm/OsmObject" |  | ||||||
| import { Feature, Point } from "geojson" | import { Feature, Point } from "geojson" | ||||||
| import { WayId } from "../../Models/OsmFeature" | import { WayId } from "../../Models/OsmFeature" | ||||||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" | import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||||
| import { Changes } from "../../Logic/Osm/Changes" | import { Changes } from "../../Logic/Osm/Changes" | ||||||
| import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource" | import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
|  | import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader" | ||||||
| 
 | 
 | ||||||
| export default class SplitRoadWizard extends Combine { | export default class SplitRoadWizard extends Combine { | ||||||
|     public dialogIsOpened: UIEventSource<boolean> |     public dialogIsOpened: UIEventSource<boolean> | ||||||
|  | @ -34,6 +34,7 @@ export default class SplitRoadWizard extends Combine { | ||||||
|         state: { |         state: { | ||||||
|             layout?: LayoutConfig |             layout?: LayoutConfig | ||||||
|             osmConnection?: OsmConnection |             osmConnection?: OsmConnection | ||||||
|  |             osmObjectDownloader?: OsmObjectDownloader | ||||||
|             changes?: Changes |             changes?: Changes | ||||||
|             indexedFeatures?: IndexedFeatureSource |             indexedFeatures?: IndexedFeatureSource | ||||||
|             selectedElement?: UIEventSource<Feature> |             selectedElement?: UIEventSource<Feature> | ||||||
|  | @ -52,7 +53,15 @@ export default class SplitRoadWizard extends Combine { | ||||||
|         const leafletMap = new UIEventSource<BaseUIElement>(undefined) |         const leafletMap = new UIEventSource<BaseUIElement>(undefined) | ||||||
| 
 | 
 | ||||||
|         function initMap() { |         function initMap() { | ||||||
|             SplitRoadWizard.setupMapComponent(id, splitPoints).then((mapComponent) => |             ;(async function ( | ||||||
|  |                 id: WayId, | ||||||
|  |                 splitPoints: UIEventSource<Feature[]> | ||||||
|  |             ): Promise<BaseUIElement> { | ||||||
|  |                 return new SvelteUIElement(WaySplitMap, { | ||||||
|  |                     osmWay: await state.osmObjectDownloader.DownloadObjectAsync(id), | ||||||
|  |                     splitPoints, | ||||||
|  |                 }) | ||||||
|  |             })(id, splitPoints).then((mapComponent) => | ||||||
|                 leafletMap.setData(mapComponent.SetClass("w-full h-80")) |                 leafletMap.setData(mapComponent.SetClass("w-full h-80")) | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|  | @ -132,15 +141,4 @@ export default class SplitRoadWizard extends Combine { | ||||||
|             self.ScrollIntoView() |             self.ScrollIntoView() | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     private static async setupMapComponent( |  | ||||||
|         id: WayId, |  | ||||||
|         splitPoints: UIEventSource<Feature[]> |  | ||||||
|     ): Promise<BaseUIElement> { |  | ||||||
|         const osmWay = await OsmObject.DownloadObjectAsync(id) |  | ||||||
|         return new SvelteUIElement(WaySplitMap, { |  | ||||||
|             osmWay, |  | ||||||
|             splitPoints, |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexe | ||||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||||
| import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; | import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; | ||||||
| import { MenuState } from "../Models/MenuState"; | import { MenuState } from "../Models/MenuState"; | ||||||
|  | import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The state needed to render a special Visualisation. |  * The state needed to render a special Visualisation. | ||||||
|  | @ -39,6 +40,7 @@ export interface SpecialVisualizationState { | ||||||
|     readonly featureSwitchUserbadge: Store<boolean> |     readonly featureSwitchUserbadge: Store<boolean> | ||||||
|     readonly featureSwitchIsTesting: Store<boolean> |     readonly featureSwitchIsTesting: Store<boolean> | ||||||
|     readonly changes: Changes |     readonly changes: Changes | ||||||
|  |     readonly osmObjectDownloader: OsmObjectDownloader | ||||||
|     /** |     /** | ||||||
|      * State of the main map |      * State of the main map | ||||||
|      */ |      */ | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import ScriptUtils from "./ScriptUtils" | import ScriptUtils from "./ScriptUtils" | ||||||
| import { appendFileSync, readFileSync, writeFileSync } from "fs" | import { appendFileSync, readFileSync, writeFileSync } from "fs" | ||||||
| import { OsmObject } from "../Logic/Osm/OsmObject" | import { OsmObject } from "../Logic/Osm/OsmObject" | ||||||
|  | import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"; | ||||||
| 
 | 
 | ||||||
| ScriptUtils.fixUtils() | ScriptUtils.fixUtils() | ||||||
| 
 | 
 | ||||||
|  | @ -17,7 +18,7 @@ const ids = JSON.parse(readFileSync("export.geojson", "utf-8")).features.map( | ||||||
| ) | ) | ||||||
| console.log(ids) | console.log(ids) | ||||||
| ids.map((id) => | ids.map((id) => | ||||||
|     OsmObject.DownloadReferencingRelations(id).then((relations) => { |     new OsmObjectDownloader().DownloadReferencingRelations(id).then((relations) => { | ||||||
|         console.log(relations) |         console.log(relations) | ||||||
|         const changeparts = relations |         const changeparts = relations | ||||||
|             .filter( |             .filter( | ||||||
|  |  | ||||||
|  | @ -1,11 +1,14 @@ | ||||||
| import { Utils } from "../../../../Utils" | import { Utils } from "../../../../Utils" | ||||||
| import { OsmObject, OsmRelation } from "../../../../Logic/Osm/OsmObject" | import { OsmRelation } from "../../../../Logic/Osm/OsmObject" | ||||||
| import { | import { | ||||||
|     InPlaceReplacedmentRTSH, |     InPlaceReplacedmentRTSH, | ||||||
|     TurnRestrictionRSH, |     TurnRestrictionRSH, | ||||||
| } from "../../../../Logic/Osm/Actions/RelationSplitHandler" | } from "../../../../Logic/Osm/Actions/RelationSplitHandler" | ||||||
| import { Changes } from "../../../../Logic/Osm/Changes" | import { Changes } from "../../../../Logic/Osm/Changes" | ||||||
| import { describe, expect, it } from "vitest" | 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", () => { | describe("RelationSplitHandler", () => { | ||||||
|     Utils.injectJsonDownloadForTests("https://www.openstreetmap.org/api/0.6/node/1124134958/ways", { |     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 () => { |     it("should split all cycling relation (split 295132739)", async () => { | ||||||
|         // Lets mimic a split action of https://www.openstreetmap.org/way/295132739
 |         // Lets mimic a split action of https://www.openstreetmap.org/way/295132739
 | ||||||
| 
 | 
 | ||||||
|  |         const downloader = new OsmObjectDownloader() | ||||||
|         const relation: OsmRelation = <OsmRelation>( |         const relation: OsmRelation = <OsmRelation>( | ||||||
|             await OsmObject.DownloadObjectAsync("relation/9572808") |             await downloader.DownloadObjectAsync("relation/9572808") | ||||||
|         ) |         ) | ||||||
|         const originalNodeIds = [ |         const originalNodeIds = [ | ||||||
|             5273988967, 170497153, 1507524582, 4524321710, 170497155, 170497157, 170497158, |             5273988967, 170497153, 1507524582, 4524321710, 170497155, 170497157, 170497158, | ||||||
|  | @ -645,9 +649,13 @@ describe("RelationSplitHandler", () => { | ||||||
|                 originalNodes: originalNodeIds, |                 originalNodes: originalNodeIds, | ||||||
|                 allWaysNodesInOrder: withSplit, |                 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 allIds = changeDescription[0].changes["members"].map((m) => m.ref).join(",") | ||||||
|         const expected = "687866206,295132739,-1,690497698" |         const expected = "687866206,295132739,-1,690497698" | ||||||
|         // "didn't find the expected order of ids in the relation to test"
 |         // "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 () => { |     it("should split turn restrictions (split of https://www.openstreetmap.org/way/143298912)", async () => { | ||||||
|  |         const downloader = new OsmObjectDownloader() | ||||||
|         const relation: OsmRelation = <OsmRelation>( |         const relation: OsmRelation = <OsmRelation>( | ||||||
|             await OsmObject.DownloadObjectAsync("relation/4374576") |             await downloader.DownloadObjectAsync("relation/4374576") | ||||||
|         ) |         ) | ||||||
|         const originalNodeIds = [ |         const originalNodeIds = [ | ||||||
|             1407529979, 1974988033, 3250129361, 1634435395, 8493044168, 875668688, 1634435396, |             1407529979, 1974988033, 3250129361, 1634435395, 8493044168, 875668688, 1634435396, | ||||||
|  | @ -695,9 +704,13 @@ describe("RelationSplitHandler", () => { | ||||||
|                 originalNodes: originalNodeIds, |                 originalNodes: originalNodeIds, | ||||||
|                 allWaysNodesInOrder: withSplit, |                 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"] |         const allIds = changeDescription[0].changes["members"] | ||||||
|             .map((m) => m.type + "/" + m.ref + "-->" + m.role) |             .map((m) => m.type + "/" + m.ref + "-->" + m.role) | ||||||
|             .join(",") |             .join(",") | ||||||
|  | @ -713,9 +726,15 @@ describe("RelationSplitHandler", () => { | ||||||
|                 originalNodes: originalNodeIds, |                 originalNodes: originalNodeIds, | ||||||
|                 allWaysNodesInOrder: withSplit, |                 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) |         expect(changesReverse.length).toEqual(0) | ||||||
|     }) |     }) | ||||||
| }) | }) | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ import { Utils } from "../../../Utils" | ||||||
| import ScriptUtils from "../../../scripts/ScriptUtils" | import ScriptUtils from "../../../scripts/ScriptUtils" | ||||||
| import { readFileSync } from "fs" | import { readFileSync } from "fs" | ||||||
| import { describe, expect, it } from "vitest" | import { describe, expect, it } from "vitest" | ||||||
|  | import OsmObjectDownloader from "../../../Logic/Osm/OsmObjectDownloader" | ||||||
| 
 | 
 | ||||||
| describe("OsmObject", () => { | describe("OsmObject", () => { | ||||||
|     describe("download referencing ways", () => { |     describe("download referencing ways", () => { | ||||||
|  | @ -79,7 +80,8 @@ describe("OsmObject", () => { | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         it("should download referencing ways", async () => { |         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).toBeDefined() | ||||||
|             expect(ways).toHaveLength(4) |             expect(ways).toHaveLength(4) | ||||||
|         }) |         }) | ||||||
|  | @ -90,7 +92,7 @@ describe("OsmObject", () => { | ||||||
|                 "https://www.openstreetmap.org/api/0.6/relation/5759328/full", |                 "https://www.openstreetmap.org/api/0.6/relation/5759328/full", | ||||||
|                 JSON.parse(readFileSync("./test/data/relation_5759328.json", { encoding: "utf-8" })) |                 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() |             const geojson = r.asGeoJson() | ||||||
|             expect(geojson.geometry.type).toBe("MultiPolygon") |             expect(geojson.geometry.type).toBe("MultiPolygon") | ||||||
|         }) |         }) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue