Let locationInput deal with multipolygon correctly

This commit is contained in:
pietervdvn 2022-09-21 02:22:22 +02:00
parent 28fc81c96f
commit 603fecb107

View file

@ -19,6 +19,9 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import FilteredLayer from "../../Models/FilteredLayer"; import FilteredLayer from "../../Models/FilteredLayer";
import {ElementStorage} from "../../Logic/ElementStorage"; import {ElementStorage} from "../../Logic/ElementStorage";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import {RelationId, WayId} from "../../Models/OsmFeature";
import {Feature, LineString, Polygon} from "geojson";
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
export default class LocationInput export default class LocationInput
extends BaseUIElement extends BaseUIElement
@ -29,7 +32,7 @@ export default class LocationInput
true true
) )
public readonly snappedOnto: UIEventSource<any> = new UIEventSource<any>(undefined) public readonly snappedOnto: UIEventSource<Feature & { properties : { id : WayId} }> = new UIEventSource(undefined)
public readonly _matching_layer: LayerConfig public readonly _matching_layer: LayerConfig
public readonly leafletMap: UIEventSource<any> public readonly leafletMap: UIEventSource<any>
public readonly bounds public readonly bounds
@ -40,7 +43,13 @@ export default class LocationInput
* The features to which the input should be snapped * The features to which the input should be snapped
* @private * @private
*/ */
private readonly _snapTo: Store<{ feature: any }[]> 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: Feature}[]>
private readonly _value: Store<Loc> private readonly _value: Store<Loc>
private readonly _snappedPoint: Store<any> private readonly _snappedPoint: Store<any>
private readonly _maxSnapDistance: number private readonly _maxSnapDistance: number
@ -57,10 +66,41 @@ export default class LocationInput
readonly allElements: ElementStorage 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?: { constructor(options?: {
minZoom?: number minZoom?: number
mapBackground?: UIEventSource<BaseLayer> mapBackground?: UIEventSource<BaseLayer>
snapTo?: UIEventSource<{ feature: any }[]> snapTo?: UIEventSource<{ feature: Feature }[]>
maxSnapDistance?: number maxSnapDistance?: number
snappedPointTags?: any snappedPointTags?: any
requiresSnapping?: boolean requiresSnapping?: boolean
@ -75,9 +115,8 @@ export default class LocationInput
} }
}) { }) {
super() super()
this._snapTo = options?.snapTo?.map((features) => this._snapToRaw = options?.snapTo?.map(feats => feats.filter(f => f.feature.geometry.type !== "Point"))
features?.filter((feat) => feat.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._maxSnapDistance = options?.maxSnapDistance
this._centerLocation = options?.centerLocation ?? new UIEventSource<Loc>({ this._centerLocation = options?.centerLocation ?? new UIEventSource<Loc>({
lat: 0, lon: 0, zoom: 0 lat: 0, lon: 0, zoom: 0
@ -105,36 +144,39 @@ export default class LocationInput
this._matching_layer = LocationInput.matchLayer this._matching_layer = 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( this._snappedPoint = this._centerLocation.map(
(loc) => { (loc) => {
if (loc === undefined) { if (loc === undefined) {
return undefined return undefined
} }
// We reproject the location onto every 'snap-to-feature' and select the closest // We reproject the location onto every 'snap-to-feature' and select the closest
let min = undefined let min = undefined
let matchedWay = undefined let matchedWay: Feature<LineString | Polygon> & {properties : {id : WayId}} = undefined
for (const feature of self._snapTo.data ?? []) { for (const feature of self._snapTo.data ?? []) {
try { try {
const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [ const nearestPointOnLine = GeoOperations.nearestPoint(feature, [
loc.lon, loc.lon,
loc.lat, loc.lat,
]) ])
if (min === undefined) { if (min === undefined) {
min = nearestPointOnLine min = nearestPointOnLine
matchedWay = feature.feature matchedWay = feature
continue continue
} }
if (min.properties.dist > nearestPointOnLine.properties.dist) { if (min.properties.dist > nearestPointOnLine.properties.dist) {
min = nearestPointOnLine min = nearestPointOnLine
matchedWay = feature.feature matchedWay = feature
} }
} catch (e) { } catch (e) {
console.log( console.log(
"Snapping to a nearest point failed for ", "Snapping to a nearest point failed for ",
feature.feature, feature,
"due to ", "due to ",
e e
) )
@ -145,6 +187,7 @@ export default class LocationInput
if (options?.requiresSnapping) { if (options?.requiresSnapping) {
return undefined return undefined
} else { } else {
// No match found - the original coordinates are returned as is
return { return {
type: "Feature", type: "Feature",
properties: options?.snappedPointTags ?? min.properties, properties: options?.snappedPointTags ?? min.properties,
@ -153,7 +196,13 @@ export default class LocationInput
} }
} }
min.properties = options?.snappedPointTags ?? min.properties min.properties = options?.snappedPointTags ?? min.properties
self.snappedOnto.setData(matchedWay) if(matchedWay.properties.id.startsWith("relation/")){
// We matched a relation instead of a way
console.log("Snapping onto a relation. The relation is", matchedWay)
}
self.snappedOnto.setData(<any> matchedWay)
return min return min
}, },
[this._snapTo] [this._snapTo]
@ -216,11 +265,11 @@ export default class LocationInput
this.clickLocation.addCallbackAndRunD((location) => this.clickLocation.addCallbackAndRunD((location) =>
this._centerLocation.setData(location) this._centerLocation.setData(location)
) )
if (this._snapTo !== undefined) { if (this._snapToRaw !== undefined) {
// Show the lines to snap to // Show the lines to snap to
console.log("Constructing the snap-to layer", this._snapTo) console.log("Constructing the snap-to layer", this._snapToRaw)
new ShowDataMultiLayer({ new ShowDataMultiLayer({
features: StaticFeatureSource.fromDateless(this._snapTo), features: StaticFeatureSource.fromDateless(this._snapToRaw),
zoomToFeatures: false, zoomToFeatures: false,
leafletMap: this.map.leafletMap, leafletMap: this.map.leafletMap,
layers: this._state.filteredLayers, layers: this._state.filteredLayers,