Merge branch 'develop' into feature/maproulette
This commit is contained in:
commit
64560b9cd2
279 changed files with 13050 additions and 4684 deletions
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>())
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue