forked from MapComplete/MapComplete
Reformat all files with prettier
This commit is contained in:
parent
e22d189376
commit
b541d3eab4
382 changed files with 50893 additions and 35566 deletions
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue