| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  | import { FeatureSource } from "../FeatureSource" | 
					
						
							|  |  |  | import { Store, UIEventSource } from "../../UIEventSource" | 
					
						
							| 
									
										
										
										
											2023-03-29 17:21:20 +02:00
										 |  |  | import { Feature, Point } from "geojson" | 
					
						
							|  |  |  | import { GeoOperations } from "../../GeoOperations" | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  | import { BBox } from "../../BBox" | 
					
						
							| 
									
										
										
										
											2023-03-29 17:21:20 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | export interface SnappingOptions { | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * If the distance is bigger then this amount, don't snap. | 
					
						
							|  |  |  |      * In meter | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |     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 }> | 
					
						
							| 
									
										
										
										
											2023-04-20 01:52:23 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * If the projected point is within `reusePointWithin`-meter of an already existing point | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     reusePointWithin?: number | 
					
						
							| 
									
										
										
										
											2023-03-29 17:21:20 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export default class SnappingFeatureSource implements FeatureSource { | 
					
						
							|  |  |  |     public readonly features: Store<Feature<Point>[]> | 
					
						
							| 
									
										
										
										
											2023-04-20 01:52:23 +02:00
										 |  |  |     /*Contains the id of the way it snapped to*/ | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |     public readonly snappedTo: Store<string> | 
					
						
							| 
									
										
										
										
											2023-04-20 01:52:23 +02:00
										 |  |  |     private readonly _snappedTo: UIEventSource<string> | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-29 17:21:20 +02:00
										 |  |  |     constructor( | 
					
						
							|  |  |  |         snapTo: FeatureSource, | 
					
						
							|  |  |  |         location: Store<{ lon: number; lat: number }>, | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |         options: SnappingOptions | 
					
						
							| 
									
										
										
										
											2023-03-29 17:21:20 +02:00
										 |  |  |     ) { | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |         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] | 
					
						
							|  |  |  |             ) | 
					
						
							| 
									
										
										
										
											2023-03-29 17:21:20 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |         this.features = location.mapD( | 
					
						
							| 
									
										
										
										
											2023-03-29 17:21:20 +02:00
										 |  |  |             ({ lon, lat }) => { | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |                 const features = simplifiedFeatures.data | 
					
						
							| 
									
										
										
										
											2023-03-29 17:21:20 +02:00
										 |  |  |                 const loc: [number, number] = [lon, lat] | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |                 const maxDistance = (options?.maxDistance ?? 1000) / 1000 | 
					
						
							| 
									
										
										
										
											2023-03-29 17:21:20 +02:00
										 |  |  |                 let bestSnap: Feature<Point, { "snapped-to": string; dist: number }> = undefined | 
					
						
							|  |  |  |                 for (const feature of features) { | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |                     if (feature.geometry.type !== "LineString") { | 
					
						
							|  |  |  |                         // TODO handle Polygons with holes
 | 
					
						
							|  |  |  |                         continue | 
					
						
							|  |  |  |                     } | 
					
						
							| 
									
										
										
										
											2023-03-29 17:21:20 +02:00
										 |  |  |                     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 | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |                 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] | 
					
						
							| 
									
										
										
										
											2023-03-29 17:21:20 +02:00
										 |  |  |             }, | 
					
						
							|  |  |  |             [snapTo.features] | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |