More refactoring to fix the tests

This commit is contained in:
Pieter Vander Vennet 2021-10-15 14:52:11 +02:00
parent 71285d34cd
commit b8abbc9505
16 changed files with 507 additions and 418 deletions

View file

@ -1,298 +1,40 @@
import * as editorlayerindex from "../../assets/editor-layer-index.json"
import BaseLayer from "../../Models/BaseLayer";
import * as L from "leaflet";
import {TileLayer} from "leaflet";
import * as X from "leaflet-providers";
import {UIEventSource} from "../UIEventSource";
import {GeoOperations} from "../GeoOperations";
import {Utils} from "../../Utils";
import Loc from "../../Models/Loc";
export interface AvailableBaseLayersObj {
readonly osmCarto: BaseLayer;
layerOverview: BaseLayer[];
AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]>
SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> ;
}
/**
* Calculates which layers are available at the current location
* Changes the basemap
*/
export default class AvailableBaseLayers {
public static layerOverview: BaseLayer[];
public static osmCarto: BaseLayer;
public static osmCarto: BaseLayer =
{
id: "osm",
name: "OpenStreetMap",
layer: () => AvailableBaseLayers.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: false, // This is a lie! Of course OSM is the best map! (But not in this context)
category: "osmbasedmap"
}
public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex());
public static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
const source = location.map(
(currentLocation) => {
if (currentLocation === undefined) {
return AvailableBaseLayers.layerOverview;
}
const currentLayers = source?.data; // A bit unorthodox - I know
const newLayers = AvailableBaseLayers.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
if (currentLayers === undefined) {
return newLayers;
}
if (newLayers.length !== currentLayers.length) {
return newLayers;
}
for (let i = 0; i < newLayers.length; i++) {
if (newLayers[i].name !== currentLayers[i].name) {
return newLayers;
}
}
return currentLayers;
});
return source;
private static implementation: AvailableBaseLayersObj
static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
return AvailableBaseLayers.implementation.AvailableLayersAt(location);
}
public static SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> {
return AvailableBaseLayers.AvailableLayersAt(location).map(available => {
// First float all 'best layers' to the top
available.sort((a, b) => {
if (a.isBest && b.isBest) {
return 0;
}
if (!a.isBest) {
return 1
}
return -1;
}
)
if (preferedCategory.data === undefined) {
return available[0]
}
let prefered: string []
if (typeof preferedCategory.data === "string") {
prefered = [preferedCategory.data]
} else {
prefered = preferedCategory.data;
}
prefered.reverse();
for (const category of prefered) {
//Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
available.sort((a, b) => {
if (a.category === category && b.category === category) {
return 0;
}
if (a.category !== category) {
return 1
}
return -1;
}
)
}
return available[0]
})
}
private static CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] {
const availableLayers = [AvailableBaseLayers.osmCarto]
const globalLayers = [];
for (const layerOverviewItem of AvailableBaseLayers.layerOverview) {
const layer = layerOverviewItem;
if (layer.feature?.geometry === undefined || layer.feature?.geometry === null) {
globalLayers.push(layer);
continue;
}
if (lon === undefined || lat === undefined) {
continue;
}
if (GeoOperations.inside([lon, lat], layer.feature)) {
availableLayers.push(layer);
}
}
return availableLayers.concat(globalLayers);
}
private static LoadRasterIndex(): BaseLayer[] {
const layers: BaseLayer[] = []
// @ts-ignore
const features = editorlayerindex.features;
for (const i in features) {
const layer = features[i];
const props = layer.properties;
if (props.id === "Bing") {
// Doesnt work
continue;
}
if (props.id === "MAPNIK") {
// Already added by default
continue;
}
if (props.overlay) {
continue;
}
if (props.url.toLowerCase().indexOf("apikey") > 0) {
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;
}
if (props.name === undefined) {
console.warn("Editor layer index: name not defined on ", props)
continue
}
const leafletLayer: () => TileLayer = () => AvailableBaseLayers.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({
id: props.id,
max_zoom: props.max_zoom ?? 25,
min_zoom: props.min_zoom ?? 1,
name: props.name,
layer: leafletLayer,
feature: layer,
isBest: props.best ?? false,
category: props.category
});
}
return layers;
}
private static LoadProviderIndex(): BaseLayer[] {
// @ts-ignore
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);
return {
feature: null,
id: id,
name: name,
layer: layer,
min_zoom: layer.minzoom,
max_zoom: layer.maxzoom,
category: "osmbasedmap",
isBest: false
}
} catch (e) {
console.error("Could not find provided layer", name, e);
return null;
}
}
const layers = [
l("CyclOSM", "CyclOSM - A bicycle oriented map"),
l("Stamen.TonerLite", "Toner Lite (by Stamen)"),
l("Stamen.TonerBackground", "Toner Background - no labels (by Stamen)"),
l("Stamen.Watercolor", "Watercolor (by Stamen)"),
l("Stadia.OSMBright", "Osm Bright (by Stadia)"),
l("CartoDB.Positron", "Positron (by CartoDB)"),
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);
static SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> {
return AvailableBaseLayers.implementation.SelectBestLayerAccordingTo(location, preferedCategory);
}
/**
* 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}", "");
const subdomainsMatch = url.match(/{switch:[^}]*}/)
let domains: string[] = [];
if (subdomainsMatch !== null) {
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);
const isUpper = urlObj.searchParams["LAYERS"] !== null;
const options = {
maxZoom: maxZoom ?? 19,
attribution: attribution + " | ",
subdomains: domains,
uppercase: isUpper,
transparent: false
};
for (const paramater of paramaters) {
let p = paramater;
if (isUpper) {
p = paramater.toUpperCase();
}
options[paramater] = urlObj.searchParams.get(p);
}
if (options.transparent === null) {
options.transparent = false;
}
return L.tileLayer.wms(urlObj.protocol + "//" + urlObj.host + urlObj.pathname, options);
}
if (attributionUrl) {
attribution = `<a href='${attributionUrl}' target='_blank'>${attribution}</a>`;
}
return L.tileLayer(url,
{
attribution: attribution,
maxZoom: maxZoom,
minZoom: 1,
// @ts-ignore
wmts: isWMTS ?? false,
subdomains: domains
});
public static implement(backend: AvailableBaseLayersObj){
AvailableBaseLayers.layerOverview = backend.layerOverview
AvailableBaseLayers.osmCarto = backend.osmCarto
AvailableBaseLayers.implementation = backend
}
}

View file

@ -0,0 +1,292 @@
import BaseLayer from "../../Models/BaseLayer";
import {UIEventSource} from "../UIEventSource";
import Loc from "../../Models/Loc";
import {GeoOperations} from "../GeoOperations";
import * as editorlayerindex from "../../assets/editor-layer-index.json";
import {TileLayer} from "leaflet";
import * as X from "leaflet-providers";
import * as L from "leaflet";
import {Utils} from "../../Utils";
import {AvailableBaseLayersObj} from "./AvailableBaseLayers";
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",
19,
false, false),
feature: null,
max_zoom: 19,
min_zoom: 0,
isBest: false, // This is a lie! Of course OSM is the best map! (But not in this context)
category: "osmbasedmap"
}
public layerOverview = AvailableBaseLayersImplementation.LoadRasterIndex().concat(AvailableBaseLayersImplementation.LoadProviderIndex());
public AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
const source = location.map(
(currentLocation) => {
if (currentLocation === undefined) {
return this.layerOverview;
}
const currentLayers = source?.data; // A bit unorthodox - I know
const newLayers = this.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
if (currentLayers === undefined) {
return newLayers;
}
if (newLayers.length !== currentLayers.length) {
return newLayers;
}
for (let i = 0; i < newLayers.length; i++) {
if (newLayers[i].name !== currentLayers[i].name) {
return newLayers;
}
}
return currentLayers;
});
return source;
}
public SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<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 (preferedCategory.data === undefined) {
return available[0]
}
let prefered: string []
if (typeof preferedCategory.data === "string") {
prefered = [preferedCategory.data]
} else {
prefered = preferedCategory.data;
}
prefered.reverse();
for (const category of prefered) {
//Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
available.sort((a, b) => {
if (a.category === category && b.category === category) {
return 0;
}
if (a.category !== category) {
return 1
}
return -1;
}
)
}
return available[0]
})
}
private CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] {
const availableLayers = [this.osmCarto]
const globalLayers = [];
for (const layerOverviewItem of this.layerOverview) {
const layer = layerOverviewItem;
if (layer.feature?.geometry === undefined || layer.feature?.geometry === null) {
globalLayers.push(layer);
continue;
}
if (lon === undefined || lat === undefined) {
continue;
}
if (GeoOperations.inside([lon, lat], layer.feature)) {
availableLayers.push(layer);
}
}
return availableLayers.concat(globalLayers);
}
private static LoadRasterIndex(): BaseLayer[] {
const layers: BaseLayer[] = []
// @ts-ignore
const features = editorlayerindex.features;
for (const i in features) {
const layer = features[i];
const props = layer.properties;
if (props.id === "Bing") {
// Doesnt work
continue;
}
if (props.id === "MAPNIK") {
// Already added by default
continue;
}
if (props.overlay) {
continue;
}
if (props.url.toLowerCase().indexOf("apikey") > 0) {
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;
}
if (props.name === undefined) {
console.warn("Editor layer index: name not defined on ", props)
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"
)
// Note: if layer.geometry is null, there is global coverage for this layer
layers.push({
id: props.id,
max_zoom: props.max_zoom ?? 25,
min_zoom: props.min_zoom ?? 1,
name: props.name,
layer: leafletLayer,
feature: layer,
isBest: props.best ?? false,
category: props.category
});
}
return layers;
}
private static LoadProviderIndex(): BaseLayer[] {
// @ts-ignore
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);
return {
feature: null,
id: id,
name: name,
layer: layer,
min_zoom: layer.minzoom,
max_zoom: layer.maxzoom,
category: "osmbasedmap",
isBest: false
}
} catch (e) {
console.error("Could not find provided layer", name, e);
return null;
}
}
const layers = [
l("CyclOSM", "CyclOSM - A bicycle oriented map"),
l("Stamen.TonerLite", "Toner Lite (by Stamen)"),
l("Stamen.TonerBackground", "Toner Background - no labels (by Stamen)"),
l("Stamen.Watercolor", "Watercolor (by Stamen)"),
l("Stadia.OSMBright", "Osm Bright (by Stadia)"),
l("CartoDB.Positron", "Positron (by CartoDB)"),
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);
}
/**
* 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}", "");
const subdomainsMatch = url.match(/{switch:[^}]*}/)
let domains: string[] = [];
if (subdomainsMatch !== null) {
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);
const isUpper = urlObj.searchParams["LAYERS"] !== null;
const options = {
maxZoom: maxZoom ?? 19,
attribution: attribution + " | ",
subdomains: domains,
uppercase: isUpper,
transparent: false
};
for (const paramater of paramaters) {
let p = paramater;
if (isUpper) {
p = paramater.toUpperCase();
}
options[paramater] = urlObj.searchParams.get(p);
}
if (options.transparent === null) {
options.transparent = false;
}
return L.tileLayer.wms(urlObj.protocol + "//" + urlObj.host + urlObj.pathname, options);
}
if (attributionUrl) {
attribution = `<a href='${attributionUrl}' target='_blank'>${attribution}</a>`;
}
return L.tileLayer(url,
{
attribution: attribution,
maxZoom: maxZoom,
minZoom: 1,
// @ts-ignore
wmts: isWMTS ?? false,
subdomains: domains
});
}
}

View file

@ -179,7 +179,7 @@ export default class OverpassFeatureSource implements FeatureSource {
self.retries.setData(0);
try {
data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date));
data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date, undefined));
self.features.setData(data.features.map(f => ({feature: f, freshness: date})));
return [bounds, date, layersToDownload];
} catch (e) {

View file

@ -80,5 +80,6 @@ export default class StrayClickHandler {
}
}

View file

@ -121,50 +121,7 @@ export default class FeaturePipelineState extends MapState {
})
}
public setupClickDialogOnMap(filterViewIsOpened: UIEventSource<boolean>, leafletMap: UIEventSource<any>) {
const self = this
function setup(){
let presetCount = 0;
for (const layer of self.layoutToUse.layers) {
for (const preset of layer.presets) {
presetCount++;
}
}
if (presetCount == 0) {
return;
}
const newPointDialogIsShown = new UIEventSource<boolean>(false);
const addNewPoint = new ScrollableFullScreen(
() => Translations.t.general.add.title.Clone(),
() => new SimpleAddUI(newPointDialogIsShown, filterViewIsOpened, self),
"new",
newPointDialogIsShown
);
addNewPoint.isShown.addCallback((isShown) => {
if (!isShown) {
self.LastClickLocation.setData(undefined);
}
});
new StrayClickHandler(
self.LastClickLocation,
self.selectedElement,
self.filteredLayers,
leafletMap,
addNewPoint
);
}
this.featureSwitchAddNew.addCallbackAndRunD(addNewAllowed => {
if (addNewAllowed) {
setup()
return true;
}
})
}

View file

@ -7,9 +7,6 @@ import BackgroundLayerResetter from "../Actors/BackgroundLayerResetter";
import Attribution from "../../UI/BigComponents/Attribution";
import Minimap, {MinimapObj} from "../../UI/Base/Minimap";
import {Tiles} from "../../Models/TileRange";
import * as L from "leaflet";
import Img from "../../UI/Base/Img";
import Svg from "../../Svg";
import BaseUIElement from "../../UI/BaseUIElement";
import FilteredLayer from "../../Models/FilteredLayer";
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
@ -26,7 +23,7 @@ export default class MapState extends UserRelatedState {
/**
The leaflet instance of the big basemap
*/
public leafletMap = new UIEventSource<L.Map>(undefined, "leafletmap");
public leafletMap = new UIEventSource<any /*L.Map*/>(undefined, "leafletmap");
/**
* A list of currently available background layers
*/
@ -67,7 +64,6 @@ export default class MapState extends UserRelatedState {
*/
public overlayToggles: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]
constructor(layoutToUse: LayoutConfig) {
super(layoutToUse);
@ -120,57 +116,19 @@ export default class MapState extends UserRelatedState {
lastClickLocation: this.LastClickLocation
})
this.overlayToggles = this.layoutToUse.tileLayerSources.filter(c => c.name !== undefined).map(c => ({
config: c,
isDisplayed: QueryParameters.GetQueryParameter("overlay-" + c.id, "" + c.defaultState, "Wether or not the overlay " + c.id + " is shown").map(str => str === "true", [], b => "" + b)
}))
this.filteredLayers = this.InitializeFilteredLayers()
this.lockBounds()
this.AddAllOverlaysToMap(this.leafletMap)
this.addHomeMarker()
}
private addHomeMarker() {
const leafletMap = this.leafletMap
const osmConnection = this.osmConnection
function addHomeMarker() {
const userDetails = osmConnection.userDetails.data;
if (userDetails === undefined) {
return false;
}
const home = userDetails.home;
if (home === undefined) {
return userDetails.loggedIn; // If logged in, the home is not set and we unregister. If not logged in, we stay registered if a login still comes
}
const leaflet = leafletMap.data;
if (leaflet === undefined) {
return false;
}
const color = getComputedStyle(document.body).getPropertyValue(
"--subtle-detail-color"
);
const icon = L.icon({
iconUrl: Img.AsData(
Svg.home_white_bg.replace(/#ffffff/g, color)
),
iconSize: [30, 30],
iconAnchor: [15, 15],
});
const marker = L.marker([home.lat, home.lon], {icon: icon});
marker.addTo(leaflet);
return true;
}
osmConnection.userDetails.addCallbackAndRunD(_ => addHomeMarker());
leafletMap.addCallbackAndRunD(_ => addHomeMarker())
}
private lockBounds() {
const layout = this.layoutToUse;
@ -198,6 +156,7 @@ export default class MapState extends UserRelatedState {
})
}
}
private InitializeFilteredLayers() {
// Initialize the filtered layers state
@ -252,8 +211,8 @@ export default class MapState extends UserRelatedState {
return new UIEventSource<FilteredLayer[]>(flayers);
}
public AddAllOverlaysToMap(leafletMap: UIEventSource<any>){
const initialized =new Set()
public AddAllOverlaysToMap(leafletMap: UIEventSource<any>) {
const initialized = new Set()
for (const overlayToggle of this.overlayToggles) {
new ShowOverlayLayer(overlayToggle.config, leafletMap, overlayToggle.isDisplayed)
initialized.add(overlayToggle.config)

View file

@ -9,6 +9,8 @@ import {Utils} from "../../Utils";
import Locale from "../../UI/i18n/Locale";
import ElementsState from "./ElementsState";
import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater";
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource";
import FeatureSource from "../FeatureSource/FeatureSource";
/**
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
@ -34,6 +36,10 @@ export default class UserRelatedState extends ElementsState {
* WHich other themes the user previously visited
*/
public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>;
/**
* A feature source containing the current home location of the user
*/
public homeLocation: FeatureSource
constructor(layoutToUse: LayoutConfig) {
super(layoutToUse);
@ -104,4 +110,37 @@ export default class UserRelatedState extends ElementsState {
.ping();
}
private initHomeLocation() {
const empty = []
const feature = UIEventSource.ListStabilized(this.osmConnection.userDetails.map(userDetails => {
if (userDetails === undefined) {
return undefined;
}
const home = userDetails.home;
if (home === undefined) {
return undefined;
}
return [home.lon, home.lat]
})).map(homeLonLat => {
if(homeLonLat === undefined){
return empty
}
return [{
"type": "Feature",
"properties": {
"user:home": "yes",
"_lon": homeLonLat[0],
"_lat": homeLonLat[1]
},
"geometry": {
"type": "Point",
"coordinates": homeLonLat
}
}]
})
this.homeLocation = new StaticFeatureSource(feature, false)
}
}

View file

@ -136,7 +136,7 @@ export class UIEventSource<T> {
if (oldList === list) {
return;
}
if (oldList.length !== list.length) {
if (oldList === undefined || oldList.length !== list.length) {
stable.setData(list);
return;
}