diff --git a/src/Logic/FeatureSource/Sources/SnappingFeatureSource.ts b/src/Logic/FeatureSource/Sources/SnappingFeatureSource.ts index 709f1f8c1c..4754ef248f 100644 --- a/src/Logic/FeatureSource/Sources/SnappingFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/SnappingFeatureSource.ts @@ -3,6 +3,8 @@ import { Store, UIEventSource } from "../../UIEventSource" import { Feature, Point } from "geojson" import { GeoOperations } from "../../GeoOperations" import { BBox } from "../../BBox" +import { RelationId } from "../../../Models/OsmFeature" +import OsmObjectDownloader from "../../Osm/OsmObjectDownloader" export interface SnappingOptions { /** @@ -33,13 +35,15 @@ export interface SnappingOptions { } export default class SnappingFeatureSource - implements FeatureSource> -{ + implements FeatureSource> { public readonly features: Store<[Feature]> /*Contains the id of the way it snapped to*/ public readonly snappedTo: Store private readonly _snappedTo: UIEventSource + // private static readonly downloadedRelations: UIEventSource> = new UIEventSource(new Map()) + private static readonly downloadedRelationMembers: UIEventSource = new UIEventSource([]) + constructor( snapTo: FeatureSource, location: Store<{ lon: number; lat: number }>, @@ -50,9 +54,9 @@ export default class SnappingFeatureSource this.snappedTo = this._snappedTo const simplifiedFeatures = snapTo.features .mapD((features) => - features + [].concat(...features .filter((feature) => feature.geometry.type !== "Point") - .map((f) => GeoOperations.forceLineString(f)) + .map((f) => GeoOperations.forceLineString(f))) ) .map( (features) => { @@ -63,9 +67,10 @@ export default class SnappingFeatureSource [location] ) + this.features = location.mapD( ({ lon, lat }) => { - const features = simplifiedFeatures.data + const features = simplifiedFeatures.data.concat(...SnappingFeatureSource.downloadedRelationMembers.data) const loc: [number, number] = [lon, lat] const maxDistance = (options?.maxDistance ?? 1000) / 1000 let bestSnap: Feature = undefined @@ -74,7 +79,12 @@ export default class SnappingFeatureSource // TODO handle Polygons with holes continue } - const snapped = GeoOperations.nearestPoint(feature, loc) + const snapped: Feature = GeoOperations.nearestPoint(feature, loc) if (snapped.properties.dist > maxDistance) { continue } @@ -82,8 +92,20 @@ export default class SnappingFeatureSource bestSnap === undefined || bestSnap.properties.dist > snapped.properties.dist ) { - snapped.properties["snapped-to"] = feature.properties.id - bestSnap = snapped + const id = feature.properties.id + if (id.startsWith("relation/")) { + /** + * This is a bit of dirty code: + * if we find a relation, we'll start to download it and the members. + * The downloaded members will then be used to snap against as well upon a following iteration + */ + SnappingFeatureSource.download(id) + continue + } + bestSnap = { + ...snapped, + properties: { ...snapped.properties, "snapped-to": id } + } } } this._snappedTo.setData(bestSnap?.properties?.["snapped-to"]) @@ -92,19 +114,47 @@ export default class SnappingFeatureSource type: "Feature", geometry: { type: "Point", - coordinates: [lon, lat], + coordinates: [lon, lat] }, properties: { "snapped-to": undefined, - dist: -1, - }, + dist: -1 + } } } const c = bestSnap.geometry.coordinates options?.snapLocation?.setData({ lon: c[0], lat: c[1] }) return [bestSnap] }, - [snapTo.features] + [snapTo.features, SnappingFeatureSource.downloadedRelationMembers] ) } + + private static _downloader = new OsmObjectDownloader() + private static _downloadedRelations = new Set() + + private static async download(id: RelationId) { + if (SnappingFeatureSource._downloadedRelations.has(id)) { + return + } + SnappingFeatureSource._downloadedRelations.add(id) + const rel = await SnappingFeatureSource._downloader.DownloadObjectAsync(id, 60 * 24) + if (rel === "deleted") { + return + } + for (const member of rel.members) { + if (member.type !== "way") { + continue + } + if (member.role !== "outer" && member.role !== "inner") { + continue + } + const way = await SnappingFeatureSource._downloader.DownloadObjectAsync(member.type + "/" + member.ref) + if (way === "deleted") { + continue + } + SnappingFeatureSource.downloadedRelationMembers.data.push(...GeoOperations.forceLineString(way.asGeoJson())) + } + SnappingFeatureSource.downloadedRelationMembers.ping() + } } diff --git a/src/Logic/GeoOperations.ts b/src/Logic/GeoOperations.ts index 71e0ff2118..732b937971 100644 --- a/src/Logic/GeoOperations.ts +++ b/src/Logic/GeoOperations.ts @@ -10,11 +10,12 @@ import { MultiPolygon, Point, Polygon, - Position, + Position } from "geojson" import { Tiles } from "../Models/TileRange" import { Utils } from "../Utils" -;("use strict") + +("use strict") export class GeoOperations { private static readonly _earthRadius = 6378137 @@ -28,7 +29,7 @@ export class GeoOperations { "behind", "sharp_left", "left", - "slight_left", + "slight_left" ] as const private static reverseBearing = { N: 0, @@ -46,7 +47,7 @@ export class GeoOperations { W: 270, WNW: 292.5, NW: 315, - NNW: 337.5, + NNW: 337.5 } /** @@ -308,7 +309,7 @@ export class GeoOperations { bufferSizeInMeter: number ): Feature | FeatureCollection { return turf.buffer(feature, bufferSizeInMeter / 1000, { - units: "kilometers", + units: "kilometers" }) } @@ -324,9 +325,9 @@ export class GeoOperations { [lon0, lat], [lon0, lat0], [lon, lat0], - [lon, lat], - ], - }, + [lon, lat] + ] + } } } @@ -356,40 +357,47 @@ export class GeoOperations { * Mostly used as helper for 'nearestPoint' * @param way */ - public static forceLineString(way: Feature): Feature + public static forceLineString(way: Feature): Feature[] public static forceLineString( way: Feature - ): Feature + ): Feature[] public static forceLineString( way: Feature - ): Feature { + ): Feature[] { if (way.geometry.type === "Polygon") { - return >{ - type: "Feature", - geometry: { - type: "LineString", - coordinates: way.geometry.coordinates[0], - }, - properties: way.properties, - } + const poly: Feature = >way + return poly.geometry.coordinates.map((linestringCoors, i) => { + return >{ + type: "Feature", + geometry: { + type: "LineString", + coordinates: linestringCoors + }, + properties: way.properties + } + }) } if (way.geometry.type === "MultiPolygon") { - return >{ - type: "Feature", - geometry: { - type: "MultiLineString", - coordinates: way.geometry.coordinates[0], - }, - properties: way.properties, - } + const mpoly: Feature = >way + + return [].concat(...mpoly.geometry.coordinates.map(linestrings => + [].concat(...linestrings.map(linestring => + >{ + type: "Feature", + geometry: { + type: "LineString", + coordinates: linestring + }, + properties: way.properties + })))) } if (way.geometry.type === "LineString") { - return >way + return [>way] } if (way.geometry.type === "MultiLineString") { - return >way + return [>way] } throw "Invalid geometry to create a way from this" } @@ -466,7 +474,7 @@ export class GeoOperations { const lon = lonLat[0] const lat = lonLat[1] const x = (180 * lon) / GeoOperations._originShift - let y = (180 * lat) / GeoOperations._originShift + let y = (180 * lat) / GeoOperations._originShiftcons y = (180 / Math.PI) * (2 * Math.atan(Math.exp((y * Math.PI) / 180)) - Math.PI / 2) return [x, y] } @@ -560,7 +568,7 @@ export class GeoOperations { } const properties = { ...f.properties, - id, + id } intersectionPart.properties = properties newFeatures.push(intersectionPart) @@ -592,8 +600,8 @@ export class GeoOperations { properties: {}, geometry: { type: "Point", - coordinates: p, - }, + coordinates: p + } } ) } @@ -609,7 +617,7 @@ export class GeoOperations { trackPoints.push(trkpt) } const header = - '' + "" return ( header + "\n" + @@ -648,7 +656,7 @@ export class GeoOperations { trackPoints.push(trkpt) } const header = - '' + "" return ( header + "\n" + @@ -674,7 +682,7 @@ export class GeoOperations { const copy = { ...feature, - geometry: { ...feature.geometry }, + geometry: { ...feature.geometry } } let coordinates: [number, number][] if (feature.geometry.type === "LineString") { @@ -732,8 +740,8 @@ export class GeoOperations { type: "Feature", geometry: { type: "LineString", - coordinates: [a, b], - }, + coordinates: [a, b] + } }, distanceMeter, { units: "meters" } @@ -780,8 +788,8 @@ export class GeoOperations { type: "Feature", geometry: { type: "Polygon", - coordinates, - }, + coordinates + } } ) return !polygons.some((polygon) => !booleanWithin(polygon, possiblyEnclosingFeature)) @@ -860,8 +868,8 @@ export class GeoOperations { type: "Feature", properties: { ...toSplit.properties }, geometry: boundary.geometry, - bbox: boundary.bbox, - }, + bbox: boundary.bbox + } ] } return [] @@ -959,8 +967,8 @@ export class GeoOperations { properties: p.properties, geometry: { type: "LineString", - coordinates: p.geometry.coordinates[0], - }, + coordinates: p.geometry.coordinates[0] + } } } @@ -988,7 +996,7 @@ export class GeoOperations { console.debug("Splitting way", feature.properties.id) result.push({ ...feature, - geometry: { ...feature.geometry, coordinates: coors.slice(i + 1) }, + geometry: { ...feature.geometry, coordinates: coors.slice(i + 1) } }) coors = coors.slice(0, i + 1) break @@ -997,7 +1005,7 @@ export class GeoOperations { } result.push({ ...feature, - geometry: { ...feature.geometry, coordinates: coors }, + geometry: { ...feature.geometry, coordinates: coors } }) } } @@ -1171,8 +1179,8 @@ export class GeoOperations { properties: multiLineStringFeature.properties, geometry: { type: "LineString", - coordinates: coors[0], - }, + coordinates: coors[0] + } } } return { @@ -1180,8 +1188,8 @@ export class GeoOperations { properties: multiLineStringFeature.properties, geometry: { type: "MultiLineString", - coordinates: coors, - }, + coordinates: coors + } } } @@ -1334,7 +1342,7 @@ export class GeoOperations { const intersection = turf.intersect( turf.featureCollection([ turf.truncate(feature), - turf.truncate(otherFeature), + turf.truncate(otherFeature) ]) ) if (intersection == null) { diff --git a/src/Logic/Osm/OsmObjectDownloader.ts b/src/Logic/Osm/OsmObjectDownloader.ts index 9b1644dbb7..45c3fa70dd 100644 --- a/src/Logic/Osm/OsmObjectDownloader.ts +++ b/src/Logic/Osm/OsmObjectDownloader.ts @@ -40,7 +40,7 @@ export default class OsmObjectDownloader { async DownloadObjectAsync( id: RelationId, maxCacheAgeInSecs?: number - ): Promise + ): Promise async DownloadObjectAsync(id: OsmId, maxCacheAgeInSecs?: number): Promise diff --git a/src/UI/BigComponents/WaySplitMap.svelte b/src/UI/BigComponents/WaySplitMap.svelte index 8e82fc3155..021e3cdb36 100644 --- a/src/UI/BigComponents/WaySplitMap.svelte +++ b/src/UI/BigComponents/WaySplitMap.svelte @@ -61,7 +61,7 @@ let map: UIEventSource = new UIEventSource(undefined) let adaptor = new MapLibreAdaptor(map, mapProperties) - let wayGeojson: Feature = GeoOperations.forceLineString(osmWay.asGeoJson()) + let wayGeojson: Feature = >osmWay.asGeoJson() adaptor.location.setData(GeoOperations.centerpointCoordinatesObj(wayGeojson)) adaptor.bounds.setData(BBox.get(wayGeojson).pad(2)) adaptor.maxbounds.setData(BBox.get(wayGeojson).pad(2))