forked from MapComplete/MapComplete
		
	reset to previous commit
This commit is contained in:
		
							parent
							
								
									d5b614fc44
								
							
						
					
					
						commit
						196d40084d
					
				
					 90 changed files with 4953 additions and 1922 deletions
				
			
		|  | @ -1,11 +1,12 @@ | |||
| import * as editorlayerindex from "../../assets/editor-layer-index.json" | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import * as L from "leaflet"; | ||||
| import {TileLayer} from "leaflet"; | ||||
| import * as X from "leaflet-providers"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {GeoOperations} from "../GeoOperations"; | ||||
| import {TileLayer} from "leaflet"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| 
 | ||||
| /** | ||||
|  * Calculates which layers are available at the current location | ||||
|  | @ -24,45 +25,87 @@ export default class AvailableBaseLayers { | |||
|                 false, false), | ||||
|             feature: null, | ||||
|             max_zoom: 19, | ||||
|             min_zoom: 0 | ||||
|             min_zoom: 0, | ||||
|             isBest: false, // This is a lie! Of course OSM is the best map! (But not in this context)
 | ||||
|             category: "osmbasedmap" | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex()); | ||||
|     public availableEditorLayers: UIEventSource<BaseLayer[]>; | ||||
| 
 | ||||
|     constructor(location: UIEventSource<{ lat: number, lon: number, zoom: number }>) { | ||||
|         const self = this; | ||||
|         this.availableEditorLayers = | ||||
|             location.map( | ||||
|                 (currentLocation) => { | ||||
|     public static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> { | ||||
|         const source = location.map( | ||||
|             (currentLocation) => { | ||||
| 
 | ||||
|                     if (currentLocation === undefined) { | ||||
|                         return AvailableBaseLayers.layerOverview; | ||||
|                     } | ||||
|                 if (currentLocation === undefined) { | ||||
|                     return AvailableBaseLayers.layerOverview; | ||||
|                 } | ||||
| 
 | ||||
|                     const currentLayers = self.availableEditorLayers?.data; | ||||
|                     const newLayers = AvailableBaseLayers.AvailableLayersAt(currentLocation?.lon, currentLocation?.lat); | ||||
|                 const currentLayers = source?.data; // A bit unorthodox - I know
 | ||||
|                 const newLayers = AvailableBaseLayers.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat); | ||||
| 
 | ||||
|                     if (currentLayers === undefined) { | ||||
|                 if (currentLayers === undefined) { | ||||
|                     return newLayers; | ||||
|                 } | ||||
|                 if (newLayers.length !== currentLayers.length) { | ||||
|                     return newLayers; | ||||
|                 } | ||||
|                 for (let i = 0; i < newLayers.length; i++) { | ||||
|                     if (newLayers[i].name !== currentLayers[i].name) { | ||||
|                         return newLayers; | ||||
|                     } | ||||
|                     if (newLayers.length !== currentLayers.length) { | ||||
|                         return newLayers; | ||||
|                     } | ||||
|                     for (let i = 0; i < newLayers.length; i++) { | ||||
|                         if (newLayers[i].name !== currentLayers[i].name) { | ||||
|                             return newLayers; | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     return currentLayers; | ||||
|                 }); | ||||
| 
 | ||||
|                 } | ||||
| 
 | ||||
|                 return currentLayers; | ||||
|             }); | ||||
|         return source; | ||||
|     } | ||||
| 
 | ||||
|     private static AvailableLayersAt(lon: number, lat: number): BaseLayer[] { | ||||
|     public static SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> { | ||||
|         return AvailableBaseLayers.AvailableLayersAt(location).map(available => { | ||||
|             // First float all 'best layers' to the top
 | ||||
|             available.sort((a, b) => { | ||||
|                     if (a.isBest && b.isBest) { | ||||
|                         return 0; | ||||
|                     } | ||||
|                     if (!a.isBest) { | ||||
|                         return 1 | ||||
|                     } | ||||
| 
 | ||||
|                     return -1; | ||||
|                 } | ||||
|             ) | ||||
| 
 | ||||
|             if (preferedCategory.data === undefined) { | ||||
|                 return available[0] | ||||
|             } | ||||
| 
 | ||||
|             let prefered: string [] | ||||
|             if (typeof preferedCategory.data === "string") { | ||||
|                 prefered = [preferedCategory.data] | ||||
|             } else { | ||||
|                 prefered = preferedCategory.data; | ||||
|             } | ||||
| 
 | ||||
|             prefered.reverse(); | ||||
|             for (const category of prefered) { | ||||
|                 //Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
 | ||||
|                 available.sort((a, b) => { | ||||
|                         if (a.category === category && b.category === category) { | ||||
|                             return 0; | ||||
|                         } | ||||
|                         if (a.category !== category) { | ||||
|                             return 1 | ||||
|                         } | ||||
| 
 | ||||
|                         return -1; | ||||
|                     } | ||||
|                 ) | ||||
|             } | ||||
|             return available[0] | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     private static CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] { | ||||
|         const availableLayers = [AvailableBaseLayers.osmCarto] | ||||
|         const globalLayers = []; | ||||
|         for (const layerOverviewItem of AvailableBaseLayers.layerOverview) { | ||||
|  | @ -140,7 +183,9 @@ export default class AvailableBaseLayers { | |||
|                 min_zoom: props.min_zoom ?? 1, | ||||
|                 name: props.name, | ||||
|                 layer: leafletLayer, | ||||
|                 feature: layer | ||||
|                 feature: layer, | ||||
|                 isBest: props.best ?? false, | ||||
|                 category: props.category | ||||
|             }); | ||||
|         } | ||||
|         return layers; | ||||
|  | @ -152,15 +197,16 @@ export default class AvailableBaseLayers { | |||
|         function l(id: string, name: string): BaseLayer { | ||||
|             try { | ||||
|                 const layer: any = () => L.tileLayer.provider(id, undefined); | ||||
|                 const baseLayer: BaseLayer = { | ||||
|                 return { | ||||
|                     feature: null, | ||||
|                     id: id, | ||||
|                     name: name, | ||||
|                     layer: layer, | ||||
|                     min_zoom: layer.minzoom, | ||||
|                     max_zoom: layer.maxzoom | ||||
|                     max_zoom: layer.maxzoom, | ||||
|                     category: "osmbasedmap", | ||||
|                     isBest: false | ||||
|                 } | ||||
|                 return baseLayer | ||||
|             } catch (e) { | ||||
|                 console.error("Could not find provided layer", name, e); | ||||
|                 return null; | ||||
|  |  | |||
|  | @ -1,265 +1,271 @@ | |||
| import * as L from "leaflet"; | ||||
| import { UIEventSource } from "../UIEventSource"; | ||||
| import { Utils } from "../../Utils"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import Svg from "../../Svg"; | ||||
| import Img from "../../UI/Base/Img"; | ||||
| import { LocalStorageSource } from "../Web/LocalStorageSource"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | ||||
| import { VariableUiElement } from "../../UI/Base/VariableUIElement"; | ||||
| import { CenterFlexedElement } from "../../UI/Base/CenterFlexedElement"; | ||||
| import {VariableUiElement} from "../../UI/Base/VariableUIElement"; | ||||
| import {CenterFlexedElement} from "../../UI/Base/CenterFlexedElement"; | ||||
| 
 | ||||
| export default class GeoLocationHandler extends VariableUiElement { | ||||
|   /** | ||||
|    * Wether or not the geolocation is active, aka the user requested the current location | ||||
|    * @private | ||||
|    */ | ||||
|   private readonly _isActive: UIEventSource<boolean>; | ||||
|     /** | ||||
|      * Wether or not the geolocation is active, aka the user requested the current location | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _isActive: UIEventSource<boolean>; | ||||
| 
 | ||||
|   /** | ||||
|    * The callback over the permission API | ||||
|    * @private | ||||
|    */ | ||||
|   private readonly _permission: UIEventSource<string>; | ||||
|   /*** | ||||
|    * The marker on the map, in order to update it | ||||
|    * @private | ||||
|    */ | ||||
|   private _marker: L.Marker; | ||||
|   /** | ||||
|    * Literally: _currentGPSLocation.data != undefined | ||||
|    * @private | ||||
|    */ | ||||
|   private readonly _hasLocation: UIEventSource<boolean>; | ||||
|   private readonly _currentGPSLocation: UIEventSource<{ | ||||
|     latlng: any; | ||||
|     accuracy: number; | ||||
|   }>; | ||||
|   /** | ||||
|    * Kept in order to update the marker | ||||
|    * @private | ||||
|    */ | ||||
|   private readonly _leafletMap: UIEventSource<L.Map>; | ||||
|   /** | ||||
|    * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs | ||||
|    * @private | ||||
|    */ | ||||
|   private _lastUserRequest: Date; | ||||
|   /** | ||||
|    * A small flag on localstorage. If the user previously granted the geolocation, it will be set. | ||||
|    * On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions. | ||||
|    * | ||||
|    * Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately. | ||||
|    * If the user denies the geolocation this time, we unset this flag | ||||
|    * @private | ||||
|    */ | ||||
|   private readonly _previousLocationGrant: UIEventSource<string>; | ||||
|   private readonly _layoutToUse: UIEventSource<LayoutConfig>; | ||||
| 
 | ||||
|   constructor( | ||||
|     currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>, | ||||
|     leafletMap: UIEventSource<L.Map>, | ||||
|     layoutToUse: UIEventSource<LayoutConfig> | ||||
|   ) { | ||||
|     const hasLocation = currentGPSLocation.map( | ||||
|       (location) => location !== undefined | ||||
|     ); | ||||
|     const previousLocationGrant = LocalStorageSource.Get( | ||||
|       "geolocation-permissions" | ||||
|     ); | ||||
|     const isActive = new UIEventSource<boolean>(false); | ||||
|     /** | ||||
|      * Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _isLocked: UIEventSource<boolean>; | ||||
| 
 | ||||
|     super( | ||||
|       hasLocation.map( | ||||
|         (hasLocation) => { | ||||
|           if (hasLocation) { | ||||
|             return new CenterFlexedElement( | ||||
|               Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem") | ||||
|             ); // crosshair_blue_ui()
 | ||||
|           } | ||||
|           if (isActive.data) { | ||||
|             return new CenterFlexedElement( | ||||
|               Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem") | ||||
|             ); // crosshair_blue_center_ui
 | ||||
|           } | ||||
|           return new CenterFlexedElement( | ||||
|             Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem") | ||||
|           ); //crosshair_ui
 | ||||
|         }, | ||||
|         [isActive] | ||||
|       ) | ||||
|     ); | ||||
|     this._isActive = isActive; | ||||
|     this._permission = new UIEventSource<string>(""); | ||||
|     this._previousLocationGrant = previousLocationGrant; | ||||
|     this._currentGPSLocation = currentGPSLocation; | ||||
|     this._leafletMap = leafletMap; | ||||
|     this._layoutToUse = layoutToUse; | ||||
|     this._hasLocation = hasLocation; | ||||
|     const self = this; | ||||
|     /** | ||||
|      * The callback over the permission API | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _permission: UIEventSource<string>; | ||||
| 
 | ||||
|     const currentPointer = this._isActive.map( | ||||
|       (isActive) => { | ||||
|         if (isActive && !self._hasLocation.data) { | ||||
|           return "cursor-wait"; | ||||
|         } | ||||
|         return "cursor-pointer"; | ||||
|       }, | ||||
|       [this._hasLocation] | ||||
|     ); | ||||
|     currentPointer.addCallbackAndRun((pointerClass) => { | ||||
|       self.SetClass(pointerClass); | ||||
|     }); | ||||
|     /*** | ||||
|      * The marker on the map, in order to update it | ||||
|      * @private | ||||
|      */ | ||||
|     private _marker: L.Marker; | ||||
|     /** | ||||
|      * Literally: _currentGPSLocation.data != undefined | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _hasLocation: UIEventSource<boolean>; | ||||
|     private readonly _currentGPSLocation: UIEventSource<{ | ||||
|         latlng: any; | ||||
|         accuracy: number; | ||||
|     }>; | ||||
|     /** | ||||
|      * Kept in order to update the marker | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _leafletMap: UIEventSource<L.Map>; | ||||
| 
 | ||||
|     this.onClick(() => self.init(true)); | ||||
|     this.init(false); | ||||
|   } | ||||
| 
 | ||||
|   private init(askPermission: boolean) { | ||||
|     const self = this; | ||||
|     const map = this._leafletMap.data; | ||||
|     /** | ||||
|      * The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs | ||||
|      * @private | ||||
|      */ | ||||
|     private _lastUserRequest: Date; | ||||
| 
 | ||||
|     this._currentGPSLocation.addCallback((location) => { | ||||
|       self._previousLocationGrant.setData("granted"); | ||||
| 
 | ||||
|       const timeSinceRequest = | ||||
|         (new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000; | ||||
|       if (timeSinceRequest < 30) { | ||||
|         self.MoveToCurrentLoction(16); | ||||
|       } | ||||
|     /** | ||||
|      * A small flag on localstorage. If the user previously granted the geolocation, it will be set. | ||||
|      * On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions. | ||||
|      * | ||||
|      * Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately. | ||||
|      * If the user denies the geolocation this time, we unset this flag | ||||
|      * @private | ||||
|      */ | ||||
|     private readonly _previousLocationGrant: UIEventSource<string>; | ||||
|     private readonly _layoutToUse: UIEventSource<LayoutConfig>; | ||||
| 
 | ||||
|       let color = "#1111cc"; | ||||
|       try { | ||||
|         color = getComputedStyle(document.body).getPropertyValue( | ||||
|           "--catch-detail-color" | ||||
|         ); | ||||
|       } catch (e) { | ||||
|         console.error(e); | ||||
|       } | ||||
|       const icon = L.icon({ | ||||
|         iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)), | ||||
|         iconSize: [40, 40], // size of the icon
 | ||||
|         iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
 | ||||
|       }); | ||||
| 
 | ||||
|       const newMarker = L.marker(location.latlng, { icon: icon }); | ||||
|       newMarker.addTo(map); | ||||
| 
 | ||||
|       if (self._marker !== undefined) { | ||||
|         map.removeLayer(self._marker); | ||||
|       } | ||||
|       self._marker = newMarker; | ||||
|     }); | ||||
| 
 | ||||
|     try { | ||||
|       navigator?.permissions | ||||
|         ?.query({ name: "geolocation" }) | ||||
|         ?.then(function (status) { | ||||
|           console.log("Geolocation is already", status); | ||||
|           if (status.state === "granted") { | ||||
|             self.StartGeolocating(false); | ||||
|           } | ||||
|           self._permission.setData(status.state); | ||||
|           status.onchange = function () { | ||||
|             self._permission.setData(status.state); | ||||
|           }; | ||||
|         }); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|     } | ||||
|     if (askPermission) { | ||||
|       self.StartGeolocating(true); | ||||
|     } else if (this._previousLocationGrant.data === "granted") { | ||||
|       this._previousLocationGrant.setData(""); | ||||
|       self.StartGeolocating(false); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private locate() { | ||||
|     const self = this; | ||||
|     const map: any = this._leafletMap.data; | ||||
| 
 | ||||
|     if (navigator.geolocation) { | ||||
|       navigator.geolocation.getCurrentPosition( | ||||
|         function (position) { | ||||
|           self._currentGPSLocation.setData({ | ||||
|             latlng: [position.coords.latitude, position.coords.longitude], | ||||
|             accuracy: position.coords.accuracy, | ||||
|           }); | ||||
|         }, | ||||
|         function () { | ||||
|           console.warn("Could not get location with navigator.geolocation"); | ||||
|         } | ||||
|       ); | ||||
|       return; | ||||
|     } else { | ||||
|       map.findAccuratePosition({ | ||||
|         maxWait: 10000, // defaults to 10000
 | ||||
|         desiredAccuracy: 50, // defaults to 20
 | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private MoveToCurrentLoction(targetZoom = 16) { | ||||
|     const location = this._currentGPSLocation.data; | ||||
|     this._lastUserRequest = undefined; | ||||
| 
 | ||||
|     if ( | ||||
|       this._currentGPSLocation.data.latlng[0] === 0 && | ||||
|       this._currentGPSLocation.data.latlng[1] === 0 | ||||
|     constructor( | ||||
|         currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>, | ||||
|         leafletMap: UIEventSource<L.Map>, | ||||
|         layoutToUse: UIEventSource<LayoutConfig> | ||||
|     ) { | ||||
|       console.debug("Not moving to GPS-location: it is null island"); | ||||
|       return; | ||||
|         const hasLocation = currentGPSLocation.map( | ||||
|             (location) => location !== undefined | ||||
|         ); | ||||
|         const previousLocationGrant = LocalStorageSource.Get( | ||||
|             "geolocation-permissions" | ||||
|         ); | ||||
|         const isActive = new UIEventSource<boolean>(false); | ||||
|         const isLocked = new UIEventSource<boolean>(false); | ||||
|         super( | ||||
|             hasLocation.map( | ||||
|                 (hasLocationData) => { | ||||
|                     let icon: string; | ||||
| 
 | ||||
|                     if (isLocked.data) { | ||||
|                         icon = Svg.crosshair_locked; | ||||
|                     } else if (hasLocationData) { | ||||
|                         icon = Svg.crosshair_blue; | ||||
|                     } else if (isActive.data) { | ||||
|                         icon = Svg.crosshair_blue_center; | ||||
|                     } else { | ||||
|                         icon = Svg.crosshair; | ||||
|                     } | ||||
| 
 | ||||
|                     return new CenterFlexedElement( | ||||
|                         Img.AsImageElement(icon, "", "width:1.25rem;height:1.25rem") | ||||
|                     ); | ||||
| 
 | ||||
|                 }, | ||||
|                 [isActive, isLocked] | ||||
|             ) | ||||
|         ); | ||||
|         this._isActive = isActive; | ||||
|         this._isLocked = isLocked; | ||||
|         this._permission = new UIEventSource<string>(""); | ||||
|         this._previousLocationGrant = previousLocationGrant; | ||||
|         this._currentGPSLocation = currentGPSLocation; | ||||
|         this._leafletMap = leafletMap; | ||||
|         this._layoutToUse = layoutToUse; | ||||
|         this._hasLocation = hasLocation; | ||||
|         const self = this; | ||||
| 
 | ||||
|         const currentPointer = this._isActive.map( | ||||
|             (isActive) => { | ||||
|                 if (isActive && !self._hasLocation.data) { | ||||
|                     return "cursor-wait"; | ||||
|                 } | ||||
|                 return "cursor-pointer"; | ||||
|             }, | ||||
|             [this._hasLocation] | ||||
|         ); | ||||
|         currentPointer.addCallbackAndRun((pointerClass) => { | ||||
|             self.SetClass(pointerClass); | ||||
|         }); | ||||
| 
 | ||||
|         this.onClick(() => { | ||||
|             if (self._hasLocation.data) { | ||||
|                 self._isLocked.setData(!self._isLocked.data); | ||||
|             } | ||||
|             self.init(true); | ||||
|         }); | ||||
|         this.init(false); | ||||
| 
 | ||||
| 
 | ||||
|         this._currentGPSLocation.addCallback((location) => { | ||||
|             self._previousLocationGrant.setData("granted"); | ||||
| 
 | ||||
|             const timeSinceRequest = | ||||
|                 (new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000; | ||||
|             if (timeSinceRequest < 30) { | ||||
|                 self.MoveToCurrentLoction(16); | ||||
|             } else if (self._isLocked.data) { | ||||
|                 self.MoveToCurrentLoction(); | ||||
|             } | ||||
| 
 | ||||
|             let color = "#1111cc"; | ||||
|             try { | ||||
|                 color = getComputedStyle(document.body).getPropertyValue( | ||||
|                     "--catch-detail-color" | ||||
|                 ); | ||||
|             } catch (e) { | ||||
|                 console.error(e); | ||||
|             } | ||||
|             const icon = L.icon({ | ||||
|                 iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)), | ||||
|                 iconSize: [40, 40], // size of the icon
 | ||||
|                 iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
 | ||||
|             }); | ||||
| 
 | ||||
|             const map = self._leafletMap.data; | ||||
| 
 | ||||
|             const newMarker = L.marker(location.latlng, {icon: icon}); | ||||
|             newMarker.addTo(map); | ||||
| 
 | ||||
|             if (self._marker !== undefined) { | ||||
|                 map.removeLayer(self._marker); | ||||
|             } | ||||
|             self._marker = newMarker; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     // We check that the GPS location is not out of bounds
 | ||||
|     const b = this._layoutToUse.data.lockLocation; | ||||
|     let inRange = true; | ||||
|     if (b) { | ||||
|       if (b !== true) { | ||||
|         // B is an array with our locklocation
 | ||||
|         inRange = | ||||
|           b[0][0] <= location.latlng[0] && | ||||
|           location.latlng[0] <= b[1][0] && | ||||
|           b[0][1] <= location.latlng[1] && | ||||
|           location.latlng[1] <= b[1][1]; | ||||
|       } | ||||
|     } | ||||
|     if (!inRange) { | ||||
|       console.log( | ||||
|         "Not zooming to GPS location: out of bounds", | ||||
|         b, | ||||
|         location.latlng | ||||
|       ); | ||||
|     } else { | ||||
|       this._leafletMap.data.setView(location.latlng, targetZoom); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private StartGeolocating(zoomToGPS = true) { | ||||
|     const self = this; | ||||
|     console.log("Starting geolocation"); | ||||
| 
 | ||||
|     this._lastUserRequest = zoomToGPS ? new Date() : new Date(0); | ||||
|     if (self._permission.data === "denied") { | ||||
|       self._previousLocationGrant.setData(""); | ||||
|       return ""; | ||||
|     } | ||||
|     if (this._currentGPSLocation.data !== undefined) { | ||||
|       this.MoveToCurrentLoction(16); | ||||
|     } | ||||
| 
 | ||||
|     console.log("Searching location using GPS"); | ||||
|     this.locate(); | ||||
| 
 | ||||
|     if (!self._isActive.data) { | ||||
|       self._isActive.setData(true); | ||||
|       Utils.DoEvery(60000, () => { | ||||
|         if (document.visibilityState !== "visible") { | ||||
|           console.log("Not starting gps: document not visible"); | ||||
|           return; | ||||
|     private init(askPermission: boolean) { | ||||
|         const self = this; | ||||
|         if (self._isActive.data) { | ||||
|             self.MoveToCurrentLoction(16); | ||||
|             return; | ||||
|         } | ||||
|         try { | ||||
|             navigator?.permissions | ||||
|                 ?.query({name: "geolocation"}) | ||||
|                 ?.then(function (status) { | ||||
|                     console.log("Geolocation is already", status); | ||||
|                     if (status.state === "granted") { | ||||
|                         self.StartGeolocating(false); | ||||
|                     } | ||||
|                     self._permission.setData(status.state); | ||||
|                     status.onchange = function () { | ||||
|                         self._permission.setData(status.state); | ||||
|                     }; | ||||
|                 }); | ||||
|         } catch (e) { | ||||
|             console.error(e); | ||||
|         } | ||||
|         if (askPermission) { | ||||
|             self.StartGeolocating(true); | ||||
|         } else if (this._previousLocationGrant.data === "granted") { | ||||
|             this._previousLocationGrant.setData(""); | ||||
|             self.StartGeolocating(false); | ||||
|         } | ||||
|         this.locate(); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|     private MoveToCurrentLoction(targetZoom = 16) { | ||||
|         const location = this._currentGPSLocation.data; | ||||
|         this._lastUserRequest = undefined; | ||||
| 
 | ||||
|         if ( | ||||
|             this._currentGPSLocation.data.latlng[0] === 0 && | ||||
|             this._currentGPSLocation.data.latlng[1] === 0 | ||||
|         ) { | ||||
|             console.debug("Not moving to GPS-location: it is null island"); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // We check that the GPS location is not out of bounds
 | ||||
|         const b = this._layoutToUse.data.lockLocation; | ||||
|         let inRange = true; | ||||
|         if (b) { | ||||
|             if (b !== true) { | ||||
|                 // B is an array with our locklocation
 | ||||
|                 inRange = | ||||
|                     b[0][0] <= location.latlng[0] && | ||||
|                     location.latlng[0] <= b[1][0] && | ||||
|                     b[0][1] <= location.latlng[1] && | ||||
|                     location.latlng[1] <= b[1][1]; | ||||
|             } | ||||
|         } | ||||
|         if (!inRange) { | ||||
|             console.log( | ||||
|                 "Not zooming to GPS location: out of bounds", | ||||
|                 b, | ||||
|                 location.latlng | ||||
|             ); | ||||
|         } else { | ||||
|             this._leafletMap.data.setView(location.latlng, targetZoom); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private StartGeolocating(zoomToGPS = true) { | ||||
|         const self = this; | ||||
|         console.log("Starting geolocation"); | ||||
| 
 | ||||
|         this._lastUserRequest = zoomToGPS ? new Date() : new Date(0); | ||||
|         if (self._permission.data === "denied") { | ||||
|             self._previousLocationGrant.setData(""); | ||||
|             return ""; | ||||
|         } | ||||
|         if (this._currentGPSLocation.data !== undefined) { | ||||
|             this.MoveToCurrentLoction(16); | ||||
|         } | ||||
| 
 | ||||
|         console.log("Searching location using GPS"); | ||||
| 
 | ||||
|         if (self._isActive.data) { | ||||
|             return; | ||||
|         } | ||||
|         self._isActive.setData(true); | ||||
|         navigator.geolocation.watchPosition( | ||||
|             function (position) { | ||||
|                 self._currentGPSLocation.setData({ | ||||
|                     latlng: [position.coords.latitude, position.coords.longitude], | ||||
|                     accuracy: position.coords.accuracy, | ||||
|                 }); | ||||
|             }, | ||||
|             function () { | ||||
|                 console.warn("Could not get location with navigator.geolocation"); | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -47,7 +47,12 @@ export default class StrayClickHandler { | |||
|                     popupAnchor: [0, -45] | ||||
|                 }) | ||||
|             }); | ||||
|             const popup = L.popup().setContent("<div id='strayclick'></div>"); | ||||
|             const popup = L.popup({ | ||||
|                 autoPan: true, | ||||
|                 autoPanPaddingTopLeft: [15,15], | ||||
|                 closeOnEscapeKey: true, | ||||
|                 autoClose: true | ||||
|             }).setContent("<div id='strayclick' style='height: 65vh'></div>"); | ||||
|             self._lastMarker.addTo(leafletMap.data); | ||||
|             self._lastMarker.bindPopup(popup); | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ import RegisteringFeatureSource from "./RegisteringFeatureSource"; | |||
| 
 | ||||
| export default class FeaturePipeline implements FeatureSource { | ||||
| 
 | ||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> ; | ||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
| 
 | ||||
|     public readonly name = "FeaturePipeline" | ||||
| 
 | ||||
|  | @ -29,7 +29,7 @@ export default class FeaturePipeline implements FeatureSource { | |||
|                 selectedElement: UIEventSource<any>) { | ||||
| 
 | ||||
|         const allLoadedFeatures = new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||
|          | ||||
| 
 | ||||
|         // first we metatag, then we save to get the metatags into storage too
 | ||||
|         // Note that we need to register before we do metatagging (as it expects the event sources)
 | ||||
| 
 | ||||
|  | @ -46,8 +46,11 @@ export default class FeaturePipeline implements FeatureSource { | |||
|         const geojsonSources: FeatureSource [] = GeoJsonSource | ||||
|             .ConstructMultiSource(flayers.data, locationControl) | ||||
|             .map(geojsonSource => { | ||||
|                 let source = new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, geojsonSource)); | ||||
|                 if(!geojsonSource.isOsmCache){ | ||||
|                 let source = new RegisteringFeatureSource( | ||||
|                     new FeatureDuplicatorPerLayer(flayers, | ||||
|                             geojsonSource | ||||
|                     )); | ||||
|                 if (!geojsonSource.isOsmCache) { | ||||
|                     source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features); | ||||
|                 } | ||||
|                 return source | ||||
|  |  | |||
|  | @ -1,9 +1,45 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| 
 | ||||
| export default interface FeatureSource { | ||||
|     features: UIEventSource<{feature: any, freshness: Date}[]>; | ||||
|     features: UIEventSource<{ feature: any, freshness: Date }[]>; | ||||
|     /** | ||||
|      * Mainly used for debuging | ||||
|      */ | ||||
|     name: string; | ||||
| } | ||||
| 
 | ||||
| export class FeatureSourceUtils { | ||||
| 
 | ||||
|     /** | ||||
|      * Exports given featurePipeline as a geojson FeatureLists (downloads as a json) | ||||
|      * @param featurePipeline The FeaturePipeline you want to export | ||||
|      * @param options The options object | ||||
|      * @param options.metadata True if you want to include the MapComplete metadata, false otherwise | ||||
|      */ | ||||
|     public static extractGeoJson(featurePipeline: FeatureSource, options: { metadata?: boolean } = {}) { | ||||
|         let defaults = { | ||||
|             metadata: false, | ||||
|         } | ||||
|         options = Utils.setDefaults(options, defaults); | ||||
| 
 | ||||
|         // Select all features, ignore the freshness and other data
 | ||||
|         let featureList: any[] = featurePipeline.features.data.map((feature) => feature.feature); | ||||
| 
 | ||||
|         if (!options.metadata) { | ||||
|             for (let i = 0; i < featureList.length; i++) { | ||||
|                 let feature = featureList[i]; | ||||
|                 for (let property in feature.properties) { | ||||
|                     if (property[0] == "_") { | ||||
|                         delete featureList[i]["properties"][property]; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return {type: "FeatureCollection", features: featureList} | ||||
| 
 | ||||
|    | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -175,7 +175,7 @@ export default class GeoJsonSource implements FeatureSource { | |||
| 
 | ||||
|                 let freshness: Date = time; | ||||
|                 if (feature.properties["_last_edit:timestamp"] !== undefined) { | ||||
|                     freshness = new Date(feature["_last_edit:timestamp"]) | ||||
|                     freshness = new Date(feature.properties["_last_edit:timestamp"]) | ||||
|                 } | ||||
| 
 | ||||
|                 newFeatures.push({feature: feature, freshness: freshness}) | ||||
|  |  | |||
|  | @ -6,11 +6,14 @@ export class GeoOperations { | |||
|         return turf.area(feature); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Converts a GeoJSon feature to a point feature | ||||
|      * @param feature | ||||
|      */ | ||||
|     static centerpoint(feature: any) { | ||||
|         const newFeature = turf.center(feature); | ||||
|         newFeature.properties = feature.properties; | ||||
|         newFeature.id = feature.id; | ||||
| 
 | ||||
|         return newFeature; | ||||
|     } | ||||
| 
 | ||||
|  | @ -273,6 +276,14 @@ export class GeoOperations { | |||
|         } | ||||
|         return undefined; | ||||
|     } | ||||
|     /** | ||||
|      * Generates the closest point on a way from a given point | ||||
|      * @param way The road on which you want to find a point | ||||
|      * @param point Point defined as [lon, lat] | ||||
|      */ | ||||
|     public static nearestPoint(way, point: [number, number]){ | ||||
|         return turf.nearestPointOnLine(way, point, {units: "kilometers"}); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,31 +6,38 @@ import Constants from "../../Models/Constants"; | |||
| import FeatureSource from "../FeatureSource/FeatureSource"; | ||||
| import {TagsFilter} from "../Tags/TagsFilter"; | ||||
| import {Tag} from "../Tags/Tag"; | ||||
| import {OsmConnection} from "./OsmConnection"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| 
 | ||||
| /** | ||||
|  * Handles all changes made to OSM. | ||||
|  * Needs an authenticator via OsmConnection | ||||
|  */ | ||||
| export class Changes implements FeatureSource{ | ||||
| export class Changes implements FeatureSource { | ||||
| 
 | ||||
|      | ||||
| 
 | ||||
|     private static _nextId = -1; // Newly assigned ID's are negative
 | ||||
|     public readonly name = "Newly added features" | ||||
|     /** | ||||
|      * The newly created points, as a FeatureSource | ||||
|      */ | ||||
|     public features = new UIEventSource<{feature: any, freshness: Date}[]>([]); | ||||
|      | ||||
|     private static _nextId = -1; // Newly assigned ID's are negative
 | ||||
|     public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]); | ||||
|     /** | ||||
|      * All the pending changes | ||||
|      */ | ||||
|     public readonly pending: UIEventSource<{ elementId: string, key: string, value: string }[]> =  | ||||
|         new UIEventSource<{elementId: string; key: string; value: string}[]>([]); | ||||
|     public readonly pending = LocalStorageSource.GetParsed<{ elementId: string, key: string, value: string }[]>("pending-changes", []) | ||||
| 
 | ||||
|     /** | ||||
|      * All the pending new objects to upload | ||||
|      */ | ||||
|     private readonly newObjects = LocalStorageSource.GetParsed<{ id: number, lat: number, lon: number }[]>("newObjects", []) | ||||
| 
 | ||||
|     private readonly isUploading = new UIEventSource(false); | ||||
| 
 | ||||
|     /** | ||||
|      * Adds a change to the pending changes | ||||
|      */ | ||||
|     private static checkChange(kv: {k: string, v: string}): { k: string, v: string } { | ||||
|     private static checkChange(kv: { k: string, v: string }): { k: string, v: string } { | ||||
|         const key = kv.k; | ||||
|         const value = kv.v; | ||||
|         if (key === undefined || key === null) { | ||||
|  | @ -49,8 +56,7 @@ export class Changes implements FeatureSource{ | |||
|         return {k: key.trim(), v: value.trim()}; | ||||
|     } | ||||
| 
 | ||||
|      | ||||
|      | ||||
| 
 | ||||
|     addTag(elementId: string, tagsFilter: TagsFilter, | ||||
|            tags?: UIEventSource<any>) { | ||||
|         const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); | ||||
|  | @ -59,7 +65,7 @@ export class Changes implements FeatureSource{ | |||
|         if (changes.length == 0) { | ||||
|             return; | ||||
|         } | ||||
|        | ||||
| 
 | ||||
|         for (const change of changes) { | ||||
|             if (elementTags[change.k] !== change.v) { | ||||
|                 elementTags[change.k] = change.v; | ||||
|  | @ -76,16 +82,16 @@ export class Changes implements FeatureSource{ | |||
|      * Uploads all the pending changes in one go. | ||||
|      * Triggered by the 'PendingChangeUploader'-actor in Actors | ||||
|      */ | ||||
|     public flushChanges(flushreason: string = undefined){ | ||||
|         if(this.pending.data.length === 0){ | ||||
|     public flushChanges(flushreason: string = undefined) { | ||||
|         if (this.pending.data.length === 0) { | ||||
|             return; | ||||
|         } | ||||
|         if(flushreason !== undefined){ | ||||
|         if (flushreason !== undefined) { | ||||
|             console.log(flushreason) | ||||
|         } | ||||
|         this.uploadAll([], this.pending.data); | ||||
|         this.pending.setData([]); | ||||
|         this.uploadAll(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create a new node element at the given lat/long. | ||||
|      * An internal OsmObject is created to upload later on, a geojson represention is returned. | ||||
|  | @ -93,12 +99,12 @@ export class Changes implements FeatureSource{ | |||
|      */ | ||||
|     public createElement(basicTags: Tag[], lat: number, lon: number) { | ||||
|         console.log("Creating a new element with ", basicTags) | ||||
|         const osmNode = new OsmNode(Changes._nextId); | ||||
|         const newId = Changes._nextId; | ||||
|         Changes._nextId--; | ||||
| 
 | ||||
|         const id = "node/" + osmNode.id; | ||||
|         osmNode.lat = lat; | ||||
|         osmNode.lon = lon; | ||||
|         const id = "node/" + newId; | ||||
| 
 | ||||
| 
 | ||||
|         const properties = {id: id}; | ||||
| 
 | ||||
|         const geojson = { | ||||
|  | @ -118,35 +124,49 @@ export class Changes implements FeatureSource{ | |||
|         // The tags are not yet written into the OsmObject, but this is applied onto a 
 | ||||
|         const changes = []; | ||||
|         for (const kv of basicTags) { | ||||
|             properties[kv.key] = kv.value; | ||||
|             if (typeof kv.value !== "string") { | ||||
|                 throw "Invalid value: don't use a regex in a preset" | ||||
|             } | ||||
|             properties[kv.key] = kv.value; | ||||
|             changes.push({elementId: id, key: kv.key, value: kv.value}) | ||||
|         } | ||||
|         | ||||
| 
 | ||||
|         console.log("New feature added and pinged") | ||||
|         this.features.data.push({feature:geojson, freshness: new Date()}); | ||||
|         this.features.data.push({feature: geojson, freshness: new Date()}); | ||||
|         this.features.ping(); | ||||
|          | ||||
| 
 | ||||
|         State.state.allElements.addOrGetElement(geojson).ping(); | ||||
| 
 | ||||
|         this.uploadAll([osmNode], changes); | ||||
|         if (State.state.osmConnection.userDetails.data.backend !== OsmConnection.oauth_configs.osm.url) { | ||||
|             properties["_backend"] = State.state.osmConnection.userDetails.data.backend | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         this.newObjects.data.push({id: newId, lat: lat, lon: lon}) | ||||
|         this.pending.data.push(...changes) | ||||
|         this.pending.ping(); | ||||
|         this.newObjects.ping(); | ||||
|         return geojson; | ||||
|     } | ||||
| 
 | ||||
|     private uploadChangesWithLatestVersions( | ||||
|         knownElements: OsmObject[], newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) { | ||||
|         knownElements: OsmObject[]) { | ||||
|         const knownById = new Map<string, OsmObject>(); | ||||
|          | ||||
|         knownElements.forEach(knownElement => { | ||||
|             knownById.set(knownElement.type + "/" + knownElement.id, knownElement) | ||||
|         }) | ||||
|          | ||||
|           | ||||
| 
 | ||||
|         const newElements: OsmNode [] = this.newObjects.data.map(spec => { | ||||
|             const newElement = new OsmNode(spec.id); | ||||
|             newElement.lat = spec.lat; | ||||
|             newElement.lon = spec.lon; | ||||
|             return newElement | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         // Here, inside the continuation, we know that all 'neededIds' are loaded in 'knownElements', which maps the ids onto the elements
 | ||||
|         // We apply the changes on them
 | ||||
|         for (const change of pending) { | ||||
|         for (const change of this.pending.data) { | ||||
|             if (parseInt(change.elementId.split("/")[1]) < 0) { | ||||
|                 // This is a new element - we should apply this on one of the new elements
 | ||||
|                 for (const newElement of newElements) { | ||||
|  | @ -168,9 +188,17 @@ export class Changes implements FeatureSource{ | |||
|             } | ||||
|         } | ||||
|         if (changedElements.length == 0 && newElements.length == 0) { | ||||
|             console.log("No changes in any object"); | ||||
|             console.log("No changes in any object - clearing"); | ||||
|             this.pending.setData([]) | ||||
|             this.newObjects.setData([]) | ||||
|             return; | ||||
|         } | ||||
|         const self = this; | ||||
| 
 | ||||
|         if (this.isUploading.data) { | ||||
|             return; | ||||
|         } | ||||
|         this.isUploading.setData(true) | ||||
| 
 | ||||
|         console.log("Beginning upload..."); | ||||
|         // At last, we build the changeset and upload
 | ||||
|  | @ -213,17 +241,22 @@ export class Changes implements FeatureSource{ | |||
|                 changes += "</osmChange>"; | ||||
| 
 | ||||
|                 return changes; | ||||
|             }); | ||||
|             }, | ||||
|             () => { | ||||
|                 console.log("Upload successfull!") | ||||
|                 self.newObjects.setData([]) | ||||
|                 self.pending.setData([]); | ||||
|                 self.isUploading.setData(false) | ||||
|             }, | ||||
|             () => self.isUploading.setData(false) | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
| 
 | ||||
|     private uploadAll( | ||||
|         newElements: OsmObject[], | ||||
|         pending: { elementId: string; key: string; value: string }[] | ||||
|     ) { | ||||
|     private uploadAll() { | ||||
|         const self = this; | ||||
| 
 | ||||
| 
 | ||||
|         const pending = this.pending.data; | ||||
|         let neededIds: string[] = []; | ||||
|         for (const change of pending) { | ||||
|             const id = change.elementId; | ||||
|  | @ -236,8 +269,7 @@ export class Changes implements FeatureSource{ | |||
| 
 | ||||
|         neededIds = Utils.Dedup(neededIds); | ||||
|         OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => { | ||||
|             console.log("KnownElements:", knownElements) | ||||
|             self.uploadChangesWithLatestVersions(knownElements, newElements, pending) | ||||
|             self.uploadChangesWithLatestVersions(knownElements) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ export class ChangesetHandler { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage) { | ||||
|     private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage): void { | ||||
|         const nodes = response.getElementsByTagName("node"); | ||||
|         // @ts-ignore
 | ||||
|         for (const node of nodes) { | ||||
|  | @ -69,7 +69,9 @@ export class ChangesetHandler { | |||
|     public UploadChangeset( | ||||
|         layout: LayoutConfig, | ||||
|         allElements: ElementStorage, | ||||
|         generateChangeXML: (csid: string) => string) { | ||||
|         generateChangeXML: (csid: string) => string, | ||||
|         whenDone: (csId: string) => void, | ||||
|         onFail: () => void) { | ||||
| 
 | ||||
|         if (this.userDetails.data.csCount == 0) { | ||||
|             // The user became a contributor!
 | ||||
|  | @ -80,6 +82,7 @@ export class ChangesetHandler { | |||
|         if (this._dryRun) { | ||||
|             const changesetXML = generateChangeXML("123456"); | ||||
|             console.log(changesetXML); | ||||
|             whenDone("123456") | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|  | @ -93,12 +96,14 @@ export class ChangesetHandler { | |||
|                 console.log(changeset); | ||||
|                 self.AddChange(csId, changeset, | ||||
|                     allElements, | ||||
|                     () => { | ||||
|                     }, | ||||
|                     whenDone, | ||||
|                     (e) => { | ||||
|                         console.error("UPLOADING FAILED!", e) | ||||
|                         onFail() | ||||
|                     } | ||||
|                 ) | ||||
|             }, { | ||||
|                 onFail: onFail | ||||
|             }) | ||||
|         } else { | ||||
|             // There still exists an open changeset (or at least we hope so)
 | ||||
|  | @ -107,15 +112,13 @@ export class ChangesetHandler { | |||
|                 csId, | ||||
|                 generateChangeXML(csId), | ||||
|                 allElements, | ||||
|                 () => { | ||||
|                 }, | ||||
|                 whenDone, | ||||
|                 (e) => { | ||||
|                     console.warn("Could not upload, changeset is probably closed: ", e); | ||||
|                     // Mark the CS as closed...
 | ||||
|                     this.currentChangeset.setData(""); | ||||
|                     // ... and try again. As the cs is closed, no recursive loop can exist  
 | ||||
|                     self.UploadChangeset(layout, allElements, generateChangeXML); | ||||
| 
 | ||||
|                     self.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail); | ||||
|                 } | ||||
|             ) | ||||
| 
 | ||||
|  | @ -161,18 +164,22 @@ export class ChangesetHandler { | |||
|         const self = this; | ||||
|         this.OpenChangeset(layout, (csId: string) => { | ||||
| 
 | ||||
|             // The cs is open - let us actually upload!
 | ||||
|             const changes = generateChangeXML(csId) | ||||
|                 // The cs is open - let us actually upload!
 | ||||
|                 const changes = generateChangeXML(csId) | ||||
| 
 | ||||
|             self.AddChange(csId, changes, allElements, (csId) => { | ||||
|                 console.log("Successfully deleted ", object.id) | ||||
|                 self.CloseChangeset(csId, continuation) | ||||
|             }, (csId) => { | ||||
|                 alert("Deletion failed... Should not happend") | ||||
|                 // FAILED
 | ||||
|                 self.CloseChangeset(csId, continuation) | ||||
|             }) | ||||
|         }, true, reason) | ||||
|                 self.AddChange(csId, changes, allElements, (csId) => { | ||||
|                     console.log("Successfully deleted ", object.id) | ||||
|                     self.CloseChangeset(csId, continuation) | ||||
|                 }, (csId) => { | ||||
|                     alert("Deletion failed... Should not happend") | ||||
|                     // FAILED
 | ||||
|                     self.CloseChangeset(csId, continuation) | ||||
|                 }) | ||||
|             }, { | ||||
|                 isDeletionCS: true, | ||||
|                 deletionReason: reason | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     private CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => { | ||||
|  | @ -204,15 +211,20 @@ export class ChangesetHandler { | |||
|     private OpenChangeset( | ||||
|         layout: LayoutConfig, | ||||
|         continuation: (changesetId: string) => void, | ||||
|         isDeletionCS: boolean = false, | ||||
|         deletionReason: string = undefined) { | ||||
| 
 | ||||
|         options?: { | ||||
|             isDeletionCS?: boolean, | ||||
|             deletionReason?: string, | ||||
|             onFail?: () => void | ||||
|         } | ||||
|     ) { | ||||
|         options = options ?? {} | ||||
|         options.isDeletionCS = options.isDeletionCS ?? false | ||||
|         const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : ""; | ||||
|         let comment = `Adding data with #MapComplete for theme #${layout.id}${commentExtra}` | ||||
|         if (isDeletionCS) { | ||||
|         if (options.isDeletionCS) { | ||||
|             comment = `Deleting a point with #MapComplete for theme #${layout.id}${commentExtra}` | ||||
|             if (deletionReason) { | ||||
|                 comment += ": " + deletionReason; | ||||
|             if (options.deletionReason) { | ||||
|                 comment += ": " + options.deletionReason; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -221,7 +233,7 @@ export class ChangesetHandler { | |||
|         const metadata = [ | ||||
|             ["created_by", `MapComplete ${Constants.vNumber}`], | ||||
|             ["comment", comment], | ||||
|             ["deletion", isDeletionCS ? "yes" : undefined], | ||||
|             ["deletion", options.isDeletionCS ? "yes" : undefined], | ||||
|             ["theme", layout.id], | ||||
|             ["language", Locale.language.data], | ||||
|             ["host", window.location.host], | ||||
|  | @ -244,7 +256,9 @@ export class ChangesetHandler { | |||
|         }, function (err, response) { | ||||
|             if (response === undefined) { | ||||
|                 console.log("err", err); | ||||
|                 alert("Could not upload change (opening failed). Please file a bug report") | ||||
|                 if(options.onFail){ | ||||
|                     options.onFail() | ||||
|                 } | ||||
|                 return; | ||||
|             } else { | ||||
|                 continuation(response); | ||||
|  | @ -265,7 +279,7 @@ export class ChangesetHandler { | |||
|     private AddChange(changesetId: string, | ||||
|                       changesetXML: string, | ||||
|                       allElements: ElementStorage, | ||||
|                       continuation: ((changesetId: string, idMapping: any) => void), | ||||
|                       continuation: ((changesetId: string) => void), | ||||
|                       onFail: ((changesetId: string, reason: string) => void) = undefined) { | ||||
|         this.auth.xhr({ | ||||
|             method: 'POST', | ||||
|  | @ -280,9 +294,9 @@ export class ChangesetHandler { | |||
|                 } | ||||
|                 return; | ||||
|             } | ||||
|             const mapping = ChangesetHandler.parseUploadChangesetResponse(response, allElements); | ||||
|             ChangesetHandler.parseUploadChangesetResponse(response, allElements); | ||||
|             console.log("Uploaded changeset ", changesetId); | ||||
|             continuation(changesetId, mapping); | ||||
|             continuation(changesetId); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ export default class UserDetails { | |||
| 
 | ||||
| export class OsmConnection { | ||||
| 
 | ||||
|     public static readonly _oauth_configs = { | ||||
|     public static readonly oauth_configs = { | ||||
|         "osm": { | ||||
|             oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem', | ||||
|             oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI', | ||||
|  | @ -47,6 +47,7 @@ export class OsmConnection { | |||
|     public auth; | ||||
|     public userDetails: UIEventSource<UserDetails>; | ||||
|     public isLoggedIn: UIEventSource<boolean> | ||||
|     private fakeUser: boolean; | ||||
|     _dryRun: boolean; | ||||
|     public preferencesHandler: OsmPreferences; | ||||
|     public changesetHandler: ChangesetHandler; | ||||
|  | @ -59,20 +60,31 @@ export class OsmConnection { | |||
|         url: string | ||||
|     }; | ||||
| 
 | ||||
|     constructor(dryRun: boolean, oauth_token: UIEventSource<string>, | ||||
|     constructor(dryRun: boolean,  | ||||
|                 fakeUser: boolean, | ||||
|                 oauth_token: UIEventSource<string>, | ||||
|                 // Used to keep multiple changesets open and to write to the correct changeset
 | ||||
|                 layoutName: string, | ||||
|                 singlePage: boolean = true, | ||||
|                 osmConfiguration: "osm" | "osm-test" = 'osm' | ||||
|     ) { | ||||
|         this.fakeUser = fakeUser; | ||||
|         this._singlePage = singlePage; | ||||
|         this._oauth_config = OsmConnection._oauth_configs[osmConfiguration] ?? OsmConnection._oauth_configs.osm; | ||||
|         this._oauth_config = OsmConnection.oauth_configs[osmConfiguration] ?? OsmConnection.oauth_configs.osm; | ||||
|         console.debug("Using backend", this._oauth_config.url) | ||||
|         OsmObject.SetBackendUrl(this._oauth_config.url + "/") | ||||
|         this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; | ||||
| 
 | ||||
|         this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails"); | ||||
|         this.userDetails.data.dryRun = dryRun; | ||||
|         this.userDetails.data.dryRun = dryRun || fakeUser; | ||||
|         if(fakeUser){ | ||||
|             const ud = this.userDetails.data; | ||||
|             ud.csCount = 5678 | ||||
|             ud.loggedIn= true; | ||||
|             ud.unreadMessages = 0 | ||||
|             ud.name = "Fake user" | ||||
|             ud.totalMessages = 42; | ||||
|         } | ||||
|         const self =this; | ||||
|         this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => { | ||||
|             if(self.userDetails.data.loggedIn == false && isLoggedIn == true){ | ||||
|  | @ -110,8 +122,10 @@ export class OsmConnection { | |||
|     public UploadChangeset( | ||||
|         layout: LayoutConfig, | ||||
|         allElements: ElementStorage, | ||||
|         generateChangeXML: (csid: string) => string) { | ||||
|         this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML); | ||||
|         generateChangeXML: (csid: string) => string, | ||||
|         whenDone: (csId: string) => void, | ||||
|         onFail: () => {}) { | ||||
|         this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail); | ||||
|     } | ||||
| 
 | ||||
|     public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> { | ||||
|  | @ -136,6 +150,10 @@ export class OsmConnection { | |||
|     } | ||||
| 
 | ||||
|     public AttemptLogin() { | ||||
|         if(this.fakeUser){ | ||||
|             console.log("AttemptLogin called, but ignored as fakeUser is set") | ||||
|             return; | ||||
|         } | ||||
|         const self = this; | ||||
|         console.log("Trying to log in..."); | ||||
|         this.updateAuthObject(); | ||||
|  |  | |||
|  | @ -5,7 +5,8 @@ import {UIEventSource} from "../UIEventSource"; | |||
| 
 | ||||
| export abstract class OsmObject { | ||||
| 
 | ||||
|     protected static backendURL = "https://www.openstreetmap.org/" | ||||
|     private static defaultBackend = "https://www.openstreetmap.org/" | ||||
|     protected static backendURL = OsmObject.defaultBackend; | ||||
|     private static polygonFeatures = OsmObject.constructPolygonFeatures() | ||||
|     private static objectCache = new Map<string, UIEventSource<OsmObject>>(); | ||||
|     private static referencingWaysCache = new Map<string, UIEventSource<OsmWay[]>>(); | ||||
|  | @ -37,15 +38,15 @@ export abstract class OsmObject { | |||
|     } | ||||
| 
 | ||||
|     static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> { | ||||
|         let src : UIEventSource<OsmObject>; | ||||
|         let src: UIEventSource<OsmObject>; | ||||
|         if (OsmObject.objectCache.has(id)) { | ||||
|             src = OsmObject.objectCache.get(id) | ||||
|             if(forceRefresh){ | ||||
|             if (forceRefresh) { | ||||
|                 src.setData(undefined) | ||||
|             }else{ | ||||
|             } else { | ||||
|                 return src; | ||||
|             } | ||||
|         }else{ | ||||
|         } else { | ||||
|             src = new UIEventSource<OsmObject>(undefined) | ||||
|         } | ||||
|         const splitted = id.split("/"); | ||||
|  | @ -157,7 +158,7 @@ export abstract class OsmObject { | |||
|         const minlat = bounds[1][0] | ||||
|         const maxlat = bounds[0][0]; | ||||
|         const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}` | ||||
|         Utils.downloadJson(url).then( data => { | ||||
|         Utils.downloadJson(url).then(data => { | ||||
|             const elements: any[] = data.elements; | ||||
|             const objects = OsmObject.ParseObjects(elements) | ||||
|             callback(objects); | ||||
|  | @ -291,6 +292,7 @@ export abstract class OsmObject { | |||
| 
 | ||||
|                 self.LoadData(element) | ||||
|                 self.SaveExtraData(element, nodes); | ||||
| 
 | ||||
|                 const meta = { | ||||
|                     "_last_edit:contributor": element.user, | ||||
|                     "_last_edit:contributor:uid": element.uid, | ||||
|  | @ -299,6 +301,11 @@ export abstract class OsmObject { | |||
|                     "_version_number": element.version | ||||
|                 } | ||||
| 
 | ||||
|                 if (OsmObject.backendURL !== OsmObject.defaultBackend) { | ||||
|                     self.tags["_backend"] = OsmObject.backendURL | ||||
|                     meta["_backend"] = OsmObject.backendURL; | ||||
|                 } | ||||
| 
 | ||||
|                 continuation(self, meta); | ||||
|             } | ||||
|         ); | ||||
|  |  | |||
|  | @ -83,7 +83,8 @@ export default class SimpleMetaTagger { | |||
| 
 | ||||
|         }, | ||||
|         (feature => { | ||||
|             const units = State.state.layoutToUse.data.units ?? []; | ||||
|             const units = State.state?.layoutToUse?.data?.units ?? []; | ||||
|             let rewritten = false; | ||||
|             for (const key in feature.properties) { | ||||
|                 if (!feature.properties.hasOwnProperty(key)) { | ||||
|                     continue; | ||||
|  | @ -95,16 +96,23 @@ export default class SimpleMetaTagger { | |||
|                     const value = feature.properties[key] | ||||
|                     const [, denomination] = unit.findDenomination(value) | ||||
|                     let canonical = denomination?.canonicalValue(value) ?? undefined; | ||||
|                     console.log("Rewritten ", key, " from", value, "into", canonical) | ||||
|                     if(canonical === undefined && !unit.eraseInvalid) { | ||||
|                     if(canonical === value){ | ||||
|                         break; | ||||
|                     } | ||||
|                      | ||||
|                     console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`) | ||||
|                     if (canonical === undefined && !unit.eraseInvalid) { | ||||
|                         break; | ||||
|                     } | ||||
| 
 | ||||
|                     feature.properties[key] = canonical; | ||||
|                     rewritten = true; | ||||
|                     break; | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
|             if(rewritten){ | ||||
|                 State.state.allElements.getEventSourceById(feature.id).ping(); | ||||
|             } | ||||
|         }) | ||||
|     ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,6 +4,22 @@ import {UIEventSource} from "../UIEventSource"; | |||
|  * UIEventsource-wrapper around localStorage | ||||
|  */ | ||||
| export class LocalStorageSource { | ||||
|      | ||||
|     static GetParsed<T>(key: string, defaultValue : T) : UIEventSource<T>{ | ||||
|         return LocalStorageSource.Get(key).map( | ||||
|             str => { | ||||
|                 if(str === undefined){ | ||||
|                     return defaultValue | ||||
|                 } | ||||
|                 try{ | ||||
|                     return JSON.parse(str) | ||||
|                 }catch{ | ||||
|                     return defaultValue | ||||
|                 } | ||||
|             }, [],  | ||||
|             value => JSON.stringify(value) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     static Get(key: string, defaultValue: string = undefined): UIEventSource<string> { | ||||
|         try { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue