import {Translation} from "../../UI/i18n/Translation"; 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 {AndOrTagConfigJson} from "./Json/TagConfigJson"; import {UIEventSource} from "../../Logic/UIEventSource"; import {FilterState} from "../FilteredLayer"; import {QueryParameters} from "../../Logic/Web/QueryParameters"; import {Utils} from "../../Utils"; export default class FilterConfig { public readonly id: string public readonly options: { question: Translation; osmTags: TagsFilter | undefined; originalTagsSpec: string | AndOrTagConfigJson fields: { name: string, type: string }[] }[]; constructor(json: FilterConfigJson, context: string) { if (json.options === undefined) { throw `A filter without options was given at ${context}` } if (json.id === undefined) { throw `A filter without id was found at ${context}` } if (json.id.match(/^[a-zA-Z0-9_-]*$/) === null) { throw `A filter with invalid id was found at ${context}. Ids should only contain letters, numbers or - _` } if (json.options.map === undefined) { throw `A filter was given where the options aren't a list at ${context}` } this.id = json.id; this.options = json.options.map((option, i) => { const ctx = `${context}.options[${i}]`; const question = Translations.T( option.question, `${ctx}.question` ); let osmTags = undefined; if ((option.fields?.length ?? 0) == 0 && option.osmTags !== undefined) { osmTags = TagUtils.Tag( option.osmTags, `${ctx}.osmTags` ); } if (question === undefined) { throw `Invalid filter: no question given at ${ctx}` } 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 } }) 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 !== undefined) { throw "Error in " + context + "." + this.id + ": the first option of a multi-filter should always be the 'reset' option and not have any filters" } } public initState(): UIEventSource { function reset(state: FilterState): string { if (state === undefined) { return "" } return "" + state.state } const defaultValue = this.options.length > 1 ? "0" : "" const qp = QueryParameters.GetQueryParameter("filter-" + this.id, defaultValue, "State of filter " + this.id) if (this.options.length > 1) { // This is a multi-option filter; state should be a number which selects the correct entry const possibleStates: FilterState [] = this.options.map((opt, i) => ({ currentFilter: opt.osmTags, state: i })) // We map the query parameter for this case return qp.map(str => { const parsed = Number(str) if (isNaN(parsed)) { // Nope, not a correct number! return undefined } return possibleStates[parsed] }, [], reset) } const option = this.options[0] if (option.fields.length > 0) { return qp.map(str => { // There are variables in play! // str should encode a json-hash try { const props = JSON.parse(str) const origTags = option.originalTagsSpec const rewrittenTags = Utils.WalkJson(origTags, v => { if (typeof v !== "string") { return v } for (const key in props) { v = (v).replace("{" + key + "}", props[key]) } return v } ) const parsed = TagUtils.Tag(rewrittenTags) return { currentFilter: parsed, state: str } } catch (e) { return undefined } }, [], reset) } // The last case is pretty boring: it is checked or it isn't const filterState: FilterState = { currentFilter: option.osmTags, state: "true" } return qp.map( str => { // Only a single option exists here if (str === "true") { return filterState } return undefined }, [], reset ) } }