Finetuning of the filter functionality
This commit is contained in:
parent
31d2bd83b9
commit
79569f5119
17 changed files with 219 additions and 309 deletions
|
@ -34,7 +34,8 @@ export default class LayerConfig {
|
||||||
passAllFeatures: boolean;
|
passAllFeatures: boolean;
|
||||||
isShown: TagRenderingConfig;
|
isShown: TagRenderingConfig;
|
||||||
minzoom: number;
|
minzoom: number;
|
||||||
maxzoom: number;
|
minzoomVisible: number;
|
||||||
|
maxzoom:number;
|
||||||
title?: TagRenderingConfig;
|
title?: TagRenderingConfig;
|
||||||
titleIcons: TagRenderingConfig[];
|
titleIcons: TagRenderingConfig[];
|
||||||
icon: TagRenderingConfig;
|
icon: TagRenderingConfig;
|
||||||
|
@ -143,7 +144,7 @@ export default class LayerConfig {
|
||||||
this.doNotDownload = json.doNotDownload ?? false;
|
this.doNotDownload = json.doNotDownload ?? false;
|
||||||
this.passAllFeatures = json.passAllFeatures ?? false;
|
this.passAllFeatures = json.passAllFeatures ?? false;
|
||||||
this.minzoom = json.minzoom ?? 0;
|
this.minzoom = json.minzoom ?? 0;
|
||||||
this.maxzoom = json.maxzoom ?? 1000;
|
this.minzoomVisible = json.minzoomVisible ?? this.minzoom;
|
||||||
this.wayHandling = json.wayHandling ?? 0;
|
this.wayHandling = json.wayHandling ?? 0;
|
||||||
this.presets = (json.presets ?? []).map((pr, i) => {
|
this.presets = (json.presets ?? []).map((pr, i) => {
|
||||||
if (pr.preciseInput === true) {
|
if (pr.preciseInput === true) {
|
||||||
|
|
|
@ -92,16 +92,16 @@ export interface LayerConfigJson {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The zoomlevel at which point the data is shown and loaded.
|
* The minimum needed zoomlevel required before loading of the data start
|
||||||
* Default: 0
|
* Default: 0
|
||||||
*/
|
*/
|
||||||
minzoom?: number;
|
minzoom?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The zoomlevel at which point the data is hidden again
|
* If zoomed out below this zoomlevel, the data will be hidden.
|
||||||
* Default: 100 (thus: always visible
|
* Default: minzoom
|
||||||
*/
|
*/
|
||||||
maxzoom?: number;
|
minzoomVisible?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The title shown in a popup for elements of this layer.
|
* The title shown in a popup for elements of this layer.
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import { CenterFlexedElement } from "./UI/Base/CenterFlexedElement";
|
import {CenterFlexedElement} from "./UI/Base/CenterFlexedElement";
|
||||||
import { FixedUiElement } from "./UI/Base/FixedUiElement";
|
import {FixedUiElement} from "./UI/Base/FixedUiElement";
|
||||||
import Toggle from "./UI/Input/Toggle";
|
import Toggle from "./UI/Input/Toggle";
|
||||||
import { Basemap } from "./UI/BigComponents/Basemap";
|
import {Basemap} from "./UI/BigComponents/Basemap";
|
||||||
import State from "./State";
|
import State from "./State";
|
||||||
import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource";
|
import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource";
|
||||||
import { UIEventSource } from "./Logic/UIEventSource";
|
import {UIEventSource} from "./Logic/UIEventSource";
|
||||||
import { QueryParameters } from "./Logic/Web/QueryParameters";
|
import {QueryParameters} from "./Logic/Web/QueryParameters";
|
||||||
import StrayClickHandler from "./Logic/Actors/StrayClickHandler";
|
import StrayClickHandler from "./Logic/Actors/StrayClickHandler";
|
||||||
import SimpleAddUI from "./UI/BigComponents/SimpleAddUI";
|
import SimpleAddUI from "./UI/BigComponents/SimpleAddUI";
|
||||||
import CenterMessageBox from "./UI/CenterMessageBox";
|
import CenterMessageBox from "./UI/CenterMessageBox";
|
||||||
import UserBadge from "./UI/BigComponents/UserBadge";
|
import UserBadge from "./UI/BigComponents/UserBadge";
|
||||||
import SearchAndGo from "./UI/BigComponents/SearchAndGo";
|
import SearchAndGo from "./UI/BigComponents/SearchAndGo";
|
||||||
import GeoLocationHandler from "./Logic/Actors/GeoLocationHandler";
|
import GeoLocationHandler from "./Logic/Actors/GeoLocationHandler";
|
||||||
import { LocalStorageSource } from "./Logic/Web/LocalStorageSource";
|
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
|
||||||
import { Utils } from "./Utils";
|
import {Utils} from "./Utils";
|
||||||
import Svg from "./Svg";
|
import Svg from "./Svg";
|
||||||
import Link from "./UI/Base/Link";
|
import Link from "./UI/Base/Link";
|
||||||
import * as personal from "./assets/themes/personalLayout/personalLayout.json";
|
import * as personal from "./assets/themes/personalLayout/personalLayout.json";
|
||||||
|
@ -34,17 +34,16 @@ import MapControlButton from "./UI/MapControlButton";
|
||||||
import Combine from "./UI/Base/Combine";
|
import Combine from "./UI/Base/Combine";
|
||||||
import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler";
|
import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler";
|
||||||
import LZString from "lz-string";
|
import LZString from "lz-string";
|
||||||
import { LayoutConfigJson } from "./Customizations/JSON/LayoutConfigJson";
|
import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson";
|
||||||
import AttributionPanel from "./UI/BigComponents/AttributionPanel";
|
import AttributionPanel from "./UI/BigComponents/AttributionPanel";
|
||||||
import ContributorCount from "./Logic/ContributorCount";
|
import ContributorCount from "./Logic/ContributorCount";
|
||||||
import FeatureSource from "./Logic/FeatureSource/FeatureSource";
|
import FeatureSource from "./Logic/FeatureSource/FeatureSource";
|
||||||
import AllKnownLayers from "./Customizations/AllKnownLayers";
|
import AllKnownLayers from "./Customizations/AllKnownLayers";
|
||||||
import LayerConfig from "./Customizations/JSON/LayerConfig";
|
import LayerConfig from "./Customizations/JSON/LayerConfig";
|
||||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
||||||
import { SimpleMapScreenshoter } from "leaflet-simple-map-screenshoter";
|
import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter";
|
||||||
import jsPDF from "jspdf";
|
import jsPDF from "jspdf";
|
||||||
import FilterView from "./UI/BigComponents/FilterView";
|
import {TagsFilter} from "./Logic/Tags/TagsFilter";
|
||||||
import { TagsFilter } from "./Logic/Tags/TagsFilter";
|
|
||||||
|
|
||||||
export class InitUiElements {
|
export class InitUiElements {
|
||||||
static InitAll(
|
static InitAll(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import FeatureSource from "./FeatureSource";
|
import FeatureSource from "./FeatureSource";
|
||||||
import {UIEventSource} from "../UIEventSource";
|
import {UIEventSource} from "../UIEventSource";
|
||||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
import FilteredLayer from "../../Models/FilteredLayer";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,7 +13,7 @@ export default class FeatureDuplicatorPerLayer implements FeatureSource {
|
||||||
|
|
||||||
public readonly name;
|
public readonly name;
|
||||||
|
|
||||||
constructor(layers: UIEventSource<{ layerDef: LayerConfig }[]>, upstream: FeatureSource) {
|
constructor(layers: UIEventSource<FilteredLayer[]>, upstream: FeatureSource) {
|
||||||
this.name = "FeatureDuplicator of "+upstream.name;
|
this.name = "FeatureDuplicator of "+upstream.name;
|
||||||
this.features = upstream.features.map(features => {
|
this.features = upstream.features.map(features => {
|
||||||
const newFeatures: { feature: any, freshness: Date }[] = [];
|
const newFeatures: { feature: any, freshness: Date }[] = [];
|
||||||
|
|
|
@ -13,6 +13,7 @@ import Loc from "../../Models/Loc";
|
||||||
import GeoJsonSource from "./GeoJsonSource";
|
import GeoJsonSource from "./GeoJsonSource";
|
||||||
import MetaTaggingFeatureSource from "./MetaTaggingFeatureSource";
|
import MetaTaggingFeatureSource from "./MetaTaggingFeatureSource";
|
||||||
import RegisteringFeatureSource from "./RegisteringFeatureSource";
|
import RegisteringFeatureSource from "./RegisteringFeatureSource";
|
||||||
|
import FilteredLayer from "../../Models/FilteredLayer";
|
||||||
|
|
||||||
export default class FeaturePipeline implements FeatureSource {
|
export default class FeaturePipeline implements FeatureSource {
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ export default class FeaturePipeline implements FeatureSource {
|
||||||
|
|
||||||
public readonly name = "FeaturePipeline"
|
public readonly name = "FeaturePipeline"
|
||||||
|
|
||||||
constructor(flayers: UIEventSource<{ isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[]>,
|
constructor(flayers: UIEventSource<FilteredLayer[]>,
|
||||||
updater: FeatureSource,
|
updater: FeatureSource,
|
||||||
fromOsmApi: FeatureSource,
|
fromOsmApi: FeatureSource,
|
||||||
layout: UIEventSource<LayoutConfig>,
|
layout: UIEventSource<LayoutConfig>,
|
||||||
|
|
|
@ -1,173 +1,169 @@
|
||||||
import FeatureSource from "./FeatureSource";
|
import FeatureSource from "./FeatureSource";
|
||||||
import { UIEventSource } from "../UIEventSource";
|
import {UIEventSource} from "../UIEventSource";
|
||||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||||
import Loc from "../../Models/Loc";
|
import Loc from "../../Models/Loc";
|
||||||
import Hash from "../Web/Hash";
|
import Hash from "../Web/Hash";
|
||||||
import { TagsFilter } from "../Tags/TagsFilter";
|
import {TagsFilter} from "../Tags/TagsFilter";
|
||||||
|
|
||||||
export default class FilteringFeatureSource implements FeatureSource {
|
export default class FilteringFeatureSource implements FeatureSource {
|
||||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> =
|
public features: UIEventSource<{ feature: any; freshness: Date }[]> =
|
||||||
new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||||
public readonly name = "FilteringFeatureSource";
|
public readonly name = "FilteringFeatureSource";
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
layers: UIEventSource<
|
layers: UIEventSource<{
|
||||||
{
|
|
||||||
isDisplayed: UIEventSource<boolean>;
|
|
||||||
layerDef: LayerConfig;
|
|
||||||
appliedFilters: UIEventSource<TagsFilter>;
|
|
||||||
}[]
|
|
||||||
>,
|
|
||||||
location: UIEventSource<Loc>,
|
|
||||||
selectedElement: UIEventSource<any>,
|
|
||||||
upstream: FeatureSource
|
|
||||||
) {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
const layerDict = {};
|
|
||||||
if (layers.data.length == 0) {
|
|
||||||
console.warn("No layers defined!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const layer of layers.data) {
|
|
||||||
layerDict[layer.layerDef.id] = layer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const features: { feature: any; freshness: Date }[] =
|
|
||||||
upstream.features.data;
|
|
||||||
|
|
||||||
const missingLayers = new Set<string>();
|
|
||||||
|
|
||||||
const newFeatures = features.filter((f) => {
|
|
||||||
const layerId = f.feature._matching_layer_id;
|
|
||||||
|
|
||||||
if (
|
|
||||||
selectedElement.data?.id === f.feature.id ||
|
|
||||||
f.feature.id === Hash.hash.data
|
|
||||||
) {
|
|
||||||
// This is the selected object - it gets a free pass even if zoom is not sufficient
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layerId !== undefined) {
|
|
||||||
const layer: {
|
|
||||||
isDisplayed: UIEventSource<boolean>;
|
isDisplayed: UIEventSource<boolean>;
|
||||||
layerDef: LayerConfig;
|
layerDef: LayerConfig;
|
||||||
appliedFilters: UIEventSource<TagsFilter>;
|
appliedFilters: UIEventSource<TagsFilter>;
|
||||||
} = layerDict[layerId];
|
}[]>,
|
||||||
if (layer === undefined) {
|
location: UIEventSource<Loc>,
|
||||||
missingLayers.add(layerId);
|
selectedElement: UIEventSource<any>,
|
||||||
return true;
|
upstream: FeatureSource
|
||||||
}
|
) {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
const isShown = layer.layerDef.isShown;
|
function update() {
|
||||||
const tags = f.feature.properties;
|
const layerDict = {};
|
||||||
if (isShown.IsKnown(tags)) {
|
if (layers.data.length == 0) {
|
||||||
const result = layer.layerDef.isShown.GetRenderValue(
|
console.warn("No layers defined!");
|
||||||
f.feature.properties
|
return;
|
||||||
).txt;
|
|
||||||
if (result !== "yes") {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
for (const layer of layers.data) {
|
||||||
|
const prev = layerDict[layer.layerDef.id]
|
||||||
if (FilteringFeatureSource.showLayer(layer, location)) {
|
if (prev !== undefined) {
|
||||||
const tagsFilter = layer.appliedFilters.data;
|
// We have seen this layer before!
|
||||||
if (tagsFilter) {
|
// We prefer the one which has a name
|
||||||
const properties = f.feature.properties;
|
if (layer.layerDef.name === undefined) {
|
||||||
if (!tagsFilter.matchesProperties(properties)) {
|
// This one is hidden, so we skip it
|
||||||
return false;
|
console.log("Ignoring layer selection from ", layer)
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layerDict[layer.layerDef.id] = layer;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
const features: { feature: any; freshness: Date }[] =
|
||||||
}
|
upstream.features.data;
|
||||||
|
|
||||||
|
const missingLayers = new Set<string>();
|
||||||
|
|
||||||
|
const newFeatures = features.filter((f) => {
|
||||||
|
const layerId = f.feature._matching_layer_id;
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedElement.data?.id === f.feature.id ||
|
||||||
|
f.feature.id === Hash.hash.data) {
|
||||||
|
// This is the selected object - it gets a free pass even if zoom is not sufficient or it is filtered away
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layerId === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const layer: {
|
||||||
|
isDisplayed: UIEventSource<boolean>;
|
||||||
|
layerDef: LayerConfig;
|
||||||
|
appliedFilters: UIEventSource<TagsFilter>;
|
||||||
|
} = layerDict[layerId];
|
||||||
|
if (layer === undefined) {
|
||||||
|
missingLayers.add(layerId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isShown = layer.layerDef.isShown;
|
||||||
|
const tags = f.feature.properties;
|
||||||
|
if (isShown.IsKnown(tags)) {
|
||||||
|
const result = layer.layerDef.isShown.GetRenderValue(
|
||||||
|
f.feature.properties
|
||||||
|
).txt;
|
||||||
|
if (result !== "yes") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagsFilter = layer.appliedFilters.data;
|
||||||
|
if (tagsFilter) {
|
||||||
|
if (!tagsFilter.matchesProperties(f.feature.properties)) {
|
||||||
|
// Hidden by the filter on the layer itself - we want to hide it no matter wat
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!FilteringFeatureSource.showLayer(layer, location)) {
|
||||||
|
// The layer itself is either disabled or hidden due to zoom constraints
|
||||||
|
// We should return true, but it might still match some other layer
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
"Filtering layer source: input: ",
|
||||||
|
upstream.features.data?.length,
|
||||||
|
"output:",
|
||||||
|
newFeatures.length
|
||||||
|
);
|
||||||
|
self.features.setData(newFeatures);
|
||||||
|
if (missingLayers.size > 0) {
|
||||||
|
console.error(
|
||||||
|
"Some layers were not found: ",
|
||||||
|
Array.from(missingLayers)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Does it match any other layer - e.g. because of a switch?
|
|
||||||
for (const toCheck of layers.data) {
|
upstream.features.addCallback(() => {
|
||||||
if (!FilteringFeatureSource.showLayer(toCheck, location)) {
|
update();
|
||||||
continue;
|
});
|
||||||
}
|
location
|
||||||
if (
|
.map((l) => {
|
||||||
toCheck.layerDef.source.osmTags.matchesProperties(
|
// We want something that is stable for the shown layers
|
||||||
f.feature.properties
|
const displayedLayerIndexes = [];
|
||||||
)
|
for (let i = 0; i < layers.data.length; i++) {
|
||||||
) {
|
const layer = layers.data[i];
|
||||||
return true;
|
if (l.zoom < layer.layerDef.minzoom) {
|
||||||
}
|
continue;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
});
|
if (!layer.isDisplayed.data) {
|
||||||
console.log(
|
continue;
|
||||||
"Filtering layer source: input: ",
|
}
|
||||||
upstream.features.data?.length,
|
displayedLayerIndexes.push(i);
|
||||||
"output:",
|
}
|
||||||
newFeatures.length
|
return displayedLayerIndexes.join(",");
|
||||||
);
|
})
|
||||||
self.features.setData(newFeatures);
|
.addCallback(() => {
|
||||||
if (missingLayers.size > 0) {
|
update();
|
||||||
console.error(
|
});
|
||||||
"Some layers were not found: ",
|
|
||||||
Array.from(missingLayers)
|
layers.addCallback(update);
|
||||||
);
|
|
||||||
}
|
const registered = new Set<UIEventSource<boolean>>();
|
||||||
|
layers.addCallbackAndRun((layers) => {
|
||||||
|
for (const layer of layers) {
|
||||||
|
if (registered.has(layer.isDisplayed)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
registered.add(layer.isDisplayed);
|
||||||
|
layer.isDisplayed.addCallback(() => update());
|
||||||
|
layer.appliedFilters.addCallback(() => update());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
upstream.features.addCallback(() => {
|
private static showLayer(
|
||||||
update();
|
layer: {
|
||||||
});
|
isDisplayed: UIEventSource<boolean>;
|
||||||
location
|
layerDef: LayerConfig;
|
||||||
.map((l) => {
|
},
|
||||||
// We want something that is stable for the shown layers
|
location: UIEventSource<Loc>
|
||||||
const displayedLayerIndexes = [];
|
) {
|
||||||
for (let i = 0; i < layers.data.length; i++) {
|
return (
|
||||||
const layer = layers.data[i];
|
layer.isDisplayed.data &&
|
||||||
if (l.zoom < layer.layerDef.minzoom) {
|
layer.layerDef.minzoomVisible <= location.data.zoom
|
||||||
continue;
|
);
|
||||||
}
|
}
|
||||||
if (l.zoom > layer.layerDef.maxzoom) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!layer.isDisplayed.data) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
displayedLayerIndexes.push(i);
|
|
||||||
}
|
|
||||||
return displayedLayerIndexes.join(",");
|
|
||||||
})
|
|
||||||
.addCallback(() => {
|
|
||||||
update();
|
|
||||||
});
|
|
||||||
|
|
||||||
layers.addCallback(update);
|
|
||||||
|
|
||||||
const registered = new Set<UIEventSource<boolean>>();
|
|
||||||
layers.addCallbackAndRun((layers) => {
|
|
||||||
for (const layer of layers) {
|
|
||||||
if (registered.has(layer.isDisplayed)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
registered.add(layer.isDisplayed);
|
|
||||||
layer.isDisplayed.addCallback(() => update());
|
|
||||||
layer.appliedFilters.addCallback(() => update());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static showLayer(
|
|
||||||
layer: {
|
|
||||||
isDisplayed: UIEventSource<boolean>;
|
|
||||||
layerDef: LayerConfig;
|
|
||||||
},
|
|
||||||
location: UIEventSource<Loc>
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
layer.isDisplayed.data &&
|
|
||||||
layer.layerDef.minzoom <= location.data.zoom &&
|
|
||||||
layer.layerDef.maxzoom >= location.data.zoom
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ export default class GeoJsonSource implements FeatureSource {
|
||||||
* @param locationControl
|
* @param locationControl
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
public static ConstructMultiSource(flayers: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[], locationControl: UIEventSource<Loc>): FeatureSource[] {
|
public static ConstructMultiSource(flayers: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[], locationControl: UIEventSource<Loc>): GeoJsonSource[] {
|
||||||
|
|
||||||
const flayersPerSource = new Map<string, { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[]>();
|
const flayersPerSource = new Map<string, { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[]>();
|
||||||
for (const flayer of flayers) {
|
for (const flayer of flayers) {
|
||||||
|
@ -65,7 +65,7 @@ export default class GeoJsonSource implements FeatureSource {
|
||||||
flayersPerSource.get(url).push(flayer)
|
flayersPerSource.get(url).push(flayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sources: FeatureSource[] = []
|
const sources: GeoJsonSource[] = []
|
||||||
|
|
||||||
flayersPerSource.forEach((flayers, key) => {
|
flayersPerSource.forEach((flayers, key) => {
|
||||||
if (flayers.length == 1) {
|
if (flayers.length == 1) {
|
||||||
|
@ -118,8 +118,7 @@ export default class GeoJsonSource implements FeatureSource {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (location.zoom < flayer.layerDef.minzoom ||
|
if (location.zoom < flayer.layerDef.minzoom) {
|
||||||
location.zoom > flayer.layerDef.maxzoom) {
|
|
||||||
// No need to download! - the layer is disabled
|
// No need to download! - the layer is disabled
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
import FeatureSource from "./FeatureSource";
|
|
||||||
import {UIEventSource} from "../UIEventSource";
|
|
||||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
|
||||||
|
|
||||||
export default class ZoomRespectingFeatureSource implements FeatureSource{
|
|
||||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
|
||||||
public readonly name: string;
|
|
||||||
|
|
||||||
constructor(layerConfig: LayerConfig, location: UIEventSource<{zoom: number}>, upstream: FeatureSource) {
|
|
||||||
this.name = "zoomrespecting("+upstream.name+")"
|
|
||||||
const empty = []
|
|
||||||
this.features = upstream.features.map(
|
|
||||||
features => {
|
|
||||||
const z = location.data.zoom
|
|
||||||
|
|
||||||
if(layerConfig.minzoom < z || layerConfig.maxzoom > z){
|
|
||||||
return empty
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return features
|
|
||||||
},[location]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
9
Models/FilteredLayer.ts
Normal file
9
Models/FilteredLayer.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import {UIEventSource} from "../Logic/UIEventSource";
|
||||||
|
import {TagsFilter} from "../Logic/Tags/TagsFilter";
|
||||||
|
import LayerConfig from "../Customizations/JSON/LayerConfig";
|
||||||
|
|
||||||
|
export default interface FilteredLayer {
|
||||||
|
readonly isDisplayed: UIEventSource<boolean>;
|
||||||
|
readonly appliedFilters: UIEventSource<TagsFilter>;
|
||||||
|
readonly layerDef: LayerConfig;
|
||||||
|
}
|
11
State.ts
11
State.ts
|
@ -21,6 +21,7 @@ import {Relation} from "./Logic/Osm/ExtractRelations";
|
||||||
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
|
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
|
||||||
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
|
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
|
||||||
import { TagsFilter } from "./Logic/Tags/TagsFilter";
|
import { TagsFilter } from "./Logic/Tags/TagsFilter";
|
||||||
|
import FilteredLayer from "./Models/FilteredLayer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains the global state: a bunch of UI-event sources
|
* Contains the global state: a bunch of UI-event sources
|
||||||
|
@ -61,15 +62,7 @@ export default class State {
|
||||||
|
|
||||||
public osmApiFeatureSource: OsmApiFeatureSource;
|
public osmApiFeatureSource: OsmApiFeatureSource;
|
||||||
|
|
||||||
public filteredLayers: UIEventSource<{
|
public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([]);
|
||||||
readonly isDisplayed: UIEventSource<boolean>;
|
|
||||||
readonly appliedFilters: UIEventSource<TagsFilter>;
|
|
||||||
readonly layerDef: LayerConfig;
|
|
||||||
}[]> = new UIEventSource<{
|
|
||||||
readonly isDisplayed: UIEventSource<boolean>;
|
|
||||||
readonly appliedFilters: UIEventSource<TagsFilter>;
|
|
||||||
readonly layerDef: LayerConfig;
|
|
||||||
}[]>([]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The latest element that was selected
|
The latest element that was selected
|
||||||
|
|
|
@ -23,7 +23,7 @@ export class VariableUiElement extends BaseUIElement {
|
||||||
el.innerHTML = contents;
|
el.innerHTML = contents;
|
||||||
} else if (contents instanceof Array) {
|
} else if (contents instanceof Array) {
|
||||||
for (const content of contents) {
|
for (const content of contents) {
|
||||||
const c = content.ConstructElement();
|
const c = content?.ConstructElement();
|
||||||
if (c !== undefined && c !== null) {
|
if (c !== undefined && c !== null) {
|
||||||
el.appendChild(c);
|
el.appendChild(c);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,9 @@ import {TagsFilter} from "../../Logic/Tags/TagsFilter";
|
||||||
import {And} from "../../Logic/Tags/And";
|
import {And} from "../../Logic/Tags/And";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import BaseUIElement from "../BaseUIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
import State from "../../State";
|
||||||
|
import {control} from "leaflet";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the filter
|
* Shows the filter
|
||||||
|
@ -22,12 +25,16 @@ export default class FilterView extends VariableUiElement {
|
||||||
constructor(filteredLayer) {
|
constructor(filteredLayer) {
|
||||||
super(
|
super(
|
||||||
filteredLayer.map((filteredLayers) =>
|
filteredLayer.map((filteredLayers) =>
|
||||||
filteredLayers.map(FilterView.createOneFilteredLayerElement)
|
filteredLayers.map(l => FilterView.createOneFilteredLayerElement(l))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static createOneFilteredLayerElement(filteredLayer) {
|
private static createOneFilteredLayerElement(filteredLayer) {
|
||||||
|
if(filteredLayer.layerDef.name === undefined){
|
||||||
|
// Name is not defined: we hide this one
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem";
|
const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem";
|
||||||
|
|
||||||
const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle);
|
const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle);
|
||||||
|
@ -52,9 +59,19 @@ export default class FilterView extends VariableUiElement {
|
||||||
.Clone()
|
.Clone()
|
||||||
.SetStyle("font-size:large;padding-left:1.25rem");
|
.SetStyle("font-size:large;padding-left:1.25rem");
|
||||||
|
|
||||||
|
const zoomStatus =
|
||||||
|
new Toggle(
|
||||||
|
undefined,
|
||||||
|
Translations.t.general.layerSelection.zoomInToSeeThisLayer.Clone()
|
||||||
|
.SetClass("alert")
|
||||||
|
.SetStyle("display: block ruby;width:min-content;"),
|
||||||
|
State.state.locationControl.map(location =>location.zoom > filteredLayer.layerDef.minzoom )
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
const style =
|
const style =
|
||||||
"display:flex;align-items:center;color:#007759;padding:0.5rem 0;";
|
"display:flex;align-items:center;color:#007759;padding:0.5rem 0;";
|
||||||
const layerChecked = new Combine([icon, styledNameChecked])
|
const layerChecked = new Combine([icon, styledNameChecked, zoomStatus])
|
||||||
.SetStyle(style)
|
.SetStyle(style)
|
||||||
.onClick(() => filteredLayer.isDisplayed.setData(false));
|
.onClick(() => filteredLayer.isDisplayed.setData(false));
|
||||||
|
|
||||||
|
@ -62,25 +79,27 @@ export default class FilterView extends VariableUiElement {
|
||||||
.SetStyle(style)
|
.SetStyle(style)
|
||||||
.onClick(() => filteredLayer.isDisplayed.setData(true));
|
.onClick(() => filteredLayer.isDisplayed.setData(true));
|
||||||
|
|
||||||
|
|
||||||
const filterPanel: BaseUIElement = FilterView.createFilterPanel(filteredLayer)
|
const filterPanel: BaseUIElement = FilterView.createFilterPanel(filteredLayer)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return new Toggle(
|
return new Toggle(
|
||||||
new Combine([layerChecked, filterPanel]),
|
new Combine([layerChecked, filterPanel]),
|
||||||
layerNotChecked,
|
layerNotChecked,
|
||||||
filteredLayer.isDisplayed
|
filteredLayer.isDisplayed
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static createFilterPanel(flayer: {
|
static createFilterPanel(flayer: {
|
||||||
layerDef: LayerConfig,
|
layerDef: LayerConfig,
|
||||||
appliedFilters: UIEventSource<TagsFilter>
|
appliedFilters: UIEventSource<TagsFilter>
|
||||||
}): BaseUIElement{
|
}): BaseUIElement {
|
||||||
const layer = flayer.layerDef
|
const layer = flayer.layerDef
|
||||||
if(layer.filters.length === 0){
|
if (layer.filters.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let listFilterElements: [BaseUIElement, UIEventSource<TagsFilter>][] = layer.filters.map(
|
let listFilterElements: [BaseUIElement, UIEventSource<TagsFilter>][] = layer.filters.map(
|
||||||
FilterView.createFilter
|
FilterView.createFilter
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import BackgroundSelector from "./BackgroundSelector";
|
import BackgroundSelector from "./BackgroundSelector";
|
||||||
import LayerSelection from "./LayerSelection";
|
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
|
||||||
import State from "../../State";
|
|
||||||
import Toggle from "../Input/Toggle";
|
|
||||||
import Combine from "../Base/Combine";
|
|
||||||
import Translations from "../i18n/Translations";
|
|
||||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
|
||||||
import BaseUIElement from "../BaseUIElement";
|
|
||||||
import {Translation} from "../i18n/Translation";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the panel with all layers and a toggle for each of them
|
|
||||||
*/
|
|
||||||
export default class LayerSelection extends Combine {
|
|
||||||
|
|
||||||
|
|
||||||
constructor(activeLayers: UIEventSource<{
|
|
||||||
readonly isDisplayed: UIEventSource<boolean>,
|
|
||||||
readonly layerDef: LayerConfig;
|
|
||||||
}[]>) {
|
|
||||||
|
|
||||||
if (activeLayers === undefined) {
|
|
||||||
throw "ActiveLayers should be defined..."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const checkboxes: BaseUIElement[] = [];
|
|
||||||
|
|
||||||
for (const layer of activeLayers.data) {
|
|
||||||
const leafletStyle = layer.layerDef.GenerateLeafletStyle(
|
|
||||||
new UIEventSource<any>({id: "node/-1"}),
|
|
||||||
false)
|
|
||||||
const leafletStyleNa = layer.layerDef.GenerateLeafletStyle(
|
|
||||||
new UIEventSource<any>({id: "node/-1"}),
|
|
||||||
false)
|
|
||||||
const icon = new Combine([leafletStyle.icon.html]).SetClass("single-layer-selection-toggle")
|
|
||||||
let iconUnselected: BaseUIElement = new Combine([leafletStyleNa.icon.html])
|
|
||||||
.SetClass("single-layer-selection-toggle")
|
|
||||||
.SetStyle("opacity:0.2;");
|
|
||||||
|
|
||||||
if (layer.layerDef.name === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name: Translation = Translations.WT(layer.layerDef.name)?.Clone()
|
|
||||||
name.SetStyle("font-size:large;margin-left: 0.5em;");
|
|
||||||
|
|
||||||
const zoomStatus = new VariableUiElement(State.state.locationControl.map(location => {
|
|
||||||
if (location.zoom < layer.layerDef.minzoom) {
|
|
||||||
return Translations.t.general.layerSelection.zoomInToSeeThisLayer.Clone()
|
|
||||||
.SetClass("alert")
|
|
||||||
.SetStyle("display: block ruby;width:min-content;")
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}))
|
|
||||||
const zoomStatusNonActive = new VariableUiElement(State.state.locationControl.map(location => {
|
|
||||||
if (location.zoom < layer.layerDef.minzoom) {
|
|
||||||
return Translations.t.general.layerSelection.zoomInToSeeThisLayer.Clone()
|
|
||||||
.SetClass("alert")
|
|
||||||
.SetStyle("display: block ruby;width:min-content;")
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}))
|
|
||||||
|
|
||||||
const style = "display:flex;align-items:center;"
|
|
||||||
const styleWhole = "display:flex; flex-wrap: wrap"
|
|
||||||
checkboxes.push(new Toggle(
|
|
||||||
new Combine([new Combine([icon, name.Clone()]).SetStyle(style), zoomStatus])
|
|
||||||
.SetStyle(styleWhole),
|
|
||||||
new Combine([new Combine([iconUnselected, "<del>", name.Clone(), "</del>"]).SetStyle(style), zoomStatusNonActive])
|
|
||||||
.SetStyle(styleWhole),
|
|
||||||
layer.isDisplayed).ToggleOnClick()
|
|
||||||
.SetStyle("margin:0.3em;")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
super(checkboxes)
|
|
||||||
this.SetStyle("display:flex;flex-direction:column;")
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -118,7 +118,7 @@ export class RadioButton<T> extends InputElement<T> {
|
||||||
const label = document.createElement("label");
|
const label = document.createElement("label");
|
||||||
label.appendChild(labelHtml);
|
label.appendChild(labelHtml);
|
||||||
label.htmlFor = input.id;
|
label.htmlFor = input.id;
|
||||||
label.classList.add("block", "w-full", "cursor-pointer", "bg-red");
|
label.classList.add("flex", "w-full", "cursor-pointer", "bg-red");
|
||||||
|
|
||||||
if (!this._dontStyle) {
|
if (!this._dontStyle) {
|
||||||
labelHtml.classList.add("p-2")
|
labelHtml.classList.add("p-2")
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default class Translations {
|
||||||
|
|
||||||
|
|
||||||
static T(t: string | any, context = undefined): Translation {
|
static T(t: string | any, context = undefined): Translation {
|
||||||
if(t === undefined){
|
if(t === undefined || t === null){
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if(typeof t === "string"){
|
if(typeof t === "string"){
|
||||||
|
@ -38,7 +38,7 @@ export default class Translations {
|
||||||
|
|
||||||
private static wtcache = {}
|
private static wtcache = {}
|
||||||
public static WT(s: string | Translation): Translation {
|
public static WT(s: string | Translation): Translation {
|
||||||
if(s === undefined){
|
if(s === undefined || s === null){
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (typeof (s) === "string") {
|
if (typeof (s) === "string") {
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--subtle-detail-color: #007759;
|
--subtle-detail-color: #e5f5ff;
|
||||||
--subtle-detail-color-contrast: black;
|
--subtle-detail-color-contrast: black;
|
||||||
--subtle-detail-color-light-contrast: lightgrey;
|
--subtle-detail-color-light-contrast: lightgrey;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue