First working version of the notes-layer, add filtering

This commit is contained in:
pietervdvn 2022-01-07 17:31:39 +01:00
parent ebb510da04
commit 91d2272861
19 changed files with 282 additions and 109 deletions

View file

@ -403,6 +403,10 @@ export class ExtraFunctions {
];
public static FullPatchFeature(params: ExtraFuncParams, feature) {
if(feature._is_patched){
return
}
feature._is_patched = true
for (const func of ExtraFunctions.allFuncs) {
feature[func._name] = func._f(params, feature)
}

View file

@ -59,6 +59,7 @@ export default class FeaturePipeline {
private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>()
private readonly metataggingRecalculated = new UIEventSource<void>(undefined)
private readonly requestMetataggingRecalculation = new UIEventSource<Date>(undefined)
/**
* Keeps track of all raw OSM-nodes.
@ -97,6 +98,10 @@ export default class FeaturePipeline {
}
);
this.requestMetataggingRecalculation.stabilized(500).addCallbackAndRunD(_ => {
self.updateAllMetaTagging("Request stabilized")
})
const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed)
const perLayerHierarchy = new Map<string, TileHierarchyMerger>()
@ -141,7 +146,7 @@ export default class FeaturePipeline {
tile => {
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
});
continue;
}
@ -169,7 +174,10 @@ export default class FeaturePipeline {
if (id === "current_view") {
handlePriviligedFeatureSource(state.currentView)
state.currentView.features.map(ffs => ffs[0]?.feature?.properties?.id).withEqualityStabilized((x,y) => x === y)
.addCallbackAndRunD(_ => self.applyMetaTags(state.currentView, state))
.addCallbackAndRunD(_ => {
self.applyMetaTags(state.currentView, <any>this.state, `currentview changed`)
}
)
continue
}
@ -187,7 +195,7 @@ export default class FeaturePipeline {
console.debug("Loaded tile ", id, tile.tileIndex, "from local cache")
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
hierarchy.registerTile(tile);
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
}
)
@ -207,13 +215,13 @@ export default class FeaturePipeline {
registerTile: (tile) => {
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
perLayerHierarchy.get(id).registerTile(tile)
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
}
})
} else {
new RegisteringAllFromFeatureSourceActor(src, state.allElements)
perLayerHierarchy.get(id).registerTile(src)
src.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(src))
src.features.addCallbackAndRunD(_ => self.onNewDataLoaded(src))
}
} else {
new DynamicGeoJsonTileSource(
@ -221,7 +229,7 @@ export default class FeaturePipeline {
tile => {
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
perLayerHierarchy.get(id).registerTile(tile)
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
},
state
)
@ -242,7 +250,7 @@ export default class FeaturePipeline {
saver?.addTile(tile)
}
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
},
state: state,
@ -282,7 +290,12 @@ export default class FeaturePipeline {
// We save the tile data for the given layer to local storage - data sourced from overpass
self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile)
perLayerHierarchy.get(source.layer.layerDef.id).registerTile(new RememberingSource(tile))
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
tile.features.addCallbackAndRunD(f => {
if(f.length === 0){
return
}
self.onNewDataLoaded(tile)
})
}
}),
@ -302,9 +315,7 @@ export default class FeaturePipeline {
// We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this
perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer)
// AT last, we always apply the metatags whenever possible
// @ts-ignore
perLayer.features.addCallbackAndRunD(_ => self.applyMetaTags(perLayer, state))
perLayer.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(perLayer))
perLayer.features.addCallbackAndRunD(_ => self.onNewDataLoaded(perLayer))
},
newGeometry
@ -312,8 +323,8 @@ export default class FeaturePipeline {
// Whenever fresh data comes in, we need to update the metatagging
self.newDataLoadedSignal.stabilized(250).addCallback(_ => {
self.updateAllMetaTagging()
self.newDataLoadedSignal.stabilized(250).addCallback(src => {
self.updateAllMetaTagging(`New data loaded by ${src.name} (and stabilized)`)
})
@ -325,7 +336,11 @@ export default class FeaturePipeline {
}, [osmFeatureSource.isRunning]
)
}
private onNewDataLoaded(src: FeatureSource){
this.newDataLoadedSignal.setData(src)
this.requestMetataggingRecalculation.setData(new Date())
}
public GetAllFeaturesWithin(bbox: BBox): any[][] {
@ -471,12 +486,16 @@ export default class FeaturePipeline {
return updater;
}
private applyMetaTags(src: FeatureSourceForLayer, state: any) {
private applyMetaTags(src: FeatureSourceForLayer, state: any, reason: string) {
const self = this
if(src === undefined){
throw "Src is undefined"
}
const layerDef = src.layer.layerDef;
console.debug(`Applying metatags onto ${src.name} due to ${reason} which has ${src.features.data?.length} features`)
if(src.features.data.length == 0){
return
}
MetaTagging.addMetatags(
src.features.data,
{
@ -495,17 +514,14 @@ export default class FeaturePipeline {
}
public updateAllMetaTagging() {
public updateAllMetaTagging(reason: string) {
const self = this;
console.debug("Updating the meta tagging of all tiles as new data got loaded")
this.perLayerHierarchy.forEach(hierarchy => {
hierarchy.loadedTiles.forEach(tile => {
self.applyMetaTags(tile, <any> this.state)
self.applyMetaTags(tile, <any> this.state, `${reason} (tile ${tile.tileIndex})`)
})
})
if(this.state.currentView !== undefined){
this.applyMetaTags(this.state.currentView, <any> this.state)
}
self.metataggingRecalculated.ping()
}

View file

@ -26,7 +26,7 @@ export default class PerLayerFeatureSourceSplitter {
if (features === undefined) {
return;
}
if (layers.data === undefined) {
if (layers.data === undefined || layers.data.length === 0) {
return;
}

View file

@ -4,7 +4,6 @@ import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import Hash from "../../Web/Hash";
import {BBox} from "../../BBox";
import {ElementStorage} from "../../ElementStorage";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled {
public features: UIEventSource<{ feature: any; freshness: Date }[]> =
@ -71,8 +70,8 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
self.registerCallback(f.feature)
if (
this.state.selectedElement.data?.id === f.feature.id ||
f.feature.id === Hash.hash.data) {
(this.state.selectedElement !== undefined && this.state.selectedElement.data?.id === f.feature.properties.id) ||
(Hash.hash.data !== undefined && f.feature.properties.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;
}
@ -89,6 +88,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
}
const tagsFilter = layer.appliedFilters.data;
console.log("Current filters for "+layer.layerDef.id+" are ",tagsFilter)
for (const filter of tagsFilter ?? []) {
const neededTags = filter.filter.options[filter.selected].osmTags
if (!neededTags.matchesProperties(f.feature.properties)) {

View file

@ -44,6 +44,10 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor
return undefined
}
const tileRange = Tiles.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
if(tileRange.total > 10000){
console.error("Got a really big tilerange, bounds and location might be out of sync")
return undefined
}
const needed = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i))
if (needed.length === 0) {

View file

@ -28,7 +28,6 @@ export default class MetaTagging {
includeDates?: true | boolean,
includeNonDates?: true | boolean
}): boolean {
if (features === undefined || features.length === 0) {
return;
}
@ -106,7 +105,6 @@ export default class MetaTagging {
}
public static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => void)[] {
const functions: ((feature: any) => any)[] = [];
for (const entry of calculatedTags) {
const key = entry[0]
const code = entry[1];
@ -148,6 +146,7 @@ export default class MetaTagging {
// Lazy function
const f = (feature: any) => {
const oldValue = feature.properties[key]
delete feature.properties[key]
Object.defineProperty(feature.properties, key, {
configurable: true,

View file

@ -9,7 +9,6 @@ import MapState from "./MapState";
import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler";
import Hash from "../Web/Hash";
import {BBox} from "../BBox";
import {FeatureSourceForLayer} from "../FeatureSource/FeatureSource";
export default class FeaturePipelineState extends MapState {
@ -33,7 +32,7 @@ export default class FeaturePipelineState extends MapState {
const sourceBBox = source.features.map(allFeatures => BBox.bboxAroundAll(allFeatures.map(f => BBox.get(f.feature))))
// Do show features indicates if the 'showDataLayer' should be shown
// Do show features indicates if the respective 'showDataLayer' should be shown. It can be hidden by e.g. clustering
const doShowFeatures = source.features.map(
f => {
const z = self.locationControl.data.zoom

View file

@ -22,7 +22,7 @@ export default class Constants {
/**
* Layer IDs of layers which have special properties through built-in hooks
*/
public static readonly priviliged_layers: string[] = [...Constants.added_by_default, "type_node", ...Constants.no_include]
public static readonly priviliged_layers: string[] = [...Constants.added_by_default, "type_node", "notes", ...Constants.no_include]
// The user journey states thresholds when a new feature gets unlocked

View file

@ -3,12 +3,18 @@ import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import FilterConfigJson from "./Json/FilterConfigJson";
import Translations from "../../UI/i18n/Translations";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import ValidatedTextField from "../../UI/Input/ValidatedTextField";
import {Utils} from "../../Utils";
import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
import {AndOrTagConfigJson} from "./Json/TagConfigJson";
export default class FilterConfig {
public readonly id: string
public readonly options: {
question: Translation;
osmTags: TagsFilter;
originalTagsSpec: string | AndOrTagConfigJson
fields: { name: string, type: string }[]
}[];
constructor(json: FilterConfigJson, context: string) {
@ -28,23 +34,49 @@ export default class FilterConfig {
}
this.id = json.id;
this.options = json.options.map((option, i) => {
const ctx = `${context}.options[${i}]`;
const question = Translations.T(
option.question,
context + ".options-[" + i + "].question"
`${ctx}.question`
);
const osmTags = TagUtils.Tag(
let osmTags = TagUtils.Tag(
option.osmTags ?? {and: []},
`${context}.options-[${i}].osmTags`
`${ctx}.osmTags`
);
if (question === undefined) {
throw `Invalid filter: no question given at ${context}[${i}]`
throw `Invalid filter: no question given at ${ctx}`
}
return {question: question, osmTags: osmTags};
const fields: { name: string, type: string }[] = ((option.fields) ?? []).map((f, i) => {
const type = f.type ?? "string"
if (!ValidatedTextField.AllTypes.has(type)) {
throw `Invalid filter: ${type} is not a valid validated textfield type (at ${ctx}.fields[${i}])\n\tTry one of ${Array.from(ValidatedTextField.AllTypes.keys()).join(",")}`
}
if (f.name === undefined || f.name === "" || f.name.match(/[a-z0-9_-]+/) == null) {
throw `Invalid filter: a variable name should match [a-z0-9_-]+ at ${ctx}.fields[${i}]`
}
return {
name: f.name,
type
}
})
if(fields.length > 0){
// erase the tags, they aren't needed
osmTags = TagUtils.Tag({and:[]})
}
return {question: question, osmTags: osmTags, fields, originalTagsSpec: option.osmTags};
});
if (this.options.some(o => o.fields.length > 0) && this.options.length > 1) {
throw `Invalid filter at ${context}: a filter with textfields should only offer a single option.`
}
if (this.options.length > 1 && this.options[0].osmTags["and"]?.length !== 0) {
throw "Error in " + context + "." + this.id + ": the first option of a multi-filter should always be the 'reset' option and not have any filters"
}
}
}

View file

@ -11,5 +11,12 @@ export default interface FilterConfigJson {
* If there is only one option this will be a checkbox
* Filtering is done based on the given osmTags that are compared to the objects in that layer.
*/
options: { question: string | any; osmTags?: AndOrTagConfigJson | string }[];
options: {
question: string | any;
osmTags?: AndOrTagConfigJson | string,
fields?: {
name: string,
type?: string | "string"
}[]
}[];
}

View file

@ -111,7 +111,7 @@ export default class TagRenderingConfig {
}
if (ValidatedTextField.AllTypes[this.freeform.type] === undefined) {
if (!ValidatedTextField.AllTypes.has(this.freeform.type)) {
const knownKeys = ValidatedTextField.tpList.map(tp => tp.name).join(", ");
throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}`
}

View file

@ -16,7 +16,7 @@ export class Tiles {
const result: T[] = []
const total = tileRange.total
if (total > 100000) {
throw "Tilerange too big (z is "+tileRange.zoomlevel+")"
throw `Tilerange too big (z is ${tileRange.zoomlevel}, total tiles needed: ${tileRange.total})`
}
for (let x = tileRange.xstart; x <= tileRange.xend; x++) {
for (let y = tileRange.ystart; y <= tileRange.yend; y++) {

View file

@ -162,7 +162,7 @@ class AutomationPanel extends Combine{
return true;
}
stateToShow.setData("Applying metatags")
pipeline.updateAllMetaTagging()
pipeline.updateAllMetaTagging("triggered by automaton")
stateToShow.setData("Gathering applicable elements")
let handled = 0

View file

@ -14,6 +14,9 @@ import FilteredLayer from "../../Models/FilteredLayer";
import BackgroundSelector from "./BackgroundSelector";
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
import {SubstitutedTranslation} from "../SubstitutedTranslation";
import ValidatedTextField from "../Input/ValidatedTextField";
import {QueryParameters} from "../../Logic/Web/QueryParameters";
export default class FilterView extends VariableUiElement {
constructor(filteredLayer: UIEventSource<FilteredLayer[]>, tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]) {
@ -144,7 +147,7 @@ export default class FilterView extends VariableUiElement {
layer.filters.forEach((f, i) => filterIndexes.set(f.id, i))
let listFilterElements: [BaseUIElement, UIEventSource<{ filter: FilterConfig, selected: number }>][] = layer.filters.map(
FilterView.createFilter
filter => FilterView.createFilter(filter)
);
listFilterElements.forEach((inputElement, i) =>
@ -193,6 +196,71 @@ export default class FilterView extends VariableUiElement {
}
private static createFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<{ filter: FilterConfig, selected: number }>] {
if (filterConfig.options[0].fields.length > 0) {
// Filter which uses one or more textfields
const filter = filterConfig.options[0]
const mappings = new Map<string, BaseUIElement>()
let allValid = new UIEventSource(true)
const properties = new UIEventSource<any>({})
for (const {name, type} of filter.fields) {
const value = QueryParameters.GetQueryParameter("filter-" + filterConfig.id + "-" + name, "", "Value for filter " + filterConfig.id)
const field = ValidatedTextField.InputForType(type, {
value
}).SetClass("inline-block")
mappings.set(name, field)
const stable = value.stabilized(250)
stable.addCallbackAndRunD(v => {
properties.data[name] = v.toLowerCase();
properties.ping()
})
allValid = allValid.map(previous => previous && field.IsValid(stable.data) && stable.data !== "", [stable])
}
const tr = new SubstitutedTranslation(filter.question, new UIEventSource<any>({id: filterConfig.id}), State.state, mappings)
const neutral = {
filter: new FilterConfig({
id: filterConfig.id,
options: [
{
question: "--",
}
]
}, "While dynamically constructing a filterconfig"),
selected: 0
}
const trigger = allValid.map(isValid => {
if (!isValid) {
return neutral
}
// Replace all the field occurences in the tags...
const osmTags = Utils.WalkJson(filter.originalTagsSpec,
v => {
if (typeof v !== "string") {
return v
}
return Utils.SubstituteKeys(v, properties.data)
}
)
// ... which we use below to construct a filter!
return {
filter: new FilterConfig({
id: filterConfig.id,
options: [
{
question: "--",
osmTags
}
]
}, "While dynamically constructing a filterconfig"),
selected: 0
}
}, [properties])
return [tr, trigger];
}
if (filterConfig.options.length === 1) {
let option = filterConfig.options[0];

View file

@ -436,7 +436,7 @@ export default class ValidatedTextField {
/**
* {string (typename) --> TextFieldDef}
*/
public static AllTypes = ValidatedTextField.allTypesDict();
public static AllTypes: Map<string, TextFieldDef> = ValidatedTextField.allTypesDict();
public static InputForType(type: string, options?: {
placeholder?: string | BaseUIElement,
@ -455,7 +455,7 @@ export default class ValidatedTextField {
}): InputElement<string> {
options = options ?? {};
options.placeholder = options.placeholder ?? type;
const tp: TextFieldDef = ValidatedTextField.AllTypes[type]
const tp: TextFieldDef = ValidatedTextField.AllTypes.get(type)
const isValidTp = tp.isValid;
let isValid;
options.textArea = options.textArea ?? type === "text";
@ -615,10 +615,11 @@ export default class ValidatedTextField {
}
private static allTypesDict() {
const types = {};
private static allTypesDict(): Map<string, TextFieldDef> {
const types = new Map<string, TextFieldDef>();
for (const tp of ValidatedTextField.tpList) {
types[tp.name] = tp;
types.set(tp.name, tp);
}
return types;
}

View file

@ -43,8 +43,8 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI"), State.state)
.SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2");
const titleIcons = new Combine(
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon,
"block w-8 h-8 max-h-8 align-baseline box-content sm:p-0.5", "width: 2rem;")
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon, State.state,
"block w-8 h-8 max-h-8 align-baseline box-content sm:p-0.5 w-10",)
))
.SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")

View file

@ -45,7 +45,10 @@ There are also some technicalities in your theme to keep in mind:
The import button can be tested in an unofficial theme by adding \`test=true\` or \`backend=osm-test\` as [URL-paramter](URL_Parameters.md).
The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console.
In the case that MapComplete is pointed to the testing grounds, the edit will be made on https://master.apis.dev.openstreetmap.org`
private static knownKeys = ["addExtraTags", "and", "calculatedTags", "changesetmessage", "clustering", "color", "condition", "customCss", "dashArray", "defaultBackgroundId", "description", "descriptionTail", "doNotDownload", "enableAddNewPoints", "enableBackgroundLayerSelection", "enableGeolocation", "enableLayers", "enableMoreQuests", "enableSearch", "enableShareScreen", "enableUserBadge", "freeform", "hideFromOverview", "hideInAnswer", "icon", "iconOverlays", "iconSize", "id", "if", "ifnot", "isShown", "key", "language", "layers", "lockLocation", "maintainer", "mappings", "maxzoom", "maxZoom", "minNeededElements", "minzoom", "multiAnswer", "name", "or", "osmTags", "passAllFeatures", "presets", "question", "render", "roaming", "roamingRenderings", "rotation", "shortDescription", "socialImage", "source", "startLat", "startLon", "startZoom", "tagRenderings", "tags", "then", "title", "titleIcons", "type", "version", "wayHandling", "widenFactor", "width"]
private static extraKeys = ["nl", "en", "fr", "de", "pt", "es", "name", "phone", "email", "amenity", "leisure", "highway", "building", "yes", "no", "true", "false"]
private static injectedDownloads = {}
private static _download_cache = new Map<string, { promise: Promise<any>, timestamp: number }>()
/**
* Parses the arguments for special visualisations
@ -67,12 +70,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return parsed;
}
private static knownKeys = ["addExtraTags", "and", "calculatedTags", "changesetmessage", "clustering", "color", "condition", "customCss", "dashArray", "defaultBackgroundId", "description", "descriptionTail", "doNotDownload", "enableAddNewPoints", "enableBackgroundLayerSelection", "enableGeolocation", "enableLayers", "enableMoreQuests", "enableSearch", "enableShareScreen", "enableUserBadge", "freeform", "hideFromOverview", "hideInAnswer", "icon", "iconOverlays", "iconSize", "id", "if", "ifnot", "isShown", "key", "language", "layers", "lockLocation", "maintainer", "mappings", "maxzoom", "maxZoom", "minNeededElements", "minzoom", "multiAnswer", "name", "or", "osmTags", "passAllFeatures", "presets", "question", "render", "roaming", "roamingRenderings", "rotation", "shortDescription", "socialImage", "source", "startLat", "startLon", "startZoom", "tagRenderings", "tags", "then", "title", "titleIcons", "type", "version", "wayHandling", "widenFactor", "width"]
private static extraKeys = ["nl", "en", "fr", "de", "pt", "es", "name", "phone", "email", "amenity", "leisure", "highway", "building", "yes", "no", "true", "false"]
private static injectedDownloads = {}
private static _download_cache = new Map<string, { promise: Promise<any>, timestamp: number }>()
static EncodeXmlValue(str) {
if (typeof str !== "string") {
str = "" + str
@ -238,6 +235,13 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return [a.substr(0, index), a.substr(index + sep.length)];
}
/**
* Given a piece of text, will replace any key occuring in 'tags' by the corresponding value
* @param txt
* @param tags
* @param useLang
* @constructor
*/
public static SubstituteKeys(txt: string | undefined, tags: any, useLang?: string): string | undefined {
if (txt === undefined) {
return undefined
@ -342,6 +346,27 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return target;
}
static WalkJson(json: any, f: (v: number | string | boolean | undefined) => any) {
if(json === undefined){
return f(undefined)
}
const jtp = typeof json
if (jtp === "boolean" || jtp === "string" || jtp === "number"){
return f(json)
}
if (json.map !== undefined) {
return json.map(sub => {
return Utils.WalkJson(sub, f);
})
}
const cp = {...json}
for (const key in json) {
cp[key] = Utils.WalkJson(json[key], f)
}
return cp
}
static getOrSetDefault<K, V>(dict: Map<K, V>, k: K, v: () => V) {
let found = dict.get(k);
if (found !== undefined) {
@ -592,6 +617,18 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return "https://osmcha.org/?filters=" + encodeURIComponent("{" + osmcha_link + "}")
}
/**
* Deepclone an object by serializing and deserializing it
* @param x
* @constructor
*/
static Clone<T>(x: T): T {
if (x === undefined) {
return undefined;
}
return JSON.parse(JSON.stringify(x));
}
private static colorDiff(c0: { r: number, g: number, b: number }, c1: { r: number, g: number, b: number }) {
return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b);
}
@ -618,17 +655,5 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
b: parseInt(hex.substr(5, 2), 16),
}
}
/**
* Deepclone an object by serializing and deserializing it
* @param x
* @constructor
*/
static Clone<T>(x: T): T {
if(x === undefined){
return undefined;
}
return JSON.parse(JSON.stringify(x));
}
}

View file

@ -12,6 +12,7 @@
"description": "Notes from OpenStreetMap",
"icon": "./assets/themes/notes/resolved.svg",
"clustering": false,
"enableDownload": true,
"layers": [
{
"id": "notes",
@ -25,25 +26,29 @@
"geoJsonZoomLevel": 12,
"maxCacheAge": 0
},
"minzoom": 10,
"minzoom": 8,
"title": {
"render": {
"en": "Note"
},
"mappings": [{
"mappings": [
{
"if": "closed_at~*",
"then": {
"en": "Closed note"
}
}]
}
]
},
"calculatedTags": [
"_first_comment:=feat.get('comments')[0].text",
"_conversation=feat.get('comments').map(c => {if(c.user_url == undefined) {return 'anonymous user, '+c.date;} return c.html+'<div class=\"subtle flex justify-end border-b border-gray-500\"><a href=\"'+c.user_url+'\" target=\"_blank\">'+c.user+'</a> &nbsp;'+c.date+'</div>'}).join('')"
"_first_comment:=feat.get('comments')[0].text.toLowerCase()",
"_conversation=feat.get('comments').map(c => { let user = 'anonymous user'; if(c.user_url !== undefined){user = '<a href=\"'+c.user_url+'\" target=\"_blank\">'+c.user+'</a>'}; return c.html +'<div class=\"subtle flex justify-end border-t border-gray-500\">' + user + '&nbsp;'+c.date+'</div>' }).join('')"
],
"titleIcons": [{
"titleIcons": [
{
"render": "<a href='https://openstreetmap.org/note/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'></a>"
}],
}
],
"tagRenderings": [
{
"id": "conversation",
@ -76,18 +81,27 @@
}
]
},
"iconSize": "40,40,bottom"
}
],
"filter": [{
"id": "bookcases",
"filter": [
{
"id": "search",
"options": [
{
"osmTags": "_first_comment~.*bookcase.*",
"question": "Should mention 'bookcase' in the first comment"
}]
}]
"osmTags": "_first_comment~.*{search}.*",
"fields": [
{
"name": "search"
}
],
"question": {
"en": "Should mention {search} in the first comment"
}
}
]
}
]
}
]
}

View file

@ -1044,6 +1044,10 @@ video {
max-height: 1rem;
}
.max-h-8 {
max-height: 2rem;
}
.w-full {
width: 100%;
}