forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			100 lines
		
	
	
	
		
			3.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			100 lines
		
	
	
	
		
			3.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { FeatureSource } from "../FeatureSource"
 | |
| import { Store, UIEventSource } from "../../UIEventSource"
 | |
| import { Feature, Point } from "geojson"
 | |
| import { GeoOperations } from "../../GeoOperations"
 | |
| import { BBox } from "../../BBox"
 | |
| 
 | |
| export interface SnappingOptions {
 | |
|     /**
 | |
|      * If the distance is bigger then this amount, don't snap.
 | |
|      * In meter
 | |
|      */
 | |
|     maxDistance: number
 | |
| 
 | |
|     allowUnsnapped?: false | boolean
 | |
| 
 | |
|     /**
 | |
|      * The snapped-to way will be written into this
 | |
|      */
 | |
|     snappedTo?: UIEventSource<string>
 | |
| 
 | |
|     /**
 | |
|      * The resulting snap coordinates will be written into this UIEventSource
 | |
|      */
 | |
|     snapLocation?: UIEventSource<{ lon: number; lat: number }>
 | |
| }
 | |
| 
 | |
| export default class SnappingFeatureSource implements FeatureSource {
 | |
|     public readonly features: Store<Feature<Point>[]>
 | |
| 
 | |
|     private readonly _snappedTo: UIEventSource<string>
 | |
|     public readonly snappedTo: Store<string>
 | |
| 
 | |
|     constructor(
 | |
|         snapTo: FeatureSource,
 | |
|         location: Store<{ lon: number; lat: number }>,
 | |
|         options: SnappingOptions
 | |
|     ) {
 | |
|         const maxDistance = options?.maxDistance
 | |
|         this._snappedTo = options.snappedTo ?? new UIEventSource<string>(undefined)
 | |
|         this.snappedTo = this._snappedTo
 | |
|         const simplifiedFeatures = snapTo.features
 | |
|             .mapD((features) =>
 | |
|                 features
 | |
|                     .filter((feature) => feature.geometry.type !== "Point")
 | |
|                     .map((f) => GeoOperations.forceLineString(<any>f))
 | |
|             )
 | |
|             .map(
 | |
|                 (features) => {
 | |
|                     const { lon, lat } = location.data
 | |
|                     const loc: [number, number] = [lon, lat]
 | |
|                     return features.filter((f) => BBox.get(f).isNearby(loc, maxDistance))
 | |
|                 },
 | |
|                 [location]
 | |
|             )
 | |
| 
 | |
|         this.features = location.mapD(
 | |
|             ({ lon, lat }) => {
 | |
|                 const features = simplifiedFeatures.data
 | |
|                 const loc: [number, number] = [lon, lat]
 | |
|                 const maxDistance = (options?.maxDistance ?? 1000) / 1000
 | |
|                 let bestSnap: Feature<Point, { "snapped-to": string; dist: number }> = undefined
 | |
|                 for (const feature of features) {
 | |
|                     if (feature.geometry.type !== "LineString") {
 | |
|                         // TODO handle Polygons with holes
 | |
|                         continue
 | |
|                     }
 | |
|                     const snapped = GeoOperations.nearestPoint(<any>feature, loc)
 | |
|                     if (snapped.properties.dist > maxDistance) {
 | |
|                         continue
 | |
|                     }
 | |
|                     if (
 | |
|                         bestSnap === undefined ||
 | |
|                         bestSnap.properties.dist > snapped.properties.dist
 | |
|                     ) {
 | |
|                         snapped.properties["snapped-to"] = feature.properties.id
 | |
|                         bestSnap = <any>snapped
 | |
|                     }
 | |
|                 }
 | |
|                 this._snappedTo.setData(bestSnap?.properties?.["snapped-to"])
 | |
|                 if (bestSnap === undefined && options?.allowUnsnapped) {
 | |
|                     bestSnap = {
 | |
|                         type: "Feature",
 | |
|                         geometry: {
 | |
|                             type: "Point",
 | |
|                             coordinates: [lon, lat],
 | |
|                         },
 | |
|                         properties: {
 | |
|                             "snapped-to": undefined,
 | |
|                             dist: -1,
 | |
|                         },
 | |
|                     }
 | |
|                 }
 | |
|                 const c = bestSnap.geometry.coordinates
 | |
|                 options?.snapLocation?.setData({ lon: c[0], lat: c[1] })
 | |
|                 return [bestSnap]
 | |
|             },
 | |
|             [snapTo.features]
 | |
|         )
 | |
|     }
 | |
| }
 |