Merge branch 'develop' into feature/maproulette

This commit is contained in:
Robin van der Linde 2022-07-27 09:28:42 +02:00
commit 64560b9cd2
Signed by untrusted user: Robin-van-der-Linde
GPG key ID: 53956B3252478F0D
279 changed files with 13050 additions and 4684 deletions

View file

@ -23,8 +23,11 @@ import TileFreshnessCalculator from "./TileFreshnessCalculator";
import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource";
import MapState from "../State/MapState";
import {ElementStorage} from "../ElementStorage";
import {Feature, Geometry} from "@turf/turf";
import {OsmFeature} from "../../Models/OsmFeature";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import {FilterState} from "../../Models/FilteredLayer";
import {GeoOperations} from "../GeoOperations";
import {Utils} from "../../Utils";
/**
@ -514,6 +517,62 @@ export default class FeaturePipeline {
return updater;
}
/**
* Builds upon 'GetAllFeaturesAndMetaWithin', but does stricter BBOX-checking and applies the filters
*/
public getAllVisibleElementsWithmeta(bbox: BBox): { center: [number, number], element: OsmFeature, layer: LayerConfig }[] {
if (bbox === undefined) {
console.warn("No bbox")
return []
}
const layers = Utils.toIdRecord(this.state.layoutToUse.layers)
const elementsWithMeta: { features: OsmFeature[], layer: string }[] = this.GetAllFeaturesAndMetaWithin(bbox)
let elements: {center: [number, number], element: OsmFeature, layer: LayerConfig }[] = []
let seenElements = new Set<string>()
for (const elementsWithMetaElement of elementsWithMeta) {
const layer = layers[elementsWithMetaElement.layer]
if(layer.title === undefined){
continue
}
const filtered = this.state.filteredLayers.data.find(fl => fl.layerDef == layer);
for (let i = 0; i < elementsWithMetaElement.features.length; i++) {
const element = elementsWithMetaElement.features[i];
if (!filtered.isDisplayed.data) {
continue
}
if (seenElements.has(element.properties.id)) {
continue
}
seenElements.add(element.properties.id)
if (!bbox.overlapsWith(BBox.get(element))) {
continue
}
if (layer?.isShown !== undefined && !layer.isShown.matchesProperties(element)) {
continue
}
const activeFilters: FilterState[] = Array.from(filtered.appliedFilters.data.values());
if (!activeFilters.every(filter => filter?.currentFilter === undefined || filter?.currentFilter?.matchesProperties(element.properties))) {
continue
}
const center = GeoOperations.centerpointCoordinates(element);
elements.push({
element,
center,
layer: layers[elementsWithMetaElement.layer],
})
}
}
return elements;
}
/**
* Inject a new point
*/

View file

@ -1,10 +1,9 @@
import {UIEventSource} from "../../UIEventSource";
import FilteredLayer from "../../../Models/FilteredLayer";
import {Store, UIEventSource} from "../../UIEventSource";
import FilteredLayer, {FilterState} from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {BBox} from "../../BBox";
import {ElementStorage} from "../../ElementStorage";
import {TagsFilter} from "../../Tags/TagsFilter";
import {tag} from "@turf/turf";
import {OsmFeature} from "../../../Models/OsmFeature";
export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled {
@ -16,7 +15,9 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
public readonly bbox: BBox
private readonly upstream: FeatureSourceForLayer;
private readonly state: {
locationControl: UIEventSource<{ zoom: number }>; selectedElement: UIEventSource<any>,
locationControl: Store<{ zoom: number }>;
selectedElement: Store<any>,
globalFilters: Store<{ filter: FilterState }[]>,
allElements: ElementStorage
};
private readonly _alreadyRegistered = new Set<UIEventSource<any>>();
@ -25,9 +26,10 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
constructor(
state: {
locationControl: UIEventSource<{ zoom: number }>,
selectedElement: UIEventSource<any>,
allElements: ElementStorage
locationControl: Store<{ zoom: number }>,
selectedElement: Store<any>,
allElements: ElementStorage,
globalFilters: Store<{ filter: FilterState }[]>
},
tileIndex,
upstream: FeatureSourceForLayer,
@ -60,6 +62,10 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
metataggingUpdated?.addCallback(_ => {
self._is_dirty.setData(true)
})
state.globalFilters.addCallback(_ => {
self.update()
})
this.update();
}
@ -69,6 +75,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
const layer = this.upstream.layer;
const features: { feature: OsmFeature; freshness: Date }[] = (this.upstream.features.data ?? []);
const includedFeatureIds = new Set<string>();
const globalFilters = self.state.globalFilters.data.map(f => f.filter);
const newFeatures = (features ?? []).filter((f) => {
self.registerCallback(f.feature)
@ -88,6 +95,14 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
}
}
for (const filter of globalFilters) {
const neededTags: TagsFilter = filter?.currentFilter
if (neededTags !== undefined && !neededTags.matchesProperties(f.feature.properties)) {
// Hidden by the filter on the layer itself - we want to hide it no matter what
return false;
}
}
includedFeatureIds.add(f.feature.properties.id)
return true;
});

View file

@ -72,7 +72,7 @@ export default class CreateNewNodeAction extends OsmCreateAction {
this.setElementId(id)
for (const kv of this._basicTags) {
if (typeof kv.value !== "string") {
throw "Invalid value: don't use a regex in a preset"
throw "Invalid value: don't use non-string value in a preset. The tag "+kv.key+"="+kv.value+" is not a string, the value is a "+typeof kv.value
}
properties[kv.key] = kv.value;
}

View file

@ -8,6 +8,7 @@ import {FixedUiElement} from "../UI/Base/FixedUiElement";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import {CountryCoder} from "latlon2country"
import Constants from "../Models/Constants";
import {TagUtils} from "./Tags/TagUtils";
export class SimpleMetaTagger {
@ -32,7 +33,7 @@ export class SimpleMetaTagger {
if (!docs.cleanupRetagger) {
for (const key of docs.keys) {
if (!key.startsWith('_') && key.toLowerCase().indexOf("theme") < 0) {
throw `Incorrect metakey ${key}: it should start with underscore (_)`
throw `Incorrect key for a calculated meta value '${key}': it should start with underscore (_)`
}
}
}
@ -211,6 +212,27 @@ export default class SimpleMetaTaggers {
return true;
})
);
private static levels = new SimpleMetaTagger(
{
doc: "Extract the 'level'-tag into a normalized, ';'-separated value",
keys: ["_level"]
},
((feature) => {
if (feature.properties["level"] === undefined) {
return false;
}
const l = feature.properties["level"]
const newValue = TagUtils.LevelsParser(l).join(";")
if(l === newValue) {
return false;
}
feature.properties["level"] = newValue
return true
})
)
private static canonicalize = new SimpleMetaTagger(
{
doc: "If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical form (e.g. `1meter` will be rewritten to `1m`)",
@ -218,7 +240,7 @@ export default class SimpleMetaTaggers {
},
((feature, _, __, state) => {
const units = Utils.NoNull([].concat(...state?.layoutToUse?.layers?.map(layer => layer.units )?? []));
const units = Utils.NoNull([].concat(...state?.layoutToUse?.layers?.map(layer => layer.units) ?? []));
if (units.length == 0) {
return;
}
@ -317,7 +339,7 @@ export default class SimpleMetaTaggers {
country_code: tags._country.toLowerCase(),
state: undefined
}
}, <any> {tag_key: "opening_hours"});
}, <any>{tag_key: "opening_hours"});
// Recalculate!
return oh.getState() ? "yes" : "no";
@ -327,12 +349,12 @@ export default class SimpleMetaTaggers {
delete tags._isOpen
tags["_isOpen"] = "parse_error";
}
}});
}
});
const tagsSource = state.allElements.getEventSourceById(feature.properties.id);
})
)
@ -400,7 +422,8 @@ export default class SimpleMetaTaggers {
SimpleMetaTaggers.currentTime,
SimpleMetaTaggers.objectMetaInfo,
SimpleMetaTaggers.noBothButLeftRight,
SimpleMetaTaggers.geometryType
SimpleMetaTaggers.geometryType,
SimpleMetaTaggers.levels
];
public static readonly lazyTags: string[] = [].concat(...SimpleMetaTaggers.metatags.filter(tagger => tagger.isLazy)

View file

@ -19,6 +19,19 @@ import TitleHandler from "../Actors/TitleHandler";
import {BBox} from "../BBox";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import {TiledStaticFeatureSource} from "../FeatureSource/Sources/StaticFeatureSource";
import {Translation, TypedTranslation} from "../../UI/i18n/Translation";
import {Tag} from "../Tags/Tag";
export interface GlobalFilter {
filter: FilterState,
id: string,
onNewPoint: {
safetyCheck: Translation,
confirmAddNew: TypedTranslation<{ preset: Translation }>
tags: Tag[]
}
}
/**
* Contains all the leaflet-map related state
@ -78,6 +91,12 @@ export default class MapState extends UserRelatedState {
* Which layers are enabled in the current theme and what filters are applied onto them
*/
public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers");
/**
* Filters which apply onto all layers
*/
public globalFilters: UIEventSource<GlobalFilter[]> = new UIEventSource([], "globalFilters")
/**
* Which overlays are shown
*/
@ -121,9 +140,9 @@ export default class MapState extends UserRelatedState {
this.overlayToggles = this.layoutToUse?.tileLayerSources
?.filter(c => c.name !== undefined)
?.map(c => ({
config: c,
isDisplayed: QueryParameters.GetBooleanQueryParameter("overlay-" + c.id, c.defaultState, "Wether or not the overlay " + c.id + " is shown")
})) ?? []
config: c,
isDisplayed: QueryParameters.GetBooleanQueryParameter("overlay-" + c.id, c.defaultState, "Wether or not the overlay " + c.id + " is shown")
})) ?? []
this.filteredLayers = this.InitializeFilteredLayers()
@ -206,7 +225,7 @@ export default class MapState extends UserRelatedState {
return [feature]
})
this.currentView = new TiledStaticFeatureSource(features, currentViewLayer);
this.currentView = new TiledStaticFeatureSource(features, currentViewLayer);
}
private initGpsLocation() {
@ -335,26 +354,24 @@ export default class MapState extends UserRelatedState {
}
private getPref(key: string, layer: LayerConfig): UIEventSource<boolean> {
const pref = this.osmConnection
.GetPreference(key)
return this.osmConnection
.GetPreference(key, layer.shownByDefault + "")
.sync(v => {
if(v === undefined){
if (v === undefined) {
return undefined
}
return v === "true";
}, [], b => {
if(b === undefined){
if (b === undefined) {
return undefined
}
return "" + b;
})
pref.setData(layer.shownByDefault)
return pref
}
private InitializeFilteredLayers() {
const layoutToUse = this.layoutToUse;
if(layoutToUse === undefined){
if (layoutToUse === undefined) {
return new UIEventSource<FilteredLayer[]>([])
}
const flayers: FilteredLayer[] = [];
@ -363,16 +380,19 @@ export default class MapState extends UserRelatedState {
if (layer.syncSelection === "local") {
isDisplayed = LocalStorageSource.GetParsed(layoutToUse.id + "-layer-" + layer.id + "-enabled", layer.shownByDefault)
} else if (layer.syncSelection === "theme-only") {
isDisplayed = this.getPref(layoutToUse.id+ "-layer-" + layer.id + "-enabled", layer)
isDisplayed = this.getPref(layoutToUse.id + "-layer-" + layer.id + "-enabled", layer)
} else if (layer.syncSelection === "global") {
isDisplayed = this.getPref("layer-" + layer.id + "-enabled", layer)
} else {
isDisplayed = QueryParameters.GetBooleanQueryParameter("layer-" + layer.id, layer.shownByDefault, "Wether or not layer "+layer.id+" is shown")
isDisplayed = QueryParameters.GetBooleanQueryParameter("layer-" + layer.id, layer.shownByDefault, "Wether or not layer " + layer.id + " is shown")
}
isDisplayed.addCallbackAndRun(_ => {
console.log("IsDisplayed?",layer.id, isDisplayed.data, layer.shownByDefault)
})
const flayer: FilteredLayer = {
isDisplayed: isDisplayed,
isDisplayed,
layerDef: layer,
appliedFilters: new UIEventSource<Map<string, FilterState>>(new Map<string, FilterState>())
};

View file

@ -127,7 +127,7 @@ export class TagUtils {
* }
* ]})
* TagUtils.FlattenMultiAnswer([tag]) // => TagUtils.Tag({and:["x=a;b", "y=0;1;2;3"] })
*
*
* TagUtils.FlattenMultiAnswer(([new Tag("x","y"), new Tag("a","b")])) // => new And([new Tag("x","y"), new Tag("a","b")])
* TagUtils.FlattenMultiAnswer(([new Tag("x","")])) // => new And([new Tag("x","")])
*/
@ -240,7 +240,7 @@ export class TagUtils {
*
* TagUtils.Tag("xyz<5").matchesProperties({xyz: 4}) // => true
* TagUtils.Tag("xyz<5").matchesProperties({xyz: 5}) // => false
*
*
* // RegexTags must match values with newlines
* TagUtils.Tag("note~.*aed.*").matchesProperties({note: "Hier bevindt zich wss een defibrillator. \\n\\n De aed bevindt zich op de 5de verdieping"}) // => true
* TagUtils.Tag("note~i~.*aed.*").matchesProperties({note: "Hier bevindt zich wss een defibrillator. \\n\\n De AED bevindt zich op de 5de verdieping"}) // => true
@ -264,13 +264,13 @@ export class TagUtils {
* @constructor
*/
public static TagD(json?: TagConfigJson, context: string = ""): TagsFilter | undefined {
if(json === undefined){
if (json === undefined) {
return undefined
}
return TagUtils.Tag(json, context)
}
/**
* INLINE sort of the given list
*/
@ -492,6 +492,16 @@ export class TagUtils {
}
return " (" + joined + ") "
}
public static ExtractSimpleTags(tf: TagsFilter) : Tag[] {
const result: Tag[] = []
tf.visit(t => {
if(t instanceof Tag){
result.push(t)
}
})
return result;
}
/**
* Returns 'true' is opposite tags are detected.
@ -581,4 +591,38 @@ export class TagUtils {
return listToFilter.some(tf => guards.some(guard => guard.shadows(tf)))
}
/**
* Parses a level specifier to the various available levels
*
* TagUtils.LevelsParser("0") // => ["0"]
* TagUtils.LevelsParser("1") // => ["1"]
* TagUtils.LevelsParser("0;2") // => ["0","2"]
* TagUtils.LevelsParser("0-5") // => ["0","1","2","3","4","5"]
* TagUtils.LevelsParser("0") // => ["0"]
* TagUtils.LevelsParser("-1") // => ["-1"]
* TagUtils.LevelsParser("0;-1") // => ["0", "-1"]
*/
public static LevelsParser(level: string): string[] {
let spec = Utils.NoNull([level])
spec = [].concat(...spec.map(s => s?.split(";")))
spec = [].concat(...spec.map(s => {
s = s.trim()
if (s.indexOf("-") < 0 || s.startsWith("-")) {
return s
}
const [start, end] = s.split("-").map(s => Number(s.trim()))
if (isNaN(start) || isNaN(end)) {
return undefined
}
const values = []
for (let i = start; i <= end; i++) {
values.push(i + "")
}
return values
}))
return Utils.NoNull(spec);
}
}