First working version of the notes-layer, add filtering
This commit is contained in:
parent
ebb510da04
commit
91d2272861
19 changed files with 282 additions and 109 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
}[]
|
||||
}[];
|
||||
}
|
|
@ -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}`
|
||||
}
|
||||
|
|
|
@ -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++) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
63
Utils.ts
63
Utils.ts
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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> '+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 + ' '+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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1044,6 +1044,10 @@ video {
|
|||
max-height: 1rem;
|
||||
}
|
||||
|
||||
.max-h-8 {
|
||||
max-height: 2rem;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue