diff --git a/Customizations/JSON/FilterConfig.ts b/Customizations/JSON/FilterConfig.ts index 4993baf4e..946aef43c 100644 --- a/Customizations/JSON/FilterConfig.ts +++ b/Customizations/JSON/FilterConfig.ts @@ -17,7 +17,7 @@ export default class FilterConfig { context + ".options-[" + i + "].question" ); const osmTags = FromJSON.Tag( - option.osmTags, + option.osmTags ?? {and:[]}, `${context}.options-[${i}].osmTags` ); diff --git a/Customizations/JSON/FilterConfigJson.ts b/Customizations/JSON/FilterConfigJson.ts index 082fd7fe0..f1a802571 100644 --- a/Customizations/JSON/FilterConfigJson.ts +++ b/Customizations/JSON/FilterConfigJson.ts @@ -7,5 +7,5 @@ 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 }[]; } diff --git a/UI/BigComponents/FilterView.ts b/UI/BigComponents/FilterView.ts index 58dda3140..a89533bc8 100644 --- a/UI/BigComponents/FilterView.ts +++ b/UI/BigComponents/FilterView.ts @@ -1,123 +1,134 @@ -import { Utils } from "./../../Utils"; -import { FixedInputElement } from "./../Input/FixedInputElement"; -import { RadioButton } from "./../Input/RadioButton"; -import { VariableUiElement } from "../Base/VariableUIElement"; +import {Utils} from "../../Utils"; +import {FixedInputElement} from "../Input/FixedInputElement"; +import {RadioButton} from "../Input/RadioButton"; +import {VariableUiElement} from "../Base/VariableUIElement"; import Toggle from "../Input/Toggle"; import Combine from "../Base/Combine"; import Translations from "../i18n/Translations"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; -import { Translation } from "../i18n/Translation"; +import {Translation} from "../i18n/Translation"; import Svg from "../../Svg"; import FilterConfig from "../../Customizations/JSON/FilterConfig"; -import CheckBoxes from "../Input/Checkboxes"; -import { InputElement } from "../Input/InputElement"; -import { TagsFilter } from "../../Logic/Tags/TagsFilter"; -import InputElementMap from "../Input/InputElementMap"; -import { And } from "../../Logic/Tags/And"; -import { UIEventSource } from "../../Logic/UIEventSource"; +import {TagsFilter} from "../../Logic/Tags/TagsFilter"; +import {And} from "../../Logic/Tags/And"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import BaseUIElement from "../BaseUIElement"; /** * Shows the filter */ export default class FilterView extends VariableUiElement { - constructor(filteredLayer) { - super( - filteredLayer.map((filteredLayers) => - filteredLayers.map(FilterView.createOneFilteredLayerElement) - ) - ); - } - - static createOneFilteredLayerElement(filteredLayer) { - const layer: LayerConfig = filteredLayer.layerDef; - const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem"; - - const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle); - const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle( - iconStyle - ); - - if (filteredLayer.layerDef.name === undefined) { - return; + constructor(filteredLayer) { + super( + filteredLayer.map((filteredLayers) => + filteredLayers.map(FilterView.createOneFilteredLayerElement) + ) + ); } - const style = - "display:flex;align-items:center;color:#007759;padding:0.5rem 0;"; + static createOneFilteredLayerElement(filteredLayer) { + const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem"; - const name: Translation = Translations.WT( - filteredLayer.layerDef.name - )?.Clone(); + const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle); + const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle( + iconStyle + ); - const styledNameChecked = name - .Clone() - .SetStyle("font-size:large;padding-left:1.25rem"); + if (filteredLayer.layerDef.name === undefined) { + return; + } - const styledNameUnChecked = name - .Clone() - .SetStyle("font-size:large;padding-left:1.25rem"); - const layerChecked = new Combine([icon, styledNameChecked]) - .SetStyle(style) - .onClick(() => filteredLayer.isDisplayed.setData(false)); + const name: Translation = Translations.WT( + filteredLayer.layerDef.name + )?.Clone(); - const layerNotChecked = new Combine([iconUnselected, styledNameUnChecked]) - .SetStyle(style) - .onClick(() => filteredLayer.isDisplayed.setData(true)); + const styledNameChecked = name + .Clone() + .SetStyle("font-size:large;padding-left:1.25rem"); - let listFilterElements: InputElement[] = layer.filters.map( - FilterView.createFilter - ); + const styledNameUnChecked = name + .Clone() + .SetStyle("font-size:large;padding-left:1.25rem"); - const update = () => { - let listTagsFilters = Utils.NoNull( - listFilterElements.map((input) => input.GetValue().data) - ); - filteredLayer.appliedFilters.setData(new And(listTagsFilters)); - }; + const style = + "display:flex;align-items:center;color:#007759;padding:0.5rem 0;"; + const layerChecked = new Combine([icon, styledNameChecked]) + .SetStyle(style) + .onClick(() => filteredLayer.isDisplayed.setData(false)); - listFilterElements.forEach((inputElement) => - inputElement.GetValue().addCallback((_) => update()) - ); + const layerNotChecked = new Combine([iconUnselected, styledNameUnChecked]) + .SetStyle(style) + .onClick(() => filteredLayer.isDisplayed.setData(true)); - return new Toggle( - new Combine([layerChecked, ...listFilterElements]), - layerNotChecked, - filteredLayer.isDisplayed - ).SetStyle("margin:0.3em;"); - } + + const filterPanel: BaseUIElement = FilterView.createFilterPanel(filteredLayer) + + return new Toggle( + new Combine([layerChecked, filterPanel]), + layerNotChecked, + filteredLayer.isDisplayed + ); + } + + static createFilterPanel(flayer: { + layerDef: LayerConfig, + appliedFilters: UIEventSource + }): BaseUIElement{ + const layer = flayer.layerDef + if(layer.filters.length === 0){ + return undefined; + } + + let listFilterElements: [BaseUIElement, UIEventSource][] = layer.filters.map( + FilterView.createFilter + ); - static createFilter(filterConfig: FilterConfig): InputElement { - if (filterConfig.options.length === 1) { - let option = filterConfig.options[0]; - let checkboxes = new CheckBoxes( - [option.question.Clone()], - new UIEventSource([]), - "background-color: #F1F1F1;padding:0.25rem 0.5rem;", - "border:none;padding-left:3rem;color:#007759;display:flex;margin:0;justify-content:center;align-items:center;flex-direction:row;flex-wrap:nowrap;", - "margin:0;padding:0;", - "margin:0;padding:0.25rem 0 0 0.25rem;" - ); + const update = () => { + let listTagsFilters = Utils.NoNull( + listFilterElements.map((input) => input[1].data) + ); + flayer.appliedFilters.setData(new And(listTagsFilters)); + }; + + listFilterElements.forEach((inputElement) => + inputElement[1].addCallback((_) => update()) + ); + + return new Combine(listFilterElements.map(input => input[0].SetClass("mt-3"))) + .SetClass("flex flex-col ml-8 bg-gray-300 rounded-xl p-2") - return new InputElementMap( - checkboxes, - (t0, t1) => t0 === t1, - (numbers) => (numbers.length > 0 ? option.osmTags : undefined), - (tagsFilter) => (tagsFilter == undefined ? [] : [0]) - ); } - let options = filterConfig.options; + static createFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource] { + if (filterConfig.options.length === 1) { + let option = filterConfig.options[0]; - return new RadioButton( - options.map( - (option) => - new FixedInputElement(option.question.Clone(), option.osmTags) - ), - true, - "background-color: #F1F1F1;padding:0.25rem 0.5rem;", - "border:none;padding-left:3rem;color:#007759;display:flex;margin:0;justify-content:center;align-items:center;flex-direction:row;flex-wrap:nowrap;", - "margin:0;padding:0;" - ); - } + const icon = Svg.checkbox_filled_svg().SetClass("block mr-2"); + const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2"); + + const toggle = new Toggle( + new Combine([icon, option.question.Clone()]).SetClass("flex"), + new Combine([iconUnselected, option.question.Clone()]).SetClass("flex") + ) + .ToggleOnClick() + .SetClass("block m-1") + + return [toggle, toggle.isEnabled.map(enabled => enabled ? option.osmTags : undefined)] + } + + let options = filterConfig.options; + + const radio = new RadioButton( + options.map( + (option) => + new FixedInputElement(option.question.Clone(), option.osmTags) + ), + { + dontStyle: true + } + ); + return [radio, radio.GetValue()] + } } diff --git a/UI/Input/Checkboxes.ts b/UI/Input/Checkboxes.ts index 251b702a9..f1fc6da94 100644 --- a/UI/Input/Checkboxes.ts +++ b/UI/Input/Checkboxes.ts @@ -11,26 +11,15 @@ export default class CheckBoxes extends InputElement { IsSelected: UIEventSource = new UIEventSource(false); private readonly value: UIEventSource; private readonly _elements: BaseUIElement[]; - private styleWrapperOverride = ""; - private styleInputOverride = ""; - private styleLabelOverride = ""; constructor( elements: BaseUIElement[], - value = new UIEventSource([]), - styleFormOverride = "", - styleWrapperOverride = "", - styleInputOverride = "", - styleLabelOverride = "" + value = new UIEventSource([]) ) { super(); this.value = value; this._elements = Utils.NoNull(elements); this.SetClass("flex flex-col"); - this.SetStyle(styleFormOverride); - this.styleWrapperOverride = styleWrapperOverride; - this.styleInputOverride = styleInputOverride; - this.styleLabelOverride = styleLabelOverride; } IsValid(ts: number[]): boolean { @@ -56,7 +45,6 @@ export default class CheckBoxes extends InputElement { input.type = "checkbox"; input.classList.add("p-1", "cursor-pointer", "m-3", "pl-3", "mr-0"); - input.style.cssText = this.styleInputOverride; const label = document.createElement("label"); label.htmlFor = input.id; @@ -68,7 +56,6 @@ export default class CheckBoxes extends InputElement { "cursor-pointer", "bg-red" ); - label.style.cssText = this.styleLabelOverride; const wrapper = document.createElement("span"); wrapper.classList.add( @@ -79,7 +66,6 @@ export default class CheckBoxes extends InputElement { "border-gray-400", "m-1" ); - wrapper.style.cssText = this.styleWrapperOverride; wrapper.appendChild(input); wrapper.appendChild(label); el.appendChild(wrapper); diff --git a/UI/Input/RadioButton.ts b/UI/Input/RadioButton.ts index c24c76800..eb48ab3e6 100644 --- a/UI/Input/RadioButton.ts +++ b/UI/Input/RadioButton.ts @@ -1,198 +1,204 @@ -import { InputElement } from "./InputElement"; -import { UIEventSource } from "../../Logic/UIEventSource"; -import { Utils } from "../../Utils"; +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {Utils} from "../../Utils"; export class RadioButton extends InputElement { - private static _nextId = 0; - IsSelected: UIEventSource = new UIEventSource(false); - private readonly value: UIEventSource; - private _elements: InputElement[]; - private _selectFirstAsDefault: boolean; - private styleFormOverride = ""; - private styleBlockOverride = ""; - private styleInputOverride = ""; - private styleLabelOverride = ""; + private static _nextId = 0; + IsSelected: UIEventSource = new UIEventSource(false); + private readonly value: UIEventSource; + private _elements: InputElement[]; + private _selectFirstAsDefault: boolean; + private _dontStyle: boolean - constructor( - elements: InputElement[], - selectFirstAsDefault = true, - styleFormOverride = "", - styleBlockOverride = "", - styleInputOverride = "", - styleLabelOverride = "" - ) { - super(); - this._selectFirstAsDefault = selectFirstAsDefault; - this._elements = Utils.NoNull(elements); - this.value = new UIEventSource(undefined); - this.styleFormOverride = styleFormOverride; - this.styleBlockOverride = styleBlockOverride; - this.styleInputOverride = styleInputOverride; - this.styleLabelOverride = styleLabelOverride; - } - protected InnerConstructElement(): HTMLElement { - const elements = this._elements; - const selectFirstAsDefault = this._selectFirstAsDefault; - - const selectedElementIndex: UIEventSource = - new UIEventSource(null); - - const value = UIEventSource.flatten( - selectedElementIndex.map((selectedIndex) => { - if (selectedIndex !== undefined && selectedIndex !== null) { - return elements[selectedIndex].GetValue(); + constructor( + elements: InputElement[], + options?: { + selectFirstAsDefault?: boolean, + dontStyle?: boolean } - }), - elements.map((e) => e?.GetValue()) - ); - value.syncWith(this.value); + ) { + super(); + options = options ?? {} + this._selectFirstAsDefault = options.selectFirstAsDefault ?? true; + this._elements = Utils.NoNull(elements); + this.value = new UIEventSource(undefined); + this._dontStyle = options.dontStyle ?? false + } - if (selectFirstAsDefault) { - value.addCallbackAndRun((selected) => { - if (selected === undefined) { - for (const element of elements) { - const v = element.GetValue().data; - if (v !== undefined) { - value.setData(v); - break; + protected InnerConstructElement(): HTMLElement { + const elements = this._elements; + const selectFirstAsDefault = this._selectFirstAsDefault; + + const selectedElementIndex: UIEventSource = + new UIEventSource(null); + + const value = UIEventSource.flatten( + selectedElementIndex.map((selectedIndex) => { + if (selectedIndex !== undefined && selectedIndex !== null) { + return elements[selectedIndex].GetValue(); + } + }), + elements.map((e) => e?.GetValue()) + ); + value.syncWith(this.value); + + if (selectFirstAsDefault) { + value.addCallbackAndRun((selected) => { + if (selected === undefined) { + for (const element of elements) { + const v = element.GetValue().data; + if (v !== undefined) { + value.setData(v); + break; + } + } + } + }); + } + + for (let i = 0; i < elements.length; i++) { + // If an element is clicked, the radio button corresponding with it should be selected as well + elements[i]?.onClick(() => { + selectedElementIndex.setData(i); + }); + elements[i].IsSelected.addCallback((isSelected) => { + if (isSelected) { + selectedElementIndex.setData(i); + } + }); + elements[i].GetValue().addCallback(() => { + selectedElementIndex.setData(i); + }); + } + + const groupId = "radiogroup" + RadioButton._nextId; + RadioButton._nextId++; + + const form = document.createElement("form"); + + const inputs = []; + const wrappers: HTMLElement[] = []; + + for (let i1 = 0; i1 < elements.length; i1++) { + let element = elements[i1]; + const labelHtml = element.ConstructElement(); + if (labelHtml === undefined) { + continue; } + + const input = document.createElement("input"); + input.id = "radio" + groupId + "-" + i1; + input.name = groupId; + input.type = "radio"; + input.classList.add( + "cursor-pointer", + "p-1", + "mr-2" + ); + + + if (!this._dontStyle) { + input.classList.add( + "p-1", + "ml-2", + "pl-2", + "pr-0", + "m-3", + "mr-0" + ); + } + input.onchange = () => { + if (input.checked) { + selectedElementIndex.setData(i1); + } + }; + + inputs.push(input); + + const label = document.createElement("label"); + label.appendChild(labelHtml); + label.htmlFor = input.id; + label.classList.add("block", "w-full", "cursor-pointer", "bg-red"); + + if (!this._dontStyle) { + labelHtml.classList.add("p-2") + } + + const block = document.createElement("div"); + block.appendChild(input); + block.appendChild(label); + block.classList.add( + "flex", + "w-full", + ); + if (!this._dontStyle) { + block.classList.add( + "m-1", + "border", + "rounded-3xl", + "border-gray-400", + ) + } + wrappers.push(block); + + form.appendChild(block); + } + + value.addCallbackAndRun((selected) => { + let somethingChecked = false; + for (let i = 0; i < inputs.length; i++) { + let input = inputs[i]; + input.checked = !somethingChecked && elements[i].IsValid(selected); + somethingChecked = somethingChecked || input.checked; + + if (input.checked) { + wrappers[i].classList.remove("border-gray-400"); + wrappers[i].classList.add("border-black"); + } else { + wrappers[i].classList.add("border-gray-400"); + wrappers[i].classList.remove("border-black"); + } + } + }); + + this.SetClass("flex flex-col"); + + return form; + } + + IsValid(t: T): boolean { + for (const inputElement of this._elements) { + if (inputElement.IsValid(t)) { + return true; + } + } + return false; + } + + GetValue(): UIEventSource { + return this.value; + } + + /* + public ShowValue(t: T): boolean { + if (t === undefined) { + return false; } - } - }); - } - - for (let i = 0; i < elements.length; i++) { - // If an element is clicked, the radio button corresponding with it should be selected as well - elements[i]?.onClick(() => { - selectedElementIndex.setData(i); - }); - elements[i].IsSelected.addCallback((isSelected) => { - if (isSelected) { - selectedElementIndex.setData(i); - } - }); - elements[i].GetValue().addCallback(() => { - selectedElementIndex.setData(i); - }); - } - - const groupId = "radiogroup" + RadioButton._nextId; - RadioButton._nextId++; - - const form = document.createElement("form"); - form.style.cssText = this.styleFormOverride; - - const inputs = []; - const wrappers: HTMLElement[] = []; - - for (let i1 = 0; i1 < elements.length; i1++) { - let element = elements[i1]; - const labelHtml = element.ConstructElement(); - if (labelHtml === undefined) { - continue; - } - - const input = document.createElement("input"); - input.id = "radio" + groupId + "-" + i1; - input.name = groupId; - input.type = "radio"; - input.classList.add( - "p-1", - "cursor-pointer", - "ml-2", - "pl-2", - "pr-0", - "m-3", - "mr-0" - ); - input.style.cssText = this.styleInputOverride; - - input.onchange = () => { - if (input.checked) { - selectedElementIndex.setData(i1); - } - }; - - inputs.push(input); - - const label = document.createElement("label"); - label.appendChild(labelHtml); - label.htmlFor = input.id; - label.classList.add("block", "w-full", "p-2", "cursor-pointer", "bg-red"); - label.style.cssText = this.styleLabelOverride; - - const block = document.createElement("div"); - block.appendChild(input); - block.appendChild(label); - block.classList.add( - "flex", - "w-full", - "border", - "rounded-3xl", - "border-gray-400", - "m-1" - ); - block.style.cssText = this.styleBlockOverride; - wrappers.push(block); - - form.appendChild(block); - } - - value.addCallbackAndRun((selected) => { - let somethingChecked = false; - for (let i = 0; i < inputs.length; i++) { - let input = inputs[i]; - input.checked = !somethingChecked && elements[i].IsValid(selected); - somethingChecked = somethingChecked || input.checked; - - if (input.checked) { - wrappers[i].classList.remove("border-gray-400"); - wrappers[i].classList.add("border-black"); - } else { - wrappers[i].classList.add("border-gray-400"); - wrappers[i].classList.remove("border-black"); - } - } - }); - - this.SetClass("flex flex-col"); - - return form; - } - - IsValid(t: T): boolean { - for (const inputElement of this._elements) { - if (inputElement.IsValid(t)) { - return true; - } - } - return false; - } - - GetValue(): UIEventSource { - return this.value; - } - - /* - public ShowValue(t: T): boolean { - if (t === undefined) { - return false; - } - if (!this.IsValid(t)) { - return false; - } - // We check that what is selected matches the previous rendering - for (let i = 0; i < this._elements.length; i++) { - const e = this._elements[i]; - if (e.IsValid(t)) { - this._selectedElementIndex.setData(i); - e.GetValue().setData(t); - const radio = document.getElementById(this.IdFor(i)); - // @ts-ignore - radio?.checked = true; - return; - } - - } - }*/ + if (!this.IsValid(t)) { + return false; + } + // We check that what is selected matches the previous rendering + for (let i = 0; i < this._elements.length; i++) { + const e = this._elements[i]; + if (e.IsValid(t)) { + this._selectedElementIndex.setData(i); + e.GetValue().setData(t); + const radio = document.getElementById(this.IdFor(i)); + // @ts-ignore + radio?.checked = true; + return; + } + + } + }*/ } diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index c72375959..041a74738 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -164,7 +164,7 @@ export default class TagRenderingQuestion extends Combine { if (configuration.multiAnswer) { return TagRenderingQuestion.GenerateMultiAnswer(configuration, inputEls, ff, configuration.mappings.map(mp => mp.ifnot)) } else { - return new RadioButton(inputEls, false) + return new RadioButton(inputEls, {selectFirstAsDefault: false}) } } diff --git a/assets/layers/nature_reserve/nature_reserve.json b/assets/layers/nature_reserve/nature_reserve.json index 4f670261c..da2380165 100644 --- a/assets/layers/nature_reserve/nature_reserve.json +++ b/assets/layers/nature_reserve/nature_reserve.json @@ -419,5 +419,38 @@ "nl": "Voeg een ontbrekend, erkend natuurreservaat toe, bv. een gebied dat beheerd wordt door het ANB of natuurpunt" } } + ], + "filter": [ + { + "options": [ + { + "question": { + "nl": "Vrij te bezoeken" + }, + "osmTags": "access=yes" + } + ] + }, + { + "options": [ + { + "question": { + "nl": "Alle natuurgebieden" + } + },{ + "question": { + "nl": "Honden mogen vrij rondlopen" + }, + "osmTags": "dog=yes" + },{ + "question": { + "nl": "Honden welkom aan de leiband" + }, + "osmTags": { + "or": ["dog=yes","dog=leashed"] + } + } + ] + } ] } \ No newline at end of file