Reformat all files with prettier

This commit is contained in:
Pieter Vander Vennet 2022-09-08 21:40:48 +02:00
parent e22d189376
commit b541d3eab4
382 changed files with 50893 additions and 35566 deletions

View file

@ -1,15 +1,17 @@
import BaseLayer from "../../Models/BaseLayer";
import {ImmutableStore, Store, UIEventSource} from "../UIEventSource";
import Loc from "../../Models/Loc";
import BaseLayer from "../../Models/BaseLayer"
import { ImmutableStore, Store, UIEventSource } from "../UIEventSource"
import Loc from "../../Models/Loc"
export interface AvailableBaseLayersObj {
readonly osmCarto: BaseLayer;
layerOverview: BaseLayer[];
readonly osmCarto: BaseLayer
layerOverview: BaseLayer[]
AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]>
SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: Store<string | string[]>): Store<BaseLayer>;
SelectBestLayerAccordingTo(
location: Store<Loc>,
preferedCategory: Store<string | string[]>
): Store<BaseLayer>
}
/**
@ -17,20 +19,28 @@ export interface AvailableBaseLayersObj {
* Changes the basemap
*/
export default class AvailableBaseLayers {
public static layerOverview: BaseLayer[];
public static osmCarto: BaseLayer;
public static layerOverview: BaseLayer[]
public static osmCarto: BaseLayer
private static implementation: AvailableBaseLayersObj
static AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> {
return AvailableBaseLayers.implementation?.AvailableLayersAt(location) ?? new ImmutableStore<BaseLayer[]>([]);
return (
AvailableBaseLayers.implementation?.AvailableLayersAt(location) ??
new ImmutableStore<BaseLayer[]>([])
)
}
static SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: UIEventSource<string | string[]>): Store<BaseLayer> {
return AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo(location, preferedCategory) ?? new ImmutableStore<BaseLayer>(undefined);
static SelectBestLayerAccordingTo(
location: Store<Loc>,
preferedCategory: UIEventSource<string | string[]>
): Store<BaseLayer> {
return (
AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo(
location,
preferedCategory
) ?? new ImmutableStore<BaseLayer>(undefined)
)
}
public static implement(backend: AvailableBaseLayersObj) {
@ -38,5 +48,4 @@ export default class AvailableBaseLayers {
AvailableBaseLayers.osmCarto = backend.osmCarto
AvailableBaseLayers.implementation = backend
}
}
}

View file

@ -1,66 +1,77 @@
import BaseLayer from "../../Models/BaseLayer";
import {Store, Stores} from "../UIEventSource";
import Loc from "../../Models/Loc";
import {GeoOperations} from "../GeoOperations";
import * as editorlayerindex from "../../assets/editor-layer-index.json";
import * as L from "leaflet";
import {TileLayer} from "leaflet";
import * as X from "leaflet-providers";
import {Utils} from "../../Utils";
import {AvailableBaseLayersObj} from "./AvailableBaseLayers";
import {BBox} from "../BBox";
import BaseLayer from "../../Models/BaseLayer"
import { Store, Stores } from "../UIEventSource"
import Loc from "../../Models/Loc"
import { GeoOperations } from "../GeoOperations"
import * as editorlayerindex from "../../assets/editor-layer-index.json"
import * as L from "leaflet"
import { TileLayer } from "leaflet"
import * as X from "leaflet-providers"
import { Utils } from "../../Utils"
import { AvailableBaseLayersObj } from "./AvailableBaseLayers"
import { BBox } from "../BBox"
export default class AvailableBaseLayersImplementation implements AvailableBaseLayersObj {
public readonly osmCarto: BaseLayer =
{
id: "osm",
name: "OpenStreetMap",
layer: () => AvailableBaseLayersImplementation.CreateBackgroundLayer("osm", "OpenStreetMap",
"https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap", "https://openStreetMap.org/copyright",
public readonly osmCarto: BaseLayer = {
id: "osm",
name: "OpenStreetMap",
layer: () =>
AvailableBaseLayersImplementation.CreateBackgroundLayer(
"osm",
"OpenStreetMap",
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
"OpenStreetMap",
"https://openStreetMap.org/copyright",
19,
false, false),
feature: null,
max_zoom: 19,
min_zoom: 0,
isBest: true, // Of course, OpenStreetMap is the best map!
category: "osmbasedmap"
}
false,
false
),
feature: null,
max_zoom: 19,
min_zoom: 0,
isBest: true, // Of course, OpenStreetMap is the best map!
category: "osmbasedmap",
}
public readonly layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat(AvailableBaseLayersImplementation.LoadProviderIndex());
public readonly globalLayers = this.layerOverview.filter(layer => layer.feature?.geometry === undefined || layer.feature?.geometry === null)
public readonly localLayers = this.layerOverview.filter(layer => layer.feature?.geometry !== undefined && layer.feature?.geometry !== null)
public readonly layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat(
AvailableBaseLayersImplementation.LoadProviderIndex()
)
public readonly globalLayers = this.layerOverview.filter(
(layer) => layer.feature?.geometry === undefined || layer.feature?.geometry === null
)
public readonly localLayers = this.layerOverview.filter(
(layer) => layer.feature?.geometry !== undefined && layer.feature?.geometry !== null
)
private static LoadRasterIndex(): BaseLayer[] {
const layers: BaseLayer[] = []
// @ts-ignore
const features = editorlayerindex.features;
const features = editorlayerindex.features
for (const i in features) {
const layer = features[i];
const props = layer.properties;
const layer = features[i]
const props = layer.properties
if (props.type === "bing") {
// A lot of work to implement - see https://github.com/pietervdvn/MapComplete/issues/648
continue;
continue
}
if (props.id === "MAPNIK") {
// Already added by default
continue;
continue
}
if (props.overlay) {
continue;
continue
}
if (props.url.toLowerCase().indexOf("apikey") > 0) {
continue;
continue
}
if (props.max_zoom < 19) {
// We want users to zoom to level 19 when adding a point
// If they are on a layer which hasn't enough precision, they can not zoom far enough. This is confusing, so we don't use this layer
continue;
continue
}
if (props.name === undefined) {
@ -68,17 +79,17 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
continue
}
const leafletLayer: () => TileLayer = () => AvailableBaseLayersImplementation.CreateBackgroundLayer(
props.id,
props.name,
props.url,
props.name,
props.license_url,
props.max_zoom,
props.type.toLowerCase() === "wms",
props.type.toLowerCase() === "wmts"
)
const leafletLayer: () => TileLayer = () =>
AvailableBaseLayersImplementation.CreateBackgroundLayer(
props.id,
props.name,
props.url,
props.name,
props.license_url,
props.max_zoom,
props.type.toLowerCase() === "wms",
props.type.toLowerCase() === "wmts"
)
// Note: if layer.geometry is null, there is global coverage for this layer
layers.push({
@ -89,34 +100,35 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
layer: leafletLayer,
feature: layer.geometry !== null ? layer : null,
isBest: props.best ?? false,
category: props.category
});
category: props.category,
})
}
return layers;
return layers
}
private static LoadProviderIndex(): BaseLayer[] {
// @ts-ignore
X; // Import X to make sure the namespace is not optimized away
X // Import X to make sure the namespace is not optimized away
function l(id: string, name: string): BaseLayer {
try {
const layer: any = L.tileLayer.provider(id, undefined);
const layer: any = L.tileLayer.provider(id, undefined)
return {
feature: null,
id: id,
name: name,
layer: () => L.tileLayer.provider(id, {
maxNativeZoom: layer.options?.maxZoom,
maxZoom: Math.max(layer.options?.maxZoom ?? 19, 21)
}),
layer: () =>
L.tileLayer.provider(id, {
maxNativeZoom: layer.options?.maxZoom,
maxZoom: Math.max(layer.options?.maxZoom ?? 19, 21),
}),
min_zoom: 1,
max_zoom: layer.options.maxZoom,
category: "osmbasedmap",
isBest: false
isBest: false,
}
} catch (e) {
console.error("Could not find provided layer", name, e);
return null;
console.error("Could not find provided layer", name, e)
return null
}
}
@ -129,38 +141,50 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
l("CartoDB.PositronNoLabels", "Positron - no labels (by CartoDB)"),
l("CartoDB.Voyager", "Voyager (by CartoDB)"),
l("CartoDB.VoyagerNoLabels", "Voyager - no labels (by CartoDB)"),
];
return Utils.NoNull(layers);
]
return Utils.NoNull(layers)
}
/**
* Converts a layer from the editor-layer-index into a tilelayer usable by leaflet
*/
private static CreateBackgroundLayer(id: string, name: string, url: string, attribution: string, attributionUrl: string,
maxZoom: number, isWms: boolean, isWMTS?: boolean): TileLayer {
url = url.replace("{zoom}", "{z}")
.replace("&BBOX={bbox}", "")
.replace("&bbox={bbox}", "");
private static CreateBackgroundLayer(
id: string,
name: string,
url: string,
attribution: string,
attributionUrl: string,
maxZoom: number,
isWms: boolean,
isWMTS?: boolean
): TileLayer {
url = url.replace("{zoom}", "{z}").replace("&BBOX={bbox}", "").replace("&bbox={bbox}", "")
const subdomainsMatch = url.match(/{switch:[^}]*}/)
let domains: string[] = [];
let domains: string[] = []
if (subdomainsMatch !== null) {
let domainsStr = subdomainsMatch[0].substr("{switch:".length);
domainsStr = domainsStr.substr(0, domainsStr.length - 1);
domains = domainsStr.split(",");
let domainsStr = subdomainsMatch[0].substr("{switch:".length)
domainsStr = domainsStr.substr(0, domainsStr.length - 1)
domains = domainsStr.split(",")
url = url.replace(/{switch:[^}]*}/, "{s}")
}
if (isWms) {
url = url.replace("&SRS={proj}", "");
url = url.replace("&srs={proj}", "");
const paramaters = ["format", "layers", "version", "service", "request", "styles", "transparent", "version"];
const urlObj = new URL(url);
url = url.replace("&SRS={proj}", "")
url = url.replace("&srs={proj}", "")
const paramaters = [
"format",
"layers",
"version",
"service",
"request",
"styles",
"transparent",
"version",
]
const urlObj = new URL(url)
const isUpper = urlObj.searchParams["LAYERS"] !== null;
const isUpper = urlObj.searchParams["LAYERS"] !== null
const options = {
maxZoom: Math.max(maxZoom ?? 19, 21),
maxNativeZoom: maxZoom ?? 19,
@ -168,116 +192,117 @@ export default class AvailableBaseLayersImplementation implements AvailableBaseL
subdomains: domains,
uppercase: isUpper,
transparent: false,
};
}
for (const paramater of paramaters) {
let p = paramater;
let p = paramater
if (isUpper) {
p = paramater.toUpperCase();
p = paramater.toUpperCase()
}
options[paramater] = urlObj.searchParams.get(p);
options[paramater] = urlObj.searchParams.get(p)
}
if (options.transparent === null) {
options.transparent = false;
options.transparent = false
}
return L.tileLayer.wms(urlObj.protocol + "//" + urlObj.host + urlObj.pathname, options);
return L.tileLayer.wms(urlObj.protocol + "//" + urlObj.host + urlObj.pathname, options)
}
if (attributionUrl) {
attribution = `<a href='${attributionUrl}' target='_blank'>${attribution}</a>`;
attribution = `<a href='${attributionUrl}' target='_blank'>${attribution}</a>`
}
return L.tileLayer(url,
{
attribution: attribution,
maxZoom: Math.max(21, maxZoom ?? 19),
maxNativeZoom: maxZoom ?? 19,
minZoom: 1,
// @ts-ignore
wmts: isWMTS ?? false,
subdomains: domains
});
return L.tileLayer(url, {
attribution: attribution,
maxZoom: Math.max(21, maxZoom ?? 19),
maxNativeZoom: maxZoom ?? 19,
minZoom: 1,
// @ts-ignore
wmts: isWMTS ?? false,
subdomains: domains,
})
}
public AvailableLayersAt(location: Store<Loc>): Store<BaseLayer[]> {
return Stores.ListStabilized(location.map(
(currentLocation) => {
return Stores.ListStabilized(
location.map((currentLocation) => {
if (currentLocation === undefined) {
return this.layerOverview;
return this.layerOverview
}
return this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
}));
return this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat)
})
)
}
public SelectBestLayerAccordingTo(location: Store<Loc>, preferedCategory: Store<string | string[]>): Store<BaseLayer> {
return this.AvailableLayersAt(location)
.map(available => {
public SelectBestLayerAccordingTo(
location: Store<Loc>,
preferedCategory: Store<string | string[]>
): Store<BaseLayer> {
return this.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 (a.isBest && b.isBest) {
return 0
}
)
if (!a.isBest) {
return 1
}
return -1
})
if (preferedCategory.data === undefined) {
return available[0]
}
let prefered: string []
let prefered: string[]
if (typeof preferedCategory.data === "string") {
prefered = [preferedCategory.data]
} else {
prefered = preferedCategory.data;
prefered = preferedCategory.data
}
prefered.reverse(/*New list, inplace reverse is fine*/);
prefered.reverse(/*New list, inplace reverse is fine*/)
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;
if (a.category === category && b.category === category) {
return 0
}
)
if (a.category !== category) {
return 1
}
return -1
})
}
return available[0]
}, [preferedCategory])
},
[preferedCategory]
)
}
private CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] {
const availableLayers = [this.osmCarto]
if (lon === undefined || lat === undefined) {
return availableLayers.concat(this.globalLayers);
return availableLayers.concat(this.globalLayers)
}
const lonlat : [number, number] = [lon, lat];
const lonlat: [number, number] = [lon, lat]
for (const layerOverviewItem of this.localLayers) {
const layer = layerOverviewItem;
const layer = layerOverviewItem
const bbox = BBox.get(layer.feature)
if(!bbox.contains(lonlat)){
if (!bbox.contains(lonlat)) {
continue
}
if (GeoOperations.inside(lonlat, layer.feature)) {
availableLayers.push(layer);
availableLayers.push(layer)
}
}
return availableLayers.concat(this.globalLayers);
return availableLayers.concat(this.globalLayers)
}
}
}

View file

@ -1,50 +1,49 @@
import {UIEventSource} from "../UIEventSource";
import BaseLayer from "../../Models/BaseLayer";
import AvailableBaseLayers from "./AvailableBaseLayers";
import Loc from "../../Models/Loc";
import {Utils} from "../../Utils";
import { UIEventSource } from "../UIEventSource"
import BaseLayer from "../../Models/BaseLayer"
import AvailableBaseLayers from "./AvailableBaseLayers"
import Loc from "../../Models/Loc"
import { Utils } from "../../Utils"
/**
* Sets the current background layer to a layer that is actually available
*/
export default class BackgroundLayerResetter {
constructor(currentBackgroundLayer: UIEventSource<BaseLayer>,
location: UIEventSource<Loc>,
availableLayers: UIEventSource<BaseLayer[]>,
defaultLayerId: string = undefined) {
constructor(
currentBackgroundLayer: UIEventSource<BaseLayer>,
location: UIEventSource<Loc>,
availableLayers: UIEventSource<BaseLayer[]>,
defaultLayerId: string = undefined
) {
if (Utils.runningFromConsole) {
return
}
defaultLayerId = defaultLayerId ?? AvailableBaseLayers.osmCarto.id;
defaultLayerId = defaultLayerId ?? AvailableBaseLayers.osmCarto.id
// Change the baselayer back to OSM if we go out of the current range of the layer
availableLayers.addCallbackAndRun(availableLayers => {
let defaultLayer = undefined;
const currentLayer = currentBackgroundLayer.data.id;
availableLayers.addCallbackAndRun((availableLayers) => {
let defaultLayer = undefined
const currentLayer = currentBackgroundLayer.data.id
for (const availableLayer of availableLayers) {
if (availableLayer.id === currentLayer) {
if (availableLayer.max_zoom < location.data.zoom) {
break;
break
}
if (availableLayer.min_zoom > location.data.zoom) {
break;
break
}
if (availableLayer.id === defaultLayerId) {
defaultLayer = availableLayer;
defaultLayer = availableLayer
}
return; // All good - the current layer still works!
return // All good - the current layer still works!
}
}
// Oops, we panned out of range for this layer!
console.log("AvailableBaseLayers-actor: detected that the current bounds aren't sufficient anymore - reverting to OSM standard")
currentBackgroundLayer.setData(defaultLayer ?? AvailableBaseLayers.osmCarto);
});
console.log(
"AvailableBaseLayers-actor: detected that the current bounds aren't sufficient anymore - reverting to OSM standard"
)
currentBackgroundLayer.setData(defaultLayer ?? AvailableBaseLayers.osmCarto)
})
}
}
}

View file

@ -1,36 +1,34 @@
import {ElementStorage} from "../ElementStorage";
import {Changes} from "../Osm/Changes";
import { ElementStorage } from "../ElementStorage"
import { Changes } from "../Osm/Changes"
export default class ChangeToElementsActor {
constructor(changes: Changes, allElements: ElementStorage) {
changes.pendingChanges.addCallbackAndRun(changes => {
changes.pendingChanges.addCallbackAndRun((changes) => {
for (const change of changes) {
const id = change.type + "/" + change.id;
const id = change.type + "/" + change.id
if (!allElements.has(id)) {
continue; // Ignored as the geometryFixer will introduce this
continue // Ignored as the geometryFixer will introduce this
}
const src = allElements.getEventSourceById(id)
let changed = false;
let changed = false
for (const kv of change.tags ?? []) {
// Apply tag changes and ping the consumers
const k = kv.k
let v = kv.v
if (v === "") {
v = undefined;
v = undefined
}
if (src.data[k] === v) {
continue
}
changed = true;
src.data[k] = v;
changed = true
src.data[k] = v
}
if (changed) {
src.ping()
}
}
})
}
}
}

View file

@ -1,60 +1,59 @@
import {Store, UIEventSource} from "../UIEventSource";
import Svg from "../../Svg";
import {LocalStorageSource} from "../Web/LocalStorageSource";
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {QueryParameters} from "../Web/QueryParameters";
import {BBox} from "../BBox";
import Constants from "../../Models/Constants";
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource";
import { Store, UIEventSource } from "../UIEventSource"
import Svg from "../../Svg"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { VariableUiElement } from "../../UI/Base/VariableUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { QueryParameters } from "../Web/QueryParameters"
import { BBox } from "../BBox"
import Constants from "../../Models/Constants"
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"
export interface GeoLocationPointProperties {
id: "gps",
"user:location": "yes",
"date": string,
"latitude": number
"longitude": number,
"speed": number,
"accuracy": number
"heading": number
"altitude": number
export interface GeoLocationPointProperties {
id: "gps"
"user:location": "yes"
date: string
latitude: number
longitude: number
speed: number
accuracy: number
heading: number
altitude: number
}
export default class GeoLocationHandler extends VariableUiElement {
private readonly currentLocation?: SimpleFeatureSource
/**
* Wether or not the geolocation is active, aka the user requested the current location
*/
private readonly _isActive: UIEventSource<boolean>;
private readonly _isActive: UIEventSource<boolean>
/**
* Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user
*/
private readonly _isLocked: UIEventSource<boolean>;
private readonly _isLocked: UIEventSource<boolean>
/**
* The callback over the permission API
* @private
*/
private readonly _permission: UIEventSource<string>;
private readonly _permission: UIEventSource<string>
/**
* Literally: _currentGPSLocation.data != undefined
* @private
*/
private readonly _hasLocation: Store<boolean>;
private readonly _currentGPSLocation: UIEventSource<GeolocationCoordinates>;
private readonly _hasLocation: Store<boolean>
private readonly _currentGPSLocation: UIEventSource<GeolocationCoordinates>
/**
* Kept in order to update the marker
* @private
*/
private readonly _leafletMap: UIEventSource<L.Map>;
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 _lastUserRequest: UIEventSource<Date>;
private _lastUserRequest: UIEventSource<Date>
/**
* A small flag on localstorage. If the user previously granted the geolocation, it will be set.
@ -64,54 +63,52 @@ export default class GeoLocationHandler extends VariableUiElement {
* If the user denies the geolocation this time, we unset this flag
* @private
*/
private readonly _previousLocationGrant: UIEventSource<string>;
private readonly _layoutToUse: LayoutConfig;
private readonly _previousLocationGrant: UIEventSource<string>
private readonly _layoutToUse: LayoutConfig
constructor(
state: {
selectedElement: UIEventSource<any>;
currentUserLocation?: SimpleFeatureSource,
leafletMap: UIEventSource<any>,
layoutToUse: LayoutConfig,
featureSwitchGeolocation: UIEventSource<boolean>
}
) {
const currentGPSLocation = new UIEventSource<GeolocationCoordinates>(undefined, "GPS-coordinate")
constructor(state: {
selectedElement: UIEventSource<any>
currentUserLocation?: SimpleFeatureSource
leafletMap: UIEventSource<any>
layoutToUse: LayoutConfig
featureSwitchGeolocation: UIEventSource<boolean>
}) {
const currentGPSLocation = new UIEventSource<GeolocationCoordinates>(
undefined,
"GPS-coordinate"
)
const leafletMap = state.leafletMap
const initedAt = new Date()
let autozoomDone = false;
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);
const permission = new UIEventSource<string>("");
const lastClick = new UIEventSource<Date>(undefined);
const lastClickWithinThreeSecs = lastClick.map(lastClick => {
let autozoomDone = false
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)
const permission = new UIEventSource<string>("")
const lastClick = new UIEventSource<Date>(undefined)
const lastClickWithinThreeSecs = lastClick.map((lastClick) => {
if (lastClick === undefined) {
return false;
return false
}
const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000
return timeDiff <= 3
})
const latLonGiven = QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon")
const willFocus = lastClick.map(lastUserRequest => {
const latLonGiven =
QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon")
const willFocus = lastClick.map((lastUserRequest) => {
const timeDiffInited = (new Date().getTime() - initedAt.getTime()) / 1000
if (!latLonGiven && !autozoomDone && timeDiffInited < Constants.zoomToLocationTimeout) {
return true
}
if (lastUserRequest === undefined) {
return false;
return false
}
const timeDiff = (new Date().getTime() - lastUserRequest.getTime()) / 1000
return timeDiff <= Constants.zoomToLocationTimeout
})
lastClick.addCallbackAndRunD(_ => {
lastClick.addCallbackAndRunD((_) => {
window.setTimeout(() => {
if (lastClickWithinThreeSecs.data || willFocus.data) {
lastClick.ping()
@ -123,7 +120,7 @@ export default class GeoLocationHandler extends VariableUiElement {
hasLocation.map(
(hasLocationData) => {
if (permission.data === "denied") {
return Svg.location_refused_svg();
return Svg.location_refused_svg()
}
if (!isActive.data) {
@ -134,7 +131,7 @@ export default class GeoLocationHandler extends VariableUiElement {
// If will focus is active too, we indicate this differently
const icon = willFocus.data ? Svg.location_svg() : Svg.location_empty_svg()
icon.SetStyle("animation: spin 4s linear infinite;")
return icon;
return icon
}
if (isLocked.data) {
return Svg.location_locked_svg()
@ -144,42 +141,41 @@ export default class GeoLocationHandler extends VariableUiElement {
}
// We have a location, so we show a dot in the center
return Svg.location_svg();
return Svg.location_svg()
},
[isActive, isLocked, permission, lastClickWithinThreeSecs, willFocus]
)
);
)
this.SetClass("mapcontrol")
this._isActive = isActive;
this._isLocked = isLocked;
this._isActive = isActive
this._isLocked = isLocked
this._permission = permission
this._previousLocationGrant = previousLocationGrant;
this._currentGPSLocation = currentGPSLocation;
this._leafletMap = leafletMap;
this._layoutToUse = state.layoutToUse;
this._hasLocation = hasLocation;
this._previousLocationGrant = previousLocationGrant
this._currentGPSLocation = currentGPSLocation
this._leafletMap = leafletMap
this._layoutToUse = state.layoutToUse
this._hasLocation = hasLocation
this._lastUserRequest = lastClick
const self = this;
const self = this
const currentPointer = this._isActive.map(
(isActive) => {
if (isActive && !self._hasLocation.data) {
return "cursor-wait";
return "cursor-wait"
}
return "cursor-pointer";
return "cursor-pointer"
},
[this._hasLocation]
);
)
currentPointer.addCallbackAndRun((pointerClass) => {
self.RemoveClass("cursor-wait")
self.RemoveClass("cursor-pointer")
self.SetClass(pointerClass);
});
self.SetClass(pointerClass)
})
this.onClick(() => {
/*
* If the previous click was within 3 seconds (and we have an active location), then we lock to the location
* If the previous click was within 3 seconds (and we have an active location), then we lock to the location
*/
if (self._hasLocation.data) {
if (isLocked.data) {
@ -197,14 +193,16 @@ export default class GeoLocationHandler extends VariableUiElement {
}
}
self.init(true, true);
});
self.init(true, true)
})
const doAutoZoomToLocation =
!latLonGiven &&
state.featureSwitchGeolocation.data &&
state.selectedElement.data !== undefined
this.init(false, doAutoZoomToLocation)
const doAutoZoomToLocation = !latLonGiven && state.featureSwitchGeolocation.data && state.selectedElement.data !== undefined
this.init(false, doAutoZoomToLocation);
isLocked.addCallbackAndRunD(isLocked => {
isLocked.addCallbackAndRunD((isLocked) => {
if (isLocked) {
leafletMap.data?.dragging?.disable()
} else {
@ -214,47 +212,45 @@ export default class GeoLocationHandler extends VariableUiElement {
this.currentLocation = state.currentUserLocation
this._currentGPSLocation.addCallback((location) => {
self._previousLocationGrant.setData("granted");
self._previousLocationGrant.setData("granted")
const feature = {
"type": "Feature",
type: "Feature",
properties: <GeoLocationPointProperties>{
id: "gps",
"user:location": "yes",
"date": new Date().toISOString(),
"latitude": location.latitude,
"longitude": location.longitude,
"speed": location.speed,
"accuracy": location.accuracy,
"heading": location.heading,
"altitude": location.altitude
date: new Date().toISOString(),
latitude: location.latitude,
longitude: location.longitude,
speed: location.speed,
accuracy: location.accuracy,
heading: location.heading,
altitude: location.altitude,
},
geometry: {
type: "Point",
coordinates: [location.longitude, location.latitude],
}
},
}
self.currentLocation?.features?.setData([{feature, freshness: new Date()}])
self.currentLocation?.features?.setData([{ feature, freshness: new Date() }])
if (willFocus.data) {
console.log("Zooming to user location: willFocus is set")
lastClick.setData(undefined);
autozoomDone = true;
self.MoveToCurrentLocation(16);
lastClick.setData(undefined)
autozoomDone = true
self.MoveToCurrentLocation(16)
} else if (self._isLocked.data) {
self.MoveToCurrentLocation();
self.MoveToCurrentLocation()
}
});
})
}
private init(askPermission: boolean, zoomToLocation: boolean) {
const self = this;
const self = this
if (self._isActive.data) {
self.MoveToCurrentLocation(16);
return;
self.MoveToCurrentLocation(16)
return
}
if (typeof navigator === "undefined") {
@ -262,27 +258,25 @@ export default class GeoLocationHandler extends VariableUiElement {
}
try {
navigator?.permissions
?.query({name: "geolocation"})
?.then(function (status) {
console.log("Geolocation permission is ", status.state);
if (status.state === "granted") {
self.StartGeolocating(zoomToLocation);
}
self._permission.setData(status.state);
status.onchange = function () {
self._permission.setData(status.state);
};
});
navigator?.permissions?.query({ name: "geolocation" })?.then(function (status) {
console.log("Geolocation permission is ", status.state)
if (status.state === "granted") {
self.StartGeolocating(zoomToLocation)
}
self._permission.setData(status.state)
status.onchange = function () {
self._permission.setData(status.state)
}
})
} catch (e) {
console.error(e);
console.error(e)
}
if (askPermission) {
self.StartGeolocating(zoomToLocation);
self.StartGeolocating(zoomToLocation)
} else if (this._previousLocationGrant.data === "granted") {
this._previousLocationGrant.setData("");
self.StartGeolocating(zoomToLocation);
this._previousLocationGrant.setData("")
self.StartGeolocating(zoomToLocation)
}
}
@ -311,7 +305,7 @@ export default class GeoLocationHandler extends VariableUiElement {
* handler._currentGPSLocation.setData(<any> {latitude : 60, longitude: 60) // out of bounds
* handler.MoveToCurrentLocation()
* resultingLocation // => [60, 60]
*
*
* // should refuse to move if out of bounds
* let resultingLocation = undefined
* let resultingzoom = 1
@ -322,7 +316,7 @@ export default class GeoLocationHandler extends VariableUiElement {
* layoutToUse: new LayoutConfig(<any>{
* id: 'test',
* title: {"en":"test"}
* "lockLocation": [ [ 2.1, 50.4], [6.4, 51.54 ]],
* "lockLocation": [ [ 2.1, 50.4], [6.4, 51.54 ]],
* description: "A testing theme",
* layers: []
* }),
@ -337,20 +331,20 @@ export default class GeoLocationHandler extends VariableUiElement {
* resultingLocation // => [51.3, 4.1]
*/
private MoveToCurrentLocation(targetZoom?: number) {
const location = this._currentGPSLocation.data;
this._lastUserRequest.setData(undefined);
const location = this._currentGPSLocation.data
this._lastUserRequest.setData(undefined)
if (
this._currentGPSLocation.data.latitude === 0 &&
this._currentGPSLocation.data.longitude === 0
) {
console.debug("Not moving to GPS-location: it is null island");
return;
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.lockLocation;
let inRange = true;
const b = this._layoutToUse.lockLocation
let inRange = true
if (b) {
if (b !== true) {
// B is an array with our locklocation
@ -358,41 +352,44 @@ export default class GeoLocationHandler extends VariableUiElement {
}
}
if (!inRange) {
console.log("Not zooming to GPS location: out of bounds", b, location);
console.log("Not zooming to GPS location: out of bounds", b, location)
} else {
const currentZoom = this._leafletMap.data.getZoom()
this._leafletMap.data.setView([location.latitude, location.longitude], Math.max(targetZoom ?? 0, currentZoom));
this._leafletMap.data.setView(
[location.latitude, location.longitude],
Math.max(targetZoom ?? 0, currentZoom)
)
}
}
private StartGeolocating(zoomToGPS = true) {
const self = this;
const self = this
this._lastUserRequest.setData(zoomToGPS ? new Date() : new Date(0))
if (self._permission.data === "denied") {
self._previousLocationGrant.setData("");
self._previousLocationGrant.setData("")
self._isActive.setData(false)
return "";
return ""
}
if (this._currentGPSLocation.data !== undefined) {
this.MoveToCurrentLocation(16);
this.MoveToCurrentLocation(16)
}
if (self._isActive.data) {
return;
return
}
self._isActive.setData(true);
self._isActive.setData(true)
navigator.geolocation.watchPosition(
function (position) {
self._currentGPSLocation.setData(position.coords);
self._currentGPSLocation.setData(position.coords)
},
function () {
console.warn("Could not get location with navigator.geolocation");
console.warn("Could not get location with navigator.geolocation")
},
{
enableHighAccuracy: true
enableHighAccuracy: true,
}
);
)
}
}

View file

@ -1,112 +1,124 @@
import {Store, UIEventSource} from "../UIEventSource";
import {Or} from "../Tags/Or";
import {Overpass} from "../Osm/Overpass";
import FeatureSource from "../FeatureSource/FeatureSource";
import {Utils} from "../../Utils";
import {TagsFilter} from "../Tags/TagsFilter";
import SimpleMetaTagger from "../SimpleMetaTagger";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import RelationsTracker from "../Osm/RelationsTracker";
import {BBox} from "../BBox";
import Loc from "../../Models/Loc";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import Constants from "../../Models/Constants";
import TileFreshnessCalculator from "../FeatureSource/TileFreshnessCalculator";
import {Tiles} from "../../Models/TileRange";
import { Store, UIEventSource } from "../UIEventSource"
import { Or } from "../Tags/Or"
import { Overpass } from "../Osm/Overpass"
import FeatureSource from "../FeatureSource/FeatureSource"
import { Utils } from "../../Utils"
import { TagsFilter } from "../Tags/TagsFilter"
import SimpleMetaTagger from "../SimpleMetaTagger"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import RelationsTracker from "../Osm/RelationsTracker"
import { BBox } from "../BBox"
import Loc from "../../Models/Loc"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import Constants from "../../Models/Constants"
import TileFreshnessCalculator from "../FeatureSource/TileFreshnessCalculator"
import { Tiles } from "../../Models/TileRange"
export default class OverpassFeatureSource implements FeatureSource {
public readonly name = "OverpassFeatureSource"
/**
* The last loaded features of the geojson
*/
public readonly features: UIEventSource<{ feature: any, freshness: Date }[]> = new UIEventSource<any[]>(undefined);
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> =
new UIEventSource<any[]>(undefined)
public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false)
public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0)
public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false);
public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0);
public readonly relationsTracker: RelationsTracker
public readonly relationsTracker: RelationsTracker;
private readonly retries: UIEventSource<number> = new UIEventSource<number>(0);
private readonly retries: UIEventSource<number> = new UIEventSource<number>(0)
private readonly state: {
readonly locationControl: Store<Loc>,
readonly layoutToUse: LayoutConfig,
readonly overpassUrl: Store<string[]>;
readonly overpassTimeout: Store<number>;
readonly locationControl: Store<Loc>
readonly layoutToUse: LayoutConfig
readonly overpassUrl: Store<string[]>
readonly overpassTimeout: Store<number>
readonly currentBounds: Store<BBox>
}
private readonly _isActive: Store<boolean>
/**
* Callback to handle all the data
*/
private readonly onBboxLoaded: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void;
private readonly onBboxLoaded: (
bbox: BBox,
date: Date,
layers: LayerConfig[],
zoomlevel: number
) => void
/**
* Keeps track of how fresh the data is
* @private
*/
private readonly freshnesses: Map<string, TileFreshnessCalculator>;
private readonly freshnesses: Map<string, TileFreshnessCalculator>
constructor(
state: {
readonly locationControl: Store<Loc>,
readonly layoutToUse: LayoutConfig,
readonly overpassUrl: Store<string[]>;
readonly overpassTimeout: Store<number>;
readonly overpassMaxZoom: Store<number>,
readonly locationControl: Store<Loc>
readonly layoutToUse: LayoutConfig
readonly overpassUrl: Store<string[]>
readonly overpassTimeout: Store<number>
readonly overpassMaxZoom: Store<number>
readonly currentBounds: Store<BBox>
},
options: {
padToTiles: Store<number>,
isActive?: Store<boolean>,
relationTracker: RelationsTracker,
onBboxLoaded?: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void,
padToTiles: Store<number>
isActive?: Store<boolean>
relationTracker: RelationsTracker
onBboxLoaded?: (
bbox: BBox,
date: Date,
layers: LayerConfig[],
zoomlevel: number
) => void
freshnesses?: Map<string, TileFreshnessCalculator>
}) {
}
) {
this.state = state
this._isActive = options.isActive;
this._isActive = options.isActive
this.onBboxLoaded = options.onBboxLoaded
this.relationsTracker = options.relationTracker
this.freshnesses = options.freshnesses
const self = this;
state.currentBounds.addCallback(_ => {
const self = this
state.currentBounds.addCallback((_) => {
self.update(options.padToTiles.data)
})
}
private GetFilter(interpreterUrl: string, layersToDownload: LayerConfig[]): Overpass {
let filters: TagsFilter[] = [];
let extraScripts: string[] = [];
let filters: TagsFilter[] = []
let extraScripts: string[] = []
for (const layer of layersToDownload) {
if (layer.source.overpassScript !== undefined) {
extraScripts.push(layer.source.overpassScript)
} else {
filters.push(layer.source.osmTags);
filters.push(layer.source.osmTags)
}
}
filters = Utils.NoNull(filters)
extraScripts = Utils.NoNull(extraScripts)
if (filters.length + extraScripts.length === 0) {
return undefined;
return undefined
}
return new Overpass(new Or(filters), extraScripts, interpreterUrl, this.state.overpassTimeout, this.relationsTracker);
return new Overpass(
new Or(filters),
extraScripts,
interpreterUrl,
this.state.overpassTimeout,
this.relationsTracker
)
}
private update(paddedZoomLevel: number) {
if (!this._isActive.data) {
return;
return
}
const self = this;
this.updateAsync(paddedZoomLevel).then(bboxDate => {
const self = this
this.updateAsync(paddedZoomLevel).then((bboxDate) => {
if (bboxDate === undefined || self.onBboxLoaded === undefined) {
return;
return
}
const [bbox, date, layers] = bboxDate
self.onBboxLoaded(bbox, date, layers, paddedZoomLevel)
@ -115,56 +127,58 @@ export default class OverpassFeatureSource implements FeatureSource {
private async updateAsync(padToZoomLevel: number): Promise<[BBox, Date, LayerConfig[]]> {
if (this.runningQuery.data) {
console.log("Still running a query, not updating");
return undefined;
console.log("Still running a query, not updating")
return undefined
}
if (this.timeout.data > 0) {
console.log("Still in timeout - not updating")
return undefined;
return undefined
}
let data: any = undefined
let date: Date = undefined
let lastUsed = 0;
let lastUsed = 0
const layersToDownload = []
const neededTiles = this.state.currentBounds.data.expandToTileBounds(padToZoomLevel).containingTileRange(padToZoomLevel)
const neededTiles = this.state.currentBounds.data
.expandToTileBounds(padToZoomLevel)
.containingTileRange(padToZoomLevel)
for (const layer of this.state.layoutToUse.layers) {
if (typeof (layer) === "string") {
if (typeof layer === "string") {
throw "A layer was not expanded!"
}
if (Constants.priviliged_layers.indexOf(layer.id) >= 0) {
continue
}
if (this.state.locationControl.data.zoom < layer.minzoom) {
continue;
continue
}
if (layer.doNotDownload) {
continue;
continue
}
if (layer.source.geojsonSource !== undefined) {
// Not our responsibility to download this layer!
continue;
continue
}
const freshness = this.freshnesses?.get(layer.id)
if (freshness !== undefined) {
const oldestDataDate = Math.min(...Tiles.MapRange(neededTiles, (x, y) => {
const date = freshness.freshnessFor(padToZoomLevel, x, y);
if (date === undefined) {
return 0
}
return date.getTime()
})) / 1000;
const oldestDataDate =
Math.min(
...Tiles.MapRange(neededTiles, (x, y) => {
const date = freshness.freshnessFor(padToZoomLevel, x, y)
if (date === undefined) {
return 0
}
return date.getTime()
})
) / 1000
const now = new Date().getTime()
const minRequiredAge = (now / 1000) - layer.maxAgeOfCache
const minRequiredAge = now / 1000 - layer.maxAgeOfCache
if (oldestDataDate >= minRequiredAge) {
// still fresh enough - not updating
continue
}
}
layersToDownload.push(layer)
@ -172,34 +186,35 @@ export default class OverpassFeatureSource implements FeatureSource {
if (layersToDownload.length == 0) {
console.debug("Not updating - no layers needed")
return;
return
}
const self = this;
const self = this
const overpassUrls = self.state.overpassUrl.data
let bounds: BBox
do {
try {
bounds = this.state.currentBounds.data?.pad(this.state.layoutToUse.widenFactor)?.expandToTileBounds(padToZoomLevel);
bounds = this.state.currentBounds.data
?.pad(this.state.layoutToUse.widenFactor)
?.expandToTileBounds(padToZoomLevel)
if (bounds === undefined) {
return undefined;
return undefined
}
const overpass = this.GetFilter(overpassUrls[lastUsed], layersToDownload);
const overpass = this.GetFilter(overpassUrls[lastUsed], layersToDownload)
if (overpass === undefined) {
return undefined;
return undefined
}
this.runningQuery.setData(true);
this.runningQuery.setData(true)
[data, date] = await overpass.queryGeoJson(bounds)
;[data, date] = await overpass.queryGeoJson(bounds)
console.log("Querying overpass is done", data)
} catch (e) {
self.retries.data++;
self.retries.ping();
console.error(`QUERY FAILED due to`, e);
self.retries.data++
self.retries.ping()
console.error(`QUERY FAILED due to`, e)
await Utils.waitFor(1000)
@ -208,34 +223,38 @@ export default class OverpassFeatureSource implements FeatureSource {
console.log("Trying next time with", overpassUrls[lastUsed])
} else {
lastUsed = 0
self.timeout.setData(self.retries.data * 5);
self.timeout.setData(self.retries.data * 5)
while (self.timeout.data > 0) {
await Utils.waitFor(1000)
console.log(self.timeout.data)
self.timeout.data--
self.timeout.ping();
self.timeout.ping()
}
}
}
} while (data === undefined && this._isActive.data);
} while (data === undefined && this._isActive.data)
try {
if (data === undefined) {
return undefined
}
data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date, undefined, this.state));
self.features.setData(data.features.map(f => ({feature: f, freshness: date})));
return [bounds, date, layersToDownload];
data.features.forEach((feature) =>
SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(
feature,
date,
undefined,
this.state
)
)
self.features.setData(data.features.map((f) => ({ feature: f, freshness: date })))
return [bounds, date, layersToDownload]
} catch (e) {
console.error("Got the overpass response, but could not process it: ", e, e.stack)
return undefined
} finally {
self.retries.setData(0);
self.runningQuery.setData(false);
self.retries.setData(0)
self.runningQuery.setData(false)
}
}
}
}

View file

@ -1,46 +1,42 @@
import {Changes} from "../Osm/Changes";
import Constants from "../../Models/Constants";
import {UIEventSource} from "../UIEventSource";
import {Utils} from "../../Utils";
import { Changes } from "../Osm/Changes"
import Constants from "../../Models/Constants"
import { UIEventSource } from "../UIEventSource"
import { Utils } from "../../Utils"
export default class PendingChangesUploader {
private lastChange: Date;
private lastChange: Date
constructor(changes: Changes, selectedFeature: UIEventSource<any>) {
const self = this;
this.lastChange = new Date();
const self = this
this.lastChange = new Date()
changes.pendingChanges.addCallback(() => {
self.lastChange = new Date();
self.lastChange = new Date()
window.setTimeout(() => {
const diff = (new Date().getTime() - self.lastChange.getTime()) / 1000;
const diff = (new Date().getTime() - self.lastChange.getTime()) / 1000
if (Constants.updateTimeoutSec >= diff - 1) {
changes.flushChanges("Flushing changes due to timeout");
changes.flushChanges("Flushing changes due to timeout")
}
}, Constants.updateTimeoutSec * 1000);
});
}, Constants.updateTimeoutSec * 1000)
})
selectedFeature
.stabilized(10000)
.addCallback(feature => {
if (feature === undefined) {
// The popup got closed - we flush
changes.flushChanges("Flushing changes due to popup closed");
}
});
selectedFeature.stabilized(10000).addCallback((feature) => {
if (feature === undefined) {
// The popup got closed - we flush
changes.flushChanges("Flushing changes due to popup closed")
}
})
if (Utils.runningFromConsole) {
return;
return
}
document.addEventListener('mouseout', e => {
document.addEventListener("mouseout", (e) => {
// @ts-ignore
if (!e.toElement && !e.relatedTarget) {
changes.flushChanges("Flushing changes due to focus lost");
changes.flushChanges("Flushing changes due to focus lost")
}
});
})
document.onfocus = () => {
changes.flushChanges("OnFocus")
@ -50,28 +46,28 @@ export default class PendingChangesUploader {
changes.flushChanges("OnFocus")
}
try {
document.addEventListener("visibilitychange", () => {
changes.flushChanges("Visibility change")
}, false);
document.addEventListener(
"visibilitychange",
() => {
changes.flushChanges("Visibility change")
},
false
)
} catch (e) {
console.warn("Could not register visibility change listener", e)
}
function onunload(e) {
if (changes.pendingChanges.data.length == 0) {
return;
return
}
changes.flushChanges("onbeforeunload - probably closing or something similar");
e.preventDefault();
changes.flushChanges("onbeforeunload - probably closing or something similar")
e.preventDefault()
return "Saving your last changes..."
}
window.onbeforeunload = onunload
// https://stackoverflow.com/questions/3239834/window-onbeforeunload-not-working-on-the-ipad#4824156
window.addEventListener("pagehide", onunload)
}
}
}

View file

@ -1,51 +1,47 @@
/**
* This actor will download the latest version of the selected element from OSM and update the tags if necessary.
*/
import {UIEventSource} from "../UIEventSource";
import {ElementStorage} from "../ElementStorage";
import {Changes} from "../Osm/Changes";
import {OsmObject} from "../Osm/OsmObject";
import {OsmConnection} from "../Osm/OsmConnection";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import SimpleMetaTagger from "../SimpleMetaTagger";
import { UIEventSource } from "../UIEventSource"
import { ElementStorage } from "../ElementStorage"
import { Changes } from "../Osm/Changes"
import { OsmObject } from "../Osm/OsmObject"
import { OsmConnection } from "../Osm/OsmConnection"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import SimpleMetaTagger from "../SimpleMetaTagger"
export default class SelectedElementTagsUpdater {
private static readonly metatags = new Set(["timestamp",
private static readonly metatags = new Set([
"timestamp",
"version",
"changeset",
"user",
"uid",
"id"])
"id",
])
constructor(state: {
selectedElement: UIEventSource<any>,
allElements: ElementStorage,
changes: Changes,
osmConnection: OsmConnection,
selectedElement: UIEventSource<any>
allElements: ElementStorage
changes: Changes
osmConnection: OsmConnection
layoutToUse: LayoutConfig
}) {
state.osmConnection.isLoggedIn.addCallbackAndRun(isLoggedIn => {
state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => {
if (isLoggedIn) {
SelectedElementTagsUpdater.installCallback(state)
return true;
return true
}
})
}
public static installCallback(state: {
selectedElement: UIEventSource<any>,
allElements: ElementStorage,
changes: Changes,
osmConnection: OsmConnection,
selectedElement: UIEventSource<any>
allElements: ElementStorage
changes: Changes
osmConnection: OsmConnection
layoutToUse: LayoutConfig
}) {
state.selectedElement.addCallbackAndRunD(s => {
state.selectedElement.addCallbackAndRunD((s) => {
let id = s.properties?.id
const backendUrl = state.osmConnection._oauth_config.url
@ -55,31 +51,31 @@ export default class SelectedElementTagsUpdater {
if (!(id.startsWith("way") || id.startsWith("node") || id.startsWith("relation"))) {
// This object is _not_ from OSM, so we skip it!
return;
return
}
if (id.indexOf("-") >= 0) {
// This is a new object
return;
return
}
OsmObject.DownloadPropertiesOf(id).then(latestTags => {
OsmObject.DownloadPropertiesOf(id).then((latestTags) => {
SelectedElementTagsUpdater.applyUpdate(state, latestTags, id)
})
});
})
}
public static applyUpdate(state: {
selectedElement: UIEventSource<any>,
allElements: ElementStorage,
changes: Changes,
osmConnection: OsmConnection,
layoutToUse: LayoutConfig
}, latestTags: any, id: string
public static applyUpdate(
state: {
selectedElement: UIEventSource<any>
allElements: ElementStorage
changes: Changes
osmConnection: OsmConnection
layoutToUse: LayoutConfig
},
latestTags: any,
id: string
) {
try {
const leftRightSensitive = state.layoutToUse.isLeftRightSensitive()
if (leftRightSensitive) {
@ -87,11 +83,11 @@ export default class SelectedElementTagsUpdater {
}
const pendingChanges = state.changes.pendingChanges.data
.filter(change => change.type + "/" + change.id === id)
.filter(change => change.tags !== undefined);
.filter((change) => change.type + "/" + change.id === id)
.filter((change) => change.tags !== undefined)
for (const pendingChange of pendingChanges) {
const tagChanges = pendingChange.tags;
const tagChanges = pendingChange.tags
for (const tagChange of tagChanges) {
const key = tagChange.k
const v = tagChange.v
@ -103,10 +99,9 @@ export default class SelectedElementTagsUpdater {
}
}
// With the changes applied, we merge them onto the upstream object
let somethingChanged = false;
const currentTagsSource = state.allElements.getEventSourceById(id);
let somethingChanged = false
const currentTagsSource = state.allElements.getEventSourceById(id)
const currentTags = currentTagsSource.data
for (const key in latestTags) {
let osmValue = latestTags[key]
@ -117,7 +112,7 @@ export default class SelectedElementTagsUpdater {
const localValue = currentTags[key]
if (localValue !== osmValue) {
somethingChanged = true;
somethingChanged = true
currentTags[key] = osmValue
}
}
@ -137,7 +132,6 @@ export default class SelectedElementTagsUpdater {
somethingChanged = true
}
if (somethingChanged) {
console.log("Detected upstream changes to the object when opening it, updating...")
currentTagsSource.ping()
@ -148,6 +142,4 @@ export default class SelectedElementTagsUpdater {
console.error("Updating the tags of selected element ", id, "failed due to", e)
}
}
}
}

View file

@ -1,63 +1,67 @@
import {UIEventSource} from "../UIEventSource";
import {OsmObject} from "../Osm/OsmObject";
import Loc from "../../Models/Loc";
import {ElementStorage} from "../ElementStorage";
import FeaturePipeline from "../FeatureSource/FeaturePipeline";
import {GeoOperations} from "../GeoOperations";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import { UIEventSource } from "../UIEventSource"
import { OsmObject } from "../Osm/OsmObject"
import Loc from "../../Models/Loc"
import { ElementStorage } from "../ElementStorage"
import FeaturePipeline from "../FeatureSource/FeaturePipeline"
import { GeoOperations } from "../GeoOperations"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
/**
* Makes sure the hash shows the selected element and vice-versa.
*/
export default class SelectedFeatureHandler {
private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "filters", "location_track", "", undefined])
private readonly hash: UIEventSource<string>;
private static readonly _no_trigger_on = new Set([
"welcome",
"copyright",
"layers",
"new",
"filters",
"location_track",
"",
undefined,
])
private readonly hash: UIEventSource<string>
private readonly state: {
selectedElement: UIEventSource<any>,
allElements: ElementStorage,
locationControl: UIEventSource<Loc>,
selectedElement: UIEventSource<any>
allElements: ElementStorage
locationControl: UIEventSource<Loc>
layoutToUse: LayoutConfig
}
constructor(
hash: UIEventSource<string>,
state: {
selectedElement: UIEventSource<any>,
allElements: ElementStorage,
featurePipeline: FeaturePipeline,
locationControl: UIEventSource<Loc>,
selectedElement: UIEventSource<any>
allElements: ElementStorage
featurePipeline: FeaturePipeline
locationControl: UIEventSource<Loc>
layoutToUse: LayoutConfig
}
) {
this.hash = hash;
this.hash = hash
this.state = state
// If the hash changes, set the selected element correctly
const self = this;
const self = this
hash.addCallback(() => self.setSelectedElementFromHash())
state.featurePipeline?.newDataLoadedSignal?.addCallbackAndRunD(_ => {
state.featurePipeline?.newDataLoadedSignal?.addCallbackAndRunD((_) => {
// New data was loaded. In initial startup, the hash might be set (via the URL) but might not be selected yet
if (hash.data === undefined || SelectedFeatureHandler._no_trigger_on.has(hash.data)) {
// This is an invalid hash anyway
return;
return
}
if (state.selectedElement.data !== undefined) {
// We already have something selected
return;
return
}
self.setSelectedElementFromHash()
})
this.initialLoad()
}
/**
* On startup: check if the hash is loaded and eventually zoom to it
* @private
@ -65,21 +69,18 @@ export default class SelectedFeatureHandler {
private initialLoad() {
const hash = this.hash.data
if (hash === undefined || hash === "" || hash.indexOf("-") >= 0) {
return;
return
}
if (SelectedFeatureHandler._no_trigger_on.has(hash)) {
return;
return
}
if (!(hash.startsWith("node") || hash.startsWith("way") || hash.startsWith("relation"))) {
return;
return
}
OsmObject.DownloadObjectAsync(hash).then(obj => {
OsmObject.DownloadObjectAsync(hash).then((obj) => {
try {
console.log("Downloaded selected object from OSM-API for initial load: ", hash)
const geojson = obj.asGeoJson()
this.state.allElements.addOrGetElement(geojson)
@ -88,9 +89,7 @@ export default class SelectedFeatureHandler {
} catch (e) {
console.error(e)
}
})
}
private setSelectedElementFromHash() {
@ -98,22 +97,21 @@ export default class SelectedFeatureHandler {
const h = this.hash.data
if (h === undefined || h === "") {
// Hash has been cleared - we clear the selected element
state.selectedElement.setData(undefined);
state.selectedElement.setData(undefined)
} else {
// we search the element to select
const feature = state.allElements.ContainingFeatures.get(h)
if (feature === undefined) {
return;
return
}
const currentlySeleced = state.selectedElement.data
if (currentlySeleced === undefined) {
state.selectedElement.setData(feature)
return;
return
}
if (currentlySeleced.properties?.id === feature.properties.id) {
// We already have the right feature
return;
return
}
state.selectedElement.setData(feature)
}
@ -121,25 +119,24 @@ export default class SelectedFeatureHandler {
// If a feature is selected via the hash, zoom there
private zoomToSelectedFeature() {
const selected = this.state.selectedElement.data
if (selected === undefined) {
return
}
const centerpoint = GeoOperations.centerpointCoordinates(selected)
const location = this.state.locationControl;
const location = this.state.locationControl
location.data.lon = centerpoint[0]
location.data.lat = centerpoint[1]
const minZoom = Math.max(14, ...(this.state.layoutToUse?.layers?.map(l => l.minzoomVisible) ?? []))
const minZoom = Math.max(
14,
...(this.state.layoutToUse?.layers?.map((l) => l.minzoomVisible) ?? [])
)
if (location.data.zoom < minZoom) {
location.data.zoom = minZoom
}
location.ping();
location.ping()
}
}
}

View file

@ -1,88 +1,87 @@
import * as L from "leaflet";
import {UIEventSource} from "../UIEventSource";
import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen";
import FilteredLayer from "../../Models/FilteredLayer";
import Constants from "../../Models/Constants";
import BaseUIElement from "../../UI/BaseUIElement";
import * as L from "leaflet"
import { UIEventSource } from "../UIEventSource"
import ScrollableFullScreen from "../../UI/Base/ScrollableFullScreen"
import FilteredLayer from "../../Models/FilteredLayer"
import Constants from "../../Models/Constants"
import BaseUIElement from "../../UI/BaseUIElement"
/**
* The stray-click-hanlders adds a marker to the map if no feature was clicked.
* Shows the given uiToShow-element in the messagebox
*/
export default class StrayClickHandler {
private _lastMarker;
private _lastMarker
constructor(
state: {
LastClickLocation: UIEventSource<{ lat: number, lon: number }>,
selectedElement: UIEventSource<string>,
filteredLayers: UIEventSource<FilteredLayer[]>,
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
selectedElement: UIEventSource<string>
filteredLayers: UIEventSource<FilteredLayer[]>
leafletMap: UIEventSource<L.Map>
},
uiToShow: ScrollableFullScreen,
iconToShow: BaseUIElement) {
const self = this;
iconToShow: BaseUIElement
) {
const self = this
const leafletMap = state.leafletMap
state.filteredLayers.data.forEach((filteredLayer) => {
filteredLayer.isDisplayed.addCallback(isEnabled => {
filteredLayer.isDisplayed.addCallback((isEnabled) => {
if (isEnabled && self._lastMarker && leafletMap.data !== undefined) {
// When a layer is activated, we remove the 'last click location' in order to force the user to reclick
// This reclick might be at a location where a feature now appeared...
state.leafletMap.data.removeLayer(self._lastMarker);
state.leafletMap.data.removeLayer(self._lastMarker)
}
})
})
state.LastClickLocation.addCallback(function (lastClick) {
if (self._lastMarker !== undefined) {
state.leafletMap.data?.removeLayer(self._lastMarker);
state.leafletMap.data?.removeLayer(self._lastMarker)
}
if (lastClick === undefined) {
return;
return
}
state.selectedElement.setData(undefined);
state.selectedElement.setData(undefined)
const clickCoor: [number, number] = [lastClick.lat, lastClick.lon]
self._lastMarker = L.marker(clickCoor, {
icon: L.divIcon({
html: iconToShow.ConstructElement(),
iconSize: [50, 50],
iconAnchor: [25, 50],
popupAnchor: [0, -45]
})
});
popupAnchor: [0, -45],
}),
})
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);
autoClose: true,
}).setContent("<div id='strayclick' style='height: 65vh'></div>")
self._lastMarker.addTo(leafletMap.data)
self._lastMarker.bindPopup(popup)
self._lastMarker.on("click", () => {
if (leafletMap.data.getZoom() < Constants.userJourney.minZoomLevelToAddNewPoints) {
self._lastMarker.closePopup()
leafletMap.data.flyTo(clickCoor, Constants.userJourney.minZoomLevelToAddNewPoints)
return;
leafletMap.data.flyTo(
clickCoor,
Constants.userJourney.minZoomLevelToAddNewPoints
)
return
}
uiToShow.AttachTo("strayclick")
uiToShow.Activate();
});
});
uiToShow.Activate()
})
})
state.selectedElement.addCallback(() => {
if (self._lastMarker !== undefined) {
leafletMap.data.removeLayer(self._lastMarker);
this._lastMarker = undefined;
leafletMap.data.removeLayer(self._lastMarker)
this._lastMarker = undefined
}
})
}
}
}

View file

@ -1,19 +1,19 @@
import {Store, UIEventSource} from "../UIEventSource";
import Locale from "../../UI/i18n/Locale";
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer";
import Combine from "../../UI/Base/Combine";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {ElementStorage} from "../ElementStorage";
import {Utils} from "../../Utils";
import { Store, UIEventSource } from "../UIEventSource"
import Locale from "../../UI/i18n/Locale"
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer"
import Combine from "../../UI/Base/Combine"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { ElementStorage } from "../ElementStorage"
import { Utils } from "../../Utils"
export default class TitleHandler {
constructor(state: {
selectedElement: Store<any>,
layoutToUse: LayoutConfig,
selectedElement: Store<any>
layoutToUse: LayoutConfig
allElements: ElementStorage
}) {
const currentTitle: Store<string> = state.selectedElement.map(
selected => {
(selected) => {
const layout = state.layoutToUse
const defaultTitle = layout?.title?.txt ?? "MapComplete"
@ -21,27 +21,32 @@ export default class TitleHandler {
return defaultTitle
}
const tags = selected.properties;
const tags = selected.properties
for (const layer of layout.layers) {
if (layer.title === undefined) {
continue;
continue
}
if (layer.source.osmTags.matchesProperties(tags)) {
const tagsSource = state.allElements.getEventSourceById(tags.id) ?? new UIEventSource<any>(tags)
const tagsSource =
state.allElements.getEventSourceById(tags.id) ??
new UIEventSource<any>(tags)
const title = new TagRenderingAnswer(tagsSource, layer.title, {})
return new Combine([defaultTitle, " | ", title]).ConstructElement()?.textContent ?? defaultTitle;
return (
new Combine([defaultTitle, " | ", title]).ConstructElement()
?.textContent ?? defaultTitle
)
}
}
return defaultTitle
}, [Locale.language]
},
[Locale.language]
)
currentTitle.addCallbackAndRunD(title => {
currentTitle.addCallbackAndRunD((title) => {
if (Utils.runningFromConsole) {
return
}
document.title = title
})
}
}
}