From e9160504a6235044a75946568c6092bcd1f9238b Mon Sep 17 00:00:00 2001 From: Ward Date: Mon, 26 Jul 2021 12:26:41 +0200 Subject: [PATCH] start creating extra filter --- InitUiElements.ts | 1057 +++++++++-------- Logic/FeatureSource/FilteringFeatureSource.ts | 262 ++-- State.ts | 3 + UI/Base/VariableUIElement.ts | 75 +- UI/BigComponents/FilterView.ts | 154 ++- UI/BigComponents/LayerControlPanel.ts | 77 +- .../public_bookcase/public_bookcase.json | 44 +- 7 files changed, 872 insertions(+), 800 deletions(-) diff --git a/InitUiElements.ts b/InitUiElements.ts index dae9ac347..518e23d66 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -1,19 +1,19 @@ -import {CenterFlexedElement} from "./UI/Base/CenterFlexedElement"; -import {FixedUiElement} from "./UI/Base/FixedUiElement"; +import { CenterFlexedElement } from "./UI/Base/CenterFlexedElement"; +import { FixedUiElement } from "./UI/Base/FixedUiElement"; import Toggle from "./UI/Input/Toggle"; -import {Basemap} from "./UI/BigComponents/Basemap"; +import { Basemap } from "./UI/BigComponents/Basemap"; import State from "./State"; import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource"; -import {UIEventSource} from "./Logic/UIEventSource"; -import {QueryParameters} from "./Logic/Web/QueryParameters"; +import { UIEventSource } from "./Logic/UIEventSource"; +import { QueryParameters } from "./Logic/Web/QueryParameters"; import StrayClickHandler from "./Logic/Actors/StrayClickHandler"; import SimpleAddUI from "./UI/BigComponents/SimpleAddUI"; import CenterMessageBox from "./UI/CenterMessageBox"; import UserBadge from "./UI/BigComponents/UserBadge"; import SearchAndGo from "./UI/BigComponents/SearchAndGo"; import GeoLocationHandler from "./Logic/Actors/GeoLocationHandler"; -import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; -import {Utils} from "./Utils"; +import { LocalStorageSource } from "./Logic/Web/LocalStorageSource"; +import { Utils } from "./Utils"; import Svg from "./Svg"; import Link from "./UI/Base/Link"; import * as personal from "./assets/themes/personalLayout/personalLayout.json"; @@ -34,570 +34,571 @@ import MapControlButton from "./UI/MapControlButton"; import Combine from "./UI/Base/Combine"; import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler"; import LZString from "lz-string"; -import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson"; +import { LayoutConfigJson } from "./Customizations/JSON/LayoutConfigJson"; import AttributionPanel from "./UI/BigComponents/AttributionPanel"; import ContributorCount from "./Logic/ContributorCount"; import FeatureSource from "./Logic/FeatureSource/FeatureSource"; import AllKnownLayers from "./Customizations/AllKnownLayers"; import LayerConfig from "./Customizations/JSON/LayerConfig"; import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; -import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter"; +import { SimpleMapScreenshoter } from "leaflet-simple-map-screenshoter"; import jsPDF from "jspdf"; import FilterView from "./UI/BigComponents/FilterView"; +import { TagsFilter } from "./Logic/Tags/TagsFilter"; export class InitUiElements { - static InitAll( - layoutToUse: LayoutConfig, - layoutFromBase64: string, - testing: UIEventSource, - layoutName: string, - layoutDefinition: string = "" + static InitAll( + layoutToUse: LayoutConfig, + layoutFromBase64: string, + testing: UIEventSource, + layoutName: string, + layoutDefinition: string = "" + ) { + if (layoutToUse === undefined) { + console.log("Incorrect layout"); + new FixedUiElement( + `Error: incorrect layout ${layoutName}
Go back` + ) + .AttachTo("centermessage") + .onClick(() => {}); + throw "Incorrect layout"; + } + + console.log( + "Using layout: ", + layoutToUse.id, + "LayoutFromBase64 is ", + layoutFromBase64 + ); + + State.state = new State(layoutToUse); + + // This 'leaks' the global state via the window object, useful for debugging + // @ts-ignore + window.mapcomplete_state = State.state; + + if (layoutToUse.hideFromOverview) { + State.state.osmConnection + .GetPreference("hidden-theme-" + layoutToUse.id + "-enabled") + .setData("true"); + } + + if (layoutFromBase64 !== "false") { + State.state.layoutDefinition = layoutDefinition; + console.log( + "Layout definition:", + Utils.EllipsesAfter(State.state.layoutDefinition, 100) + ); + if (testing.data !== "true") { + State.state.osmConnection.OnLoggedIn(() => { + State.state.osmConnection + .GetLongPreference("installed-theme-" + layoutToUse.id) + .setData(State.state.layoutDefinition); + }); + } else { + console.warn( + "NOT saving custom layout to OSM as we are tesing -> probably in an iFrame" + ); + } + } + + function updateFavs() { + // This is purely for the personal theme to load the layers there + const favs = State.state.favouriteLayers.data ?? []; + + const neededLayers = new Set(); + + console.log("Favourites are: ", favs); + layoutToUse.layers.splice(0, layoutToUse.layers.length); + let somethingChanged = false; + for (const fav of favs) { + if (AllKnownLayers.sharedLayers.has(fav)) { + const layer = AllKnownLayers.sharedLayers.get(fav); + if (!neededLayers.has(layer)) { + neededLayers.add(layer); + somethingChanged = true; + } + } + + for (const layouts of State.state.installedThemes.data) { + for (const layer of layouts.layout.layers) { + if (typeof layer === "string") { + continue; + } + if (layer.id === fav) { + if (!neededLayers.has(layer)) { + neededLayers.add(layer); + somethingChanged = true; + } + } + } + } + } + if (somethingChanged) { + console.log("layoutToUse.layers:", layoutToUse.layers); + State.state.layoutToUse.data.layers = Array.from(neededLayers); + State.state.layoutToUse.ping(); + State.state.layerUpdater?.ForceRefresh(); + } + } + + if (layoutToUse.customCss !== undefined) { + Utils.LoadCustomCss(layoutToUse.customCss); + } + + InitUiElements.InitBaseMap(); + + InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => { + new UserBadge().AttachTo("userbadge"); + }); + + InitUiElements.OnlyIf(State.state.featureSwitchSearch, () => { + new SearchAndGo().AttachTo("searchbox"); + }); + + InitUiElements.OnlyIf(State.state.featureSwitchWelcomeMessage, () => { + InitUiElements.InitWelcomeMessage(); + }); + + if ( + (window != window.top && !State.state.featureSwitchWelcomeMessage.data) || + State.state.featureSwitchIframe.data ) { - if (layoutToUse === undefined) { - console.log("Incorrect layout"); - new FixedUiElement( - `Error: incorrect layout ${layoutName}
Go back` - ) - .AttachTo("centermessage") - .onClick(() => { - }); - throw "Incorrect layout"; - } - - console.log( - "Using layout: ", - layoutToUse.id, - "LayoutFromBase64 is ", - layoutFromBase64 - ); - - State.state = new State(layoutToUse); - - // This 'leaks' the global state via the window object, useful for debugging - // @ts-ignore - window.mapcomplete_state = State.state; - - if (layoutToUse.hideFromOverview) { - State.state.osmConnection - .GetPreference("hidden-theme-" + layoutToUse.id + "-enabled") - .setData("true"); - } - - if (layoutFromBase64 !== "false") { - State.state.layoutDefinition = layoutDefinition; - console.log( - "Layout definition:", - Utils.EllipsesAfter(State.state.layoutDefinition, 100) - ); - if (testing.data !== "true") { - State.state.osmConnection.OnLoggedIn(() => { - State.state.osmConnection - .GetLongPreference("installed-theme-" + layoutToUse.id) - .setData(State.state.layoutDefinition); - }); - } else { - console.warn( - "NOT saving custom layout to OSM as we are tesing -> probably in an iFrame" - ); - } - } - - function updateFavs() { - // This is purely for the personal theme to load the layers there - const favs = State.state.favouriteLayers.data ?? []; - - const neededLayers = new Set(); - - console.log("Favourites are: ", favs); - layoutToUse.layers.splice(0, layoutToUse.layers.length); - let somethingChanged = false; - for (const fav of favs) { - if (AllKnownLayers.sharedLayers.has(fav)) { - const layer = AllKnownLayers.sharedLayers.get(fav); - if (!neededLayers.has(layer)) { - neededLayers.add(layer); - somethingChanged = true; - } - } - - for (const layouts of State.state.installedThemes.data) { - for (const layer of layouts.layout.layers) { - if (typeof layer === "string") { - continue; - } - if (layer.id === fav) { - if (!neededLayers.has(layer)) { - neededLayers.add(layer); - somethingChanged = true; - } - } - } - } - } - if (somethingChanged) { - console.log("layoutToUse.layers:", layoutToUse.layers); - State.state.layoutToUse.data.layers = Array.from(neededLayers); - State.state.layoutToUse.ping(); - State.state.layerUpdater?.ForceRefresh(); - } - } - - if (layoutToUse.customCss !== undefined) { - Utils.LoadCustomCss(layoutToUse.customCss); - } - - InitUiElements.InitBaseMap(); - - InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => { - new UserBadge().AttachTo("userbadge"); - }); - - InitUiElements.OnlyIf(State.state.featureSwitchSearch, () => { - new SearchAndGo().AttachTo("searchbox"); - }); - - InitUiElements.OnlyIf(State.state.featureSwitchWelcomeMessage, () => { - InitUiElements.InitWelcomeMessage(); - }); - - if ( - (window != window.top && !State.state.featureSwitchWelcomeMessage.data) || - State.state.featureSwitchIframe.data - ) { - const currentLocation = State.state.locationControl; - const url = `${window.location.origin}${window.location.pathname}?z=${ - currentLocation.data.zoom ?? 0 - }&lat=${currentLocation.data.lat ?? 0}&lon=${ - currentLocation.data.lon ?? 0 - }`; - new MapControlButton( - new Link(Svg.pop_out_img, url, true).SetClass( - "block w-full h-full p-1.5" - ) - ).AttachTo("messagesbox"); - } - - State.state.osmConnection.userDetails - .map((userDetails: UserDetails) => userDetails?.home) - .addCallbackAndRunD((home) => { - const color = getComputedStyle(document.body).getPropertyValue( - "--subtle-detail-color" - ); - const icon = L.icon({ - iconUrl: Img.AsData(Svg.home_white_bg.replace(/#ffffff/g, color)), - iconSize: [30, 30], - iconAnchor: [15, 15], - }); - const marker = L.marker([home.lat, home.lon], {icon: icon}); - marker.addTo(State.state.leafletMap.data); - }); - - const geolocationButton = new Toggle( - new MapControlButton( - new GeoLocationHandler( - State.state.currentGPSLocation, - State.state.leafletMap, - State.state.layoutToUse - ) - ), - undefined, - State.state.featureSwitchGeolocation - ); - - const plus = new MapControlButton( - new CenterFlexedElement( - Img.AsImageElement(Svg.plus_zoom, "", "width:1.25rem;height:1.25rem") - ) - ).onClick(() => { - State.state.locationControl.data.zoom++; - State.state.locationControl.ping(); - }); - - const min = new MapControlButton( - new CenterFlexedElement( - Img.AsImageElement(Svg.min_zoom, "", "width:1.25rem;height:1.25rem") - ) - ).onClick(() => { - State.state.locationControl.data.zoom--; - State.state.locationControl.ping(); - }); - - - // To download pdf of leaflet you need to turn it into and image first - // Then export that image as a pdf - // leaflet-simple-map-screenshoter: to make image - // jsPDF: to make pdf - - const screenshot = new MapControlButton( - new CenterFlexedElement( - Img.AsImageElement(Svg.bug, "", "width:1.25rem;height:1.25rem") - ) - ).onClick(() => { - const screenshotter = new SimpleMapScreenshoter(); - console.log("Debug - Screenshot"); - screenshotter.addTo(State.state.leafletMap.data); - let doc = new jsPDF(); - screenshotter.takeScreen('image').then(image => { - // TO DO: scale image on pdf to its original size - doc.addImage(image, 'PNG', 0, 0, screen.width / 10, screen.height / 10); - doc.setDisplayMode('fullheight'); - doc.save("Screenshot"); - }); - //screenshotter.remove(); - // The line below is for downloading the png - //screenshotter.takeScreen().then(blob => Utils.offerContentsAsDownloadableFile(blob, "Screenshot.png")); - }); - - new Combine( - [plus, min, geolocationButton, screenshot].map((el) => el.SetClass("m-0.5 md:m-1")) + const currentLocation = State.state.locationControl; + const url = `${window.location.origin}${window.location.pathname}?z=${ + currentLocation.data.zoom ?? 0 + }&lat=${currentLocation.data.lat ?? 0}&lon=${ + currentLocation.data.lon ?? 0 + }`; + new MapControlButton( + new Link(Svg.pop_out_img, url, true).SetClass( + "block w-full h-full p-1.5" ) - .SetClass("flex flex-col") - .AttachTo("bottom-right"); - - if (layoutToUse.id === personal.id) { - updateFavs(); - } - InitUiElements.setupAllLayerElements(); - - if (layoutToUse.id === personal.id) { - State.state.favouriteLayers.addCallback(updateFavs); - State.state.installedThemes.addCallback(updateFavs); - } else { - State.state.locationControl.ping(); - } - - // Reset the loading message once things are loaded - new CenterMessageBox().AttachTo("centermessage"); - document - .getElementById("centermessage") - .classList.add("pointer-events-none"); + ).AttachTo("messagesbox"); } - static LoadLayoutFromHash( - userLayoutParam: UIEventSource - ): [LayoutConfig, string] { - try { - let hash = location.hash.substr(1); - const layoutFromBase64 = userLayoutParam.data; - // layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter - - const dedicatedHashFromLocalStorage = LocalStorageSource.Get( - "user-layout-" + layoutFromBase64.replace(" ", "_") - ); - if (dedicatedHashFromLocalStorage.data?.length < 10) { - dedicatedHashFromLocalStorage.setData(undefined); - } - - const hashFromLocalStorage = LocalStorageSource.Get( - "last-loaded-user-layout" - ); - if (hash.length < 10) { - hash = dedicatedHashFromLocalStorage.data ?? hashFromLocalStorage.data; - } else { - console.log("Saving hash to local storage"); - hashFromLocalStorage.setData(hash); - dedicatedHashFromLocalStorage.setData(hash); - } - - let json: {}; - try { - json = JSON.parse(atob(hash)); - } catch (e) { - // We try to decode with lz-string - json = JSON.parse( - Utils.UnMinify(LZString.decompressFromBase64(hash)) - ) as LayoutConfigJson; - } - - // @ts-ignore - const layoutToUse = new LayoutConfig(json, false); - userLayoutParam.setData(layoutToUse.id); - return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))]; - } catch (e) { - new FixedUiElement( - "Error: could not parse the custom layout:
" + e - ).AttachTo("centermessage"); - throw e; - } - } - - private static OnlyIf( - featureSwitch: UIEventSource, - callback: () => void - ) { - featureSwitch.addCallbackAndRun(() => { - if (featureSwitch.data) { - callback(); - } - }); - } - - private static InitWelcomeMessage() { - const isOpened = new UIEventSource(false); - const fullOptions = new FullWelcomePaneWithTabs(isOpened); - - // ?-Button on Desktop, opens panel with close-X. - const help = new MapControlButton(Svg.help_svg()); - help.onClick(() => isOpened.setData(true)); - new Toggle(fullOptions.SetClass("welcomeMessage"), help, isOpened).AttachTo( - "messagesbox" + State.state.osmConnection.userDetails + .map((userDetails: UserDetails) => userDetails?.home) + .addCallbackAndRunD((home) => { + const color = getComputedStyle(document.body).getPropertyValue( + "--subtle-detail-color" ); - const openedTime = new Date().getTime(); - State.state.locationControl.addCallback(() => { - if (new Date().getTime() - openedTime < 15 * 1000) { - // Don't autoclose the first 15 secs when the map is moving - return; - } - isOpened.setData(false); + const icon = L.icon({ + iconUrl: Img.AsData(Svg.home_white_bg.replace(/#ffffff/g, color)), + iconSize: [30, 30], + iconAnchor: [15, 15], }); + const marker = L.marker([home.lat, home.lon], { icon: icon }); + marker.addTo(State.state.leafletMap.data); + }); - State.state.selectedElement.addCallbackAndRunD((_) => { - isOpened.setData(false); - }); - isOpened.setData( - Hash.hash.data === undefined || - Hash.hash.data === "" || - Hash.hash.data == "welcome" - ); - } - - private static InitLayerSelection(featureSource: FeatureSource) { - const copyrightNotice = new ScrollableFullScreen( - () => Translations.t.general.attribution.attributionTitle.Clone(), - () => - new AttributionPanel( - State.state.layoutToUse, - new ContributorCount(featureSource).Contributors - ), - "copyright" - ); - - const copyrightButton = new Toggle( - copyrightNotice, - new MapControlButton(Svg.osm_copyright_svg()), - copyrightNotice.isShown + const geolocationButton = new Toggle( + new MapControlButton( + new GeoLocationHandler( + State.state.currentGPSLocation, + State.state.leafletMap, + State.state.layoutToUse ) - .ToggleOnClick() - .SetClass("p-0.5"); + ), + undefined, + State.state.featureSwitchGeolocation + ); - const layerControlPanel = new LayerControlPanel( - State.state.layerControlIsOpened - ).SetClass("block p-1 rounded-full"); + const plus = new MapControlButton( + new CenterFlexedElement( + Img.AsImageElement(Svg.plus_zoom, "", "width:1.25rem;height:1.25rem") + ) + ).onClick(() => { + State.state.locationControl.data.zoom++; + State.state.locationControl.ping(); + }); - const layerControlButton = new Toggle( - layerControlPanel, - new MapControlButton(Svg.layers_svg()), - State.state.layerControlIsOpened - ).ToggleOnClick(); + const min = new MapControlButton( + new CenterFlexedElement( + Img.AsImageElement(Svg.min_zoom, "", "width:1.25rem;height:1.25rem") + ) + ).onClick(() => { + State.state.locationControl.data.zoom--; + State.state.locationControl.ping(); + }); - const layerControl = new Toggle( - layerControlButton, - "", - State.state.featureSwitchLayers - ); + // To download pdf of leaflet you need to turn it into and image first + // Then export that image as a pdf + // leaflet-simple-map-screenshoter: to make image + // jsPDF: to make pdf - const filterView = new FilterView(State.state.FilterIsOpened).SetClass( - "block p-1 rounded-full" - ); + const screenshot = new MapControlButton( + new CenterFlexedElement( + Img.AsImageElement(Svg.bug, "", "width:1.25rem;height:1.25rem") + ) + ).onClick(() => { + const screenshotter = new SimpleMapScreenshoter(); + console.log("Debug - Screenshot"); + screenshotter.addTo(State.state.leafletMap.data); + let doc = new jsPDF(); + screenshotter.takeScreen("image").then((image) => { + // TO DO: scale image on pdf to its original size + doc.addImage(image, "PNG", 0, 0, screen.width / 10, screen.height / 10); + doc.setDisplayMode("fullheight"); + doc.save("Screenshot"); + }); + //screenshotter.remove(); + // The line below is for downloading the png + //screenshotter.takeScreen().then(blob => Utils.offerContentsAsDownloadableFile(blob, "Screenshot.png")); + }); - const filterMapControlButton = new MapControlButton( - new CenterFlexedElement( - Img.AsImageElement(Svg.filter, "", "width:1.25rem;height:1.25rem") - ) - ); + new Combine( + [plus, min, geolocationButton, screenshot].map((el) => + el.SetClass("m-0.5 md:m-1") + ) + ) + .SetClass("flex flex-col") + .AttachTo("bottom-right"); - const filterButton = new Toggle( - filterView, - filterMapControlButton, - State.state.FilterIsOpened - ).ToggleOnClick(); + if (layoutToUse.id === personal.id) { + updateFavs(); + } + InitUiElements.setupAllLayerElements(); - const filterControl = new Toggle( - filterButton, - "", - State.state.featureSwitchFilter - ); - - new Combine([copyrightButton, layerControl, filterControl]).AttachTo( - "bottom-left" - ); - - State.state.locationControl.addCallback(() => { - // Close the layer selection when the map is moved - layerControlButton.isEnabled.setData(false); - copyrightButton.isEnabled.setData(false); - }); - - State.state.selectedElement.addCallbackAndRunD((_) => { - layerControlButton.isEnabled.setData(false); - copyrightButton.isEnabled.setData(false); - }); + if (layoutToUse.id === personal.id) { + State.state.favouriteLayers.addCallback(updateFavs); + State.state.installedThemes.addCallback(updateFavs); + } else { + State.state.locationControl.ping(); } - private static InitBaseMap() { - State.state.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(State.state.locationControl); - State.state.backgroundLayer = State.state.backgroundLayerId.map( - (selectedId: string) => { - if (selectedId === undefined) { - return AvailableBaseLayers.osmCarto; - } + // Reset the loading message once things are loaded + new CenterMessageBox().AttachTo("centermessage"); + document + .getElementById("centermessage") + .classList.add("pointer-events-none"); + } - const available = State.state.availableBackgroundLayers.data; - for (const layer of available) { - if (layer.id === selectedId) { - return layer; - } - } - return AvailableBaseLayers.osmCarto; - }, - [State.state.availableBackgroundLayers], - (layer) => layer.id - ); + static LoadLayoutFromHash( + userLayoutParam: UIEventSource + ): [LayoutConfig, string] { + try { + let hash = location.hash.substr(1); + const layoutFromBase64 = userLayoutParam.data; + // layoutFromBase64 contains the name of the theme. This is partly to do tracking with goat counter - new LayerResetter( - State.state.backgroundLayer, - State.state.locationControl, - State.state.availableBackgroundLayers, - State.state.layoutToUse.map( - (layout: LayoutConfig) => layout.defaultBackgroundId - ) - ); + const dedicatedHashFromLocalStorage = LocalStorageSource.Get( + "user-layout-" + layoutFromBase64.replace(" ", "_") + ); + if (dedicatedHashFromLocalStorage.data?.length < 10) { + dedicatedHashFromLocalStorage.setData(undefined); + } - const attr = new Attribution( - State.state.locationControl, - State.state.osmConnection.userDetails, - State.state.layoutToUse, - State.state.leafletMap - ); + const hashFromLocalStorage = LocalStorageSource.Get( + "last-loaded-user-layout" + ); + if (hash.length < 10) { + hash = dedicatedHashFromLocalStorage.data ?? hashFromLocalStorage.data; + } else { + console.log("Saving hash to local storage"); + hashFromLocalStorage.setData(hash); + dedicatedHashFromLocalStorage.setData(hash); + } - const bm = new Basemap( - "leafletDiv", - State.state.locationControl, - State.state.backgroundLayer, - State.state.LastClickLocation, - attr - ); - State.state.leafletMap.setData(bm.map); - const layout = State.state.layoutToUse.data; - if (layout.lockLocation) { - if (layout.lockLocation === true) { - const tile = Utils.embedded_tile( - layout.startLat, - layout.startLon, - layout.startZoom - 1 - ); - const bounds = Utils.tile_bounds(tile.z, tile.x, tile.y); - // We use the bounds to get a sense of distance for this zoom level - const latDiff = bounds[0][0] - bounds[1][0]; - const lonDiff = bounds[0][1] - bounds[1][1]; - layout.lockLocation = [ - [layout.startLat - latDiff, layout.startLon - lonDiff], - [layout.startLat + latDiff, layout.startLon + lonDiff], - ]; - } - console.warn("Locking the bounds to ", layout.lockLocation); - bm.map.setMaxBounds(layout.lockLocation); - bm.map.setMinZoom(layout.startZoom); + let json: {}; + try { + json = JSON.parse(atob(hash)); + } catch (e) { + // We try to decode with lz-string + json = JSON.parse( + Utils.UnMinify(LZString.decompressFromBase64(hash)) + ) as LayoutConfigJson; + } + + // @ts-ignore + const layoutToUse = new LayoutConfig(json, false); + userLayoutParam.setData(layoutToUse.id); + return [layoutToUse, btoa(Utils.MinifyJSON(JSON.stringify(json)))]; + } catch (e) { + new FixedUiElement( + "Error: could not parse the custom layout:
" + e + ).AttachTo("centermessage"); + throw e; + } + } + + private static OnlyIf( + featureSwitch: UIEventSource, + callback: () => void + ) { + featureSwitch.addCallbackAndRun(() => { + if (featureSwitch.data) { + callback(); + } + }); + } + + private static InitWelcomeMessage() { + const isOpened = new UIEventSource(false); + const fullOptions = new FullWelcomePaneWithTabs(isOpened); + + // ?-Button on Desktop, opens panel with close-X. + const help = new MapControlButton(Svg.help_svg()); + help.onClick(() => isOpened.setData(true)); + new Toggle(fullOptions.SetClass("welcomeMessage"), help, isOpened).AttachTo( + "messagesbox" + ); + const openedTime = new Date().getTime(); + State.state.locationControl.addCallback(() => { + if (new Date().getTime() - openedTime < 15 * 1000) { + // Don't autoclose the first 15 secs when the map is moving + return; + } + isOpened.setData(false); + }); + + State.state.selectedElement.addCallbackAndRunD((_) => { + isOpened.setData(false); + }); + isOpened.setData( + Hash.hash.data === undefined || + Hash.hash.data === "" || + Hash.hash.data == "welcome" + ); + } + + private static InitLayerSelection(featureSource: FeatureSource) { + const copyrightNotice = new ScrollableFullScreen( + () => Translations.t.general.attribution.attributionTitle.Clone(), + () => + new AttributionPanel( + State.state.layoutToUse, + new ContributorCount(featureSource).Contributors + ), + "copyright" + ); + + const copyrightButton = new Toggle( + copyrightNotice, + new MapControlButton(Svg.osm_copyright_svg()), + copyrightNotice.isShown + ) + .ToggleOnClick() + .SetClass("p-0.5"); + + const layerControlPanel = new LayerControlPanel( + State.state.layerControlIsOpened + ).SetClass("block p-1 rounded-full"); + + const layerControlButton = new Toggle( + layerControlPanel, + new MapControlButton(Svg.layers_svg()), + State.state.layerControlIsOpened + ).ToggleOnClick(); + + const layerControl = new Toggle( + layerControlButton, + "", + State.state.featureSwitchLayers + ); + + // const filterView = new FilterView(State.state.FilterIsOpened).SetClass( + // "block p-1 rounded-full" + // ); + + // const filterMapControlButton = new MapControlButton( + // new CenterFlexedElement( + // Img.AsImageElement(Svg.filter, "", "width:1.25rem;height:1.25rem") + // ) + // ); + + // const filterButton = new Toggle( + // filterView, + // filterMapControlButton, + // State.state.FilterIsOpened + // ).ToggleOnClick(); + + // const filterControl = new Toggle( + // filterButton, + // "", + // State.state.featureSwitchFilter + // ); + + new Combine([copyrightButton, layerControl]).AttachTo("bottom-left"); + + State.state.locationControl.addCallback(() => { + // Close the layer selection when the map is moved + layerControlButton.isEnabled.setData(false); + copyrightButton.isEnabled.setData(false); + }); + + State.state.selectedElement.addCallbackAndRunD((_) => { + layerControlButton.isEnabled.setData(false); + copyrightButton.isEnabled.setData(false); + }); + } + + private static InitBaseMap() { + State.state.availableBackgroundLayers = + AvailableBaseLayers.AvailableLayersAt(State.state.locationControl); + State.state.backgroundLayer = State.state.backgroundLayerId.map( + (selectedId: string) => { + if (selectedId === undefined) { + return AvailableBaseLayers.osmCarto; } + + const available = State.state.availableBackgroundLayers.data; + for (const layer of available) { + if (layer.id === selectedId) { + return layer; + } + } + return AvailableBaseLayers.osmCarto; + }, + [State.state.availableBackgroundLayers], + (layer) => layer.id + ); + + new LayerResetter( + State.state.backgroundLayer, + State.state.locationControl, + State.state.availableBackgroundLayers, + State.state.layoutToUse.map( + (layout: LayoutConfig) => layout.defaultBackgroundId + ) + ); + + const attr = new Attribution( + State.state.locationControl, + State.state.osmConnection.userDetails, + State.state.layoutToUse, + State.state.leafletMap + ); + + const bm = new Basemap( + "leafletDiv", + State.state.locationControl, + State.state.backgroundLayer, + State.state.LastClickLocation, + attr + ); + State.state.leafletMap.setData(bm.map); + const layout = State.state.layoutToUse.data; + if (layout.lockLocation) { + if (layout.lockLocation === true) { + const tile = Utils.embedded_tile( + layout.startLat, + layout.startLon, + layout.startZoom - 1 + ); + const bounds = Utils.tile_bounds(tile.z, tile.x, tile.y); + // We use the bounds to get a sense of distance for this zoom level + const latDiff = bounds[0][0] - bounds[1][0]; + const lonDiff = bounds[0][1] - bounds[1][1]; + layout.lockLocation = [ + [layout.startLat - latDiff, layout.startLon - lonDiff], + [layout.startLat + latDiff, layout.startLon + lonDiff], + ]; + } + console.warn("Locking the bounds to ", layout.lockLocation); + bm.map.setMaxBounds(layout.lockLocation); + bm.map.setMinZoom(layout.startZoom); } + } - private static InitLayers(): FeatureSource { - const state = State.state; - state.filteredLayers = state.layoutToUse.map((layoutToUse) => { - const flayers = []; + private static InitLayers(): FeatureSource { + const state = State.state; + state.filteredLayers = state.layoutToUse.map((layoutToUse) => { + const flayers = []; - for (const layer of layoutToUse.layers) { - const isDisplayed = QueryParameters.GetQueryParameter( - "layer-" + layer.id, - "true", - "Wether or not layer " + layer.id + " is shown" - ).map( - (str) => str !== "false", - [], - (b) => b.toString() - ); - const flayer = { - isDisplayed: isDisplayed, - layerDef: layer, - }; - flayers.push(flayer); - } - return flayers; - }); - - const updater = new LoadFromOverpass( - state.locationControl, - state.layoutToUse, - state.leafletMap + for (const layer of layoutToUse.layers) { + const isDisplayed = QueryParameters.GetQueryParameter( + "layer-" + layer.id, + "true", + "Wether or not layer " + layer.id + " is shown" + ).map( + (str) => str !== "false", + [], + (b) => b.toString() ); - State.state.layerUpdater = updater; + const flayer = { + isDisplayed: isDisplayed, + layerDef: layer, + appliedFilters: new UIEventSource(undefined) + }; + flayers.push(flayer); + } + return flayers; + }); - const source = new FeaturePipeline( - state.filteredLayers, - updater, - state.osmApiFeatureSource, - state.layoutToUse, - state.changes, - state.locationControl, - state.selectedElement - ); + const updater = new LoadFromOverpass( + state.locationControl, + state.layoutToUse, + state.leafletMap + ); + State.state.layerUpdater = updater; - State.state.featurePipeline = source; - new ShowDataLayer( - source.features, - State.state.leafletMap, - State.state.layoutToUse - ); + const source = new FeaturePipeline( + state.filteredLayers, + updater, + state.osmApiFeatureSource, + state.layoutToUse, + state.changes, + state.locationControl, + state.selectedElement + ); - const selectedFeatureHandler = new SelectedFeatureHandler( - Hash.hash, - State.state.selectedElement, - source, - State.state.osmApiFeatureSource - ); - selectedFeatureHandler.zoomToSelectedFeature(State.state.locationControl); - return source; - } + State.state.featurePipeline = source; + new ShowDataLayer( + source.features, + State.state.leafletMap, + State.state.layoutToUse + ); - private static setupAllLayerElements() { - // ------------- Setup the layers ------------------------------- + const selectedFeatureHandler = new SelectedFeatureHandler( + Hash.hash, + State.state.selectedElement, + source, + State.state.osmApiFeatureSource + ); + selectedFeatureHandler.zoomToSelectedFeature(State.state.locationControl); + return source; + } - const source = InitUiElements.InitLayers(); - InitUiElements.InitLayerSelection(source); + private static setupAllLayerElements() { + // ------------- Setup the layers ------------------------------- - // ------------------ Setup various other UI elements ------------ + const source = InitUiElements.InitLayers(); + InitUiElements.InitLayerSelection(source); - InitUiElements.OnlyIf(State.state.featureSwitchAddNew, () => { - let presetCount = 0; - for (const layer of State.state.filteredLayers.data) { - for (const preset of layer.layerDef.presets) { - presetCount++; - } - } - if (presetCount == 0) { - return; - } + // ------------------ Setup various other UI elements ------------ - const newPointDialogIsShown = new UIEventSource(false); - const addNewPoint = new ScrollableFullScreen( - () => Translations.t.general.add.title.Clone(), - () => new SimpleAddUI(newPointDialogIsShown), - "new", - newPointDialogIsShown - ); - addNewPoint.isShown.addCallback((isShown) => { - if (!isShown) { - State.state.LastClickLocation.setData(undefined); - } - }); + InitUiElements.OnlyIf(State.state.featureSwitchAddNew, () => { + let presetCount = 0; + for (const layer of State.state.filteredLayers.data) { + for (const preset of layer.layerDef.presets) { + presetCount++; + } + } + if (presetCount == 0) { + return; + } - new StrayClickHandler( - State.state.LastClickLocation, - State.state.selectedElement, - State.state.filteredLayers, - State.state.leafletMap, - addNewPoint - ); - }); - } + const newPointDialogIsShown = new UIEventSource(false); + const addNewPoint = new ScrollableFullScreen( + () => Translations.t.general.add.title.Clone(), + () => new SimpleAddUI(newPointDialogIsShown), + "new", + newPointDialogIsShown + ); + addNewPoint.isShown.addCallback((isShown) => { + if (!isShown) { + State.state.LastClickLocation.setData(undefined); + } + }); + + new StrayClickHandler( + State.state.LastClickLocation, + State.state.selectedElement, + State.state.filteredLayers, + State.state.leafletMap, + addNewPoint + ); + }); + } } diff --git a/Logic/FeatureSource/FilteringFeatureSource.ts b/Logic/FeatureSource/FilteringFeatureSource.ts index 043f2ea3a..87c89c8a1 100644 --- a/Logic/FeatureSource/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/FilteringFeatureSource.ts @@ -1,134 +1,174 @@ import FeatureSource from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; +import { UIEventSource } from "../UIEventSource"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; import Loc from "../../Models/Loc"; import Hash from "../Web/Hash"; +import { TagsFilter } from "../Tags/TagsFilter"; export default class FilteringFeatureSource implements FeatureSource { - public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); - public readonly name = "FilteringFeatureSource" + public features: UIEventSource<{ feature: any; freshness: Date }[]> = + new UIEventSource<{ feature: any; freshness: Date }[]>([]); + public readonly name = "FilteringFeatureSource"; - constructor(layers: UIEventSource<{ - isDisplayed: UIEventSource, - layerDef: LayerConfig - }[]>, - location: UIEventSource, - selectedElement: UIEventSource, - upstream: FeatureSource) { + constructor( + layers: UIEventSource< + { + isDisplayed: UIEventSource; + layerDef: LayerConfig; + appliedFilters: UIEventSource; + }[] + >, + location: UIEventSource, + selectedElement: UIEventSource, + upstream: FeatureSource + ) { + const self = this; - const self = this; + function update() { + const layerDict = {}; + if (layers.data.length == 0) { + console.warn("No layers defined!"); + return; + } + for (const layer of layers.data) { + layerDict[layer.layerDef.id] = layer; + } - function update() { + const features: { feature: any; freshness: Date }[] = + upstream.features.data; - const layerDict = {}; - if (layers.data.length == 0) { - console.warn("No layers defined!") - return; - } - for (const layer of layers.data) { - layerDict[layer.layerDef.id] = layer; - } + const missingLayers = new Set(); - const features: { feature: any, freshness: Date }[] = upstream.features.data; + const newFeatures = features.filter((f) => { + const layerId = f.feature._matching_layer_id; - const missingLayers = new Set(); - - const newFeatures = features.filter(f => { - const layerId = f.feature._matching_layer_id; - - if(selectedElement.data?.id === f.feature.id || f.feature.id === Hash.hash.data){ - // This is the selected object - it gets a free pass even if zoom is not sufficient - return true; - } - - if (layerId !== undefined) { - const layer: { - isDisplayed: UIEventSource, - layerDef: LayerConfig - } = layerDict[layerId]; - if (layer === undefined) { - missingLayers.add(layerId) - return true; - } - - const isShown = layer.layerDef.isShown - const tags = f.feature.properties; - if (isShown.IsKnown(tags)) { - const result = layer.layerDef.isShown.GetRenderValue(f.feature.properties).txt; - if (result !== "yes") { - return false; - } - } - - if (FilteringFeatureSource.showLayer(layer, location)) { - return true; - } - } - // Does it match any other layer - e.g. because of a switch? - for (const toCheck of layers.data) { - if (!FilteringFeatureSource.showLayer(toCheck, location)) { - continue; - } - if (toCheck.layerDef.source.osmTags.matchesProperties(f.feature.properties)) { - return true; - } - } - return false; - - }); - console.log("Filtering layer source: input: ", upstream.features.data?.length, "output:", newFeatures.length) - self.features.setData(newFeatures); - if (missingLayers.size > 0) { - console.error("Some layers were not found: ", Array.from(missingLayers)) - } + if ( + selectedElement.data?.id === f.feature.id || + f.feature.id === Hash.hash.data + ) { + // This is the selected object - it gets a free pass even if zoom is not sufficient + return true; } + if (layerId !== undefined) { + const layer: { + isDisplayed: UIEventSource; + layerDef: LayerConfig; + appliedFilters: UIEventSource; + } = layerDict[layerId]; + if (layer === undefined) { + missingLayers.add(layerId); + return true; + } - upstream.features.addCallback(() => { - update() - }); - location.map(l => { - // We want something that is stable for the shown layers - const displayedLayerIndexes = []; - for (let i = 0; i < layers.data.length; i++) { - const layer = layers.data[i]; - if (l.zoom < layer.layerDef.minzoom) { - continue; - } - if (l.zoom > layer.layerDef.maxzoom) { - continue; - } - if (!layer.isDisplayed.data) { - continue; - } - displayedLayerIndexes.push(i); + const isShown = layer.layerDef.isShown; + const tags = f.feature.properties; + if (isShown.IsKnown(tags)) { + const result = layer.layerDef.isShown.GetRenderValue( + f.feature.properties + ).txt; + if (result !== "yes") { + return false; } - return displayedLayerIndexes.join(",") - }).addCallback(() => { - update(); - }); + } - layers.addCallback(update); + if (FilteringFeatureSource.showLayer(layer, location)) { + const tagsFilter = layer.appliedFilters.data; - const registered = new Set>(); - layers.addCallbackAndRun(layers => { - for (const layer of layers) { - if (registered.has(layer.isDisplayed)) { - continue; - } - registered.add(layer.isDisplayed); - layer.isDisplayed.addCallback(() => update()); + if (tagsFilter) { + const properties = f.feature.properties; + if (!tagsFilter.matchesProperties(properties)) { + return false; + } } - }) + return true; + } + } + // Does it match any other layer - e.g. because of a switch? + for (const toCheck of layers.data) { + if (!FilteringFeatureSource.showLayer(toCheck, location)) { + continue; + } + if ( + toCheck.layerDef.source.osmTags.matchesProperties( + f.feature.properties + ) + ) { + return true; + } + } + return false; + }); + console.log( + "Filtering layer source: input: ", + upstream.features.data?.length, + "output:", + newFeatures.length + ); + self.features.setData(newFeatures); + if (missingLayers.size > 0) { + console.error( + "Some layers were not found: ", + Array.from(missingLayers) + ); + } + } + + upstream.features.addCallback(() => { + update(); + }); + location + .map((l) => { + // We want something that is stable for the shown layers + const displayedLayerIndexes = []; + for (let i = 0; i < layers.data.length; i++) { + const layer = layers.data[i]; + if (l.zoom < layer.layerDef.minzoom) { + continue; + } + if (l.zoom > layer.layerDef.maxzoom) { + continue; + } + if (!layer.isDisplayed.data) { + continue; + } + displayedLayerIndexes.push(i); + } + return displayedLayerIndexes.join(","); + }) + .addCallback(() => { update(); + }); - } + layers.addCallback(update); - private static showLayer(layer: { - isDisplayed: UIEventSource, - layerDef: LayerConfig - }, location: UIEventSource) { - return layer.isDisplayed.data && (layer.layerDef.minzoom <= location.data.zoom) && (layer.layerDef.maxzoom >= location.data.zoom) - } -} \ No newline at end of file + const registered = new Set>(); + layers.addCallbackAndRun((layers) => { + for (const layer of layers) { + if (registered.has(layer.isDisplayed)) { + continue; + } + registered.add(layer.isDisplayed); + layer.isDisplayed.addCallback(() => update()); + layer.appliedFilters.addCallback(() => update()); + } + }); + + update(); + } + + private static showLayer( + layer: { + isDisplayed: UIEventSource; + layerDef: LayerConfig; + }, + location: UIEventSource + ) { + return ( + layer.isDisplayed.data && + layer.layerDef.minzoom <= location.data.zoom && + layer.layerDef.maxzoom >= location.data.zoom + ); + } +} diff --git a/State.ts b/State.ts index ca7fbff29..1a51bebb8 100644 --- a/State.ts +++ b/State.ts @@ -20,6 +20,7 @@ import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; import {Relation} from "./Logic/Osm/ExtractRelations"; import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource"; import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; +import { TagsFilter } from "./Logic/Tags/TagsFilter"; /** * Contains the global state: a bunch of UI-event sources @@ -62,9 +63,11 @@ export default class State { public filteredLayers: UIEventSource<{ readonly isDisplayed: UIEventSource; + readonly appliedFilters: UIEventSource; readonly layerDef: LayerConfig; }[]> = new UIEventSource<{ readonly isDisplayed: UIEventSource; + readonly appliedFilters: UIEventSource; readonly layerDef: LayerConfig; }[]>([]); diff --git a/UI/Base/VariableUIElement.ts b/UI/Base/VariableUIElement.ts index e7effe17f..abab84bba 100644 --- a/UI/Base/VariableUIElement.ts +++ b/UI/Base/VariableUIElement.ts @@ -1,46 +1,43 @@ -import {UIEventSource} from "../../Logic/UIEventSource"; +import { UIEventSource } from "../../Logic/UIEventSource"; import BaseUIElement from "../BaseUIElement"; export class VariableUiElement extends BaseUIElement { + private _element: HTMLElement; - private _element : HTMLElement; - - constructor(contents: UIEventSource) { - super(); - - this._element = document.createElement("span") - const el = this._element - contents.addCallbackAndRun(contents => { - while (el.firstChild) { - el.removeChild( - el.lastChild - ) - } + constructor( + contents: UIEventSource + ) { + super(); - if (contents === undefined) { - return el; - } - if (typeof contents === "string") { - el.innerHTML = contents - } else if (contents instanceof Array) { - for (const content of contents) { - const c = content.ConstructElement(); - if (c !== undefined && c !== null) { - el.appendChild(c) - } + this._element = document.createElement("span"); + const el = this._element; + contents.addCallbackAndRun((contents) => { + while (el.firstChild) { + el.removeChild(el.lastChild); + } - } - } else { - const c = contents.ConstructElement(); - if (c !== undefined && c !== null) { - el.appendChild(c) - } - } - }) - } + if (contents === undefined) { + return el; + } + if (typeof contents === "string") { + el.innerHTML = contents; + } else if (contents instanceof Array) { + for (const content of contents) { + const c = content.ConstructElement(); + if (c !== undefined && c !== null) { + el.appendChild(c); + } + } + } else { + const c = contents.ConstructElement(); + if (c !== undefined && c !== null) { + el.appendChild(c); + } + } + }); + } - protected InnerConstructElement(): HTMLElement { - return this._element; - } - -} \ No newline at end of file + protected InnerConstructElement(): HTMLElement { + return this._element; + } +} diff --git a/UI/BigComponents/FilterView.ts b/UI/BigComponents/FilterView.ts index 9b5b79386..918b26f31 100644 --- a/UI/BigComponents/FilterView.ts +++ b/UI/BigComponents/FilterView.ts @@ -1,3 +1,6 @@ +import { Utils } from "./../../Utils"; +import { FixedInputElement } from "./../Input/FixedInputElement"; +import { RadioButton } from "./../Input/RadioButton"; import { FixedUiElement } from "./../Base/FixedUiElement"; import { LayerConfigJson } from "./../../Customizations/JSON/LayerConfigJson"; import { UIEventSource } from "../../Logic/UIEventSource"; @@ -11,79 +14,104 @@ import BaseUIElement from "../BaseUIElement"; import { Translation } from "../i18n/Translation"; import ScrollableFullScreen from "../Base/ScrollableFullScreen"; 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"; /** * Shows the filter */ -export default class FilterView extends ScrollableFullScreen { - constructor(isShown: UIEventSource) { - super(FilterView.GenTitle, FilterView.Generatecontent, "filter", isShown); - } - private static GenTitle(): BaseUIElement { - return new FixedUiElement(`Filter`).SetClass( - "text-2xl break-words font-bold p-2" +export default class FilterView extends VariableUiElement { + constructor(filteredLayer) { + super( + filteredLayer.map((filteredLayers) => + filteredLayers.map(FilterView.createOneFilteredLayerElement) + ) ); } - private static Generatecontent(): BaseUIElement { - let filterPanel: BaseUIElement = new FixedUiElement(""); + static createOneFilteredLayerElement(filteredLayer) { + const layer: LayerConfig = filteredLayer.layerDef; + const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem"; - if (State.state.filteredLayers.data.length > 1) { - let activeLayers = State.state.filteredLayers; + const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle); + const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle( + iconStyle + ); - if (activeLayers === undefined) { - throw "ActiveLayers should be defined..."; - } - - const checkboxes: BaseUIElement[] = []; - - for (const layer of activeLayers.data) { - 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 (layer.layerDef.name === undefined) { - continue; - } - - const style = "display:flex;align-items:center;color:#007759"; - - const name: Translation = Translations.WT(layer.layerDef.name)?.Clone(); - - const styledNameChecked = name - .Clone() - .SetStyle("font-size:large;padding-left:1.25rem"); - - const styledNameUnChecked = name - .Clone() - .SetStyle("font-size:large;padding-left:1.25rem"); - - const layerChecked = new Combine([icon, styledNameChecked]).SetStyle( - style - ); - - const layerNotChecked = new Combine([ - iconUnselected, - styledNameUnChecked, - ]).SetStyle(style); - - checkboxes.push( - new Toggle(layerChecked, layerNotChecked, layer.isDisplayed) - .ToggleOnClick() - .SetStyle("margin:0.3em;") - ); - } - - let combinedCheckboxes = new Combine(checkboxes); - combinedCheckboxes.SetStyle("display:flex;flex-direction:column;"); - - filterPanel = new Combine([combinedCheckboxes]); - - return filterPanel; + if (filteredLayer.layerDef.name === undefined) { + return; } + + const style = "display:flex;align-items:center;color:#007759"; + + const name: Translation = Translations.WT( + filteredLayer.layerDef.name + )?.Clone(); + + const styledNameChecked = name + .Clone() + .SetStyle("font-size:large;padding-left:1.25rem"); + + const styledNameUnChecked = name + .Clone() + .SetStyle("font-size:large;padding-left:1.25rem"); + + const layerChecked = new Combine([icon, styledNameChecked]).SetStyle(style); + + const layerNotChecked = new Combine([ + iconUnselected, + styledNameUnChecked, + ]).SetStyle(style); + + let listFilterElements: InputElement[] = layer.filters.map( + FilterView.createFilter + ); + + function update() { + let listTagsFilters = Utils.NoNull( + listFilterElements.map((input) => input.GetValue().data) + ); + filteredLayer.appliedTags.setData(new And(listTagsFilters)); + } + + listFilterElements.forEach((inputElement) => + inputElement.GetValue().addCallback((_) => update()) + ); + + return new Toggle( + new Combine([layerChecked, ...listFilterElements]), + layerNotChecked, + filteredLayer.isDisplayed + ) + .ToggleOnClick() + .SetStyle("margin:0.3em;"); + } + + static createFilter(filterConfig: FilterConfig): InputElement { + if (filterConfig.options.length === 1) { + let option = filterConfig.options[0]; + let checkboxes = new CheckBoxes([option.question.Clone()]); + + return new InputElementMap( + checkboxes, + (t0, t1) => t0 === t1, + (numbers) => (numbers.length > 0 ? option.osmTags : undefined), + (tagsFilter) => (tagsFilter == undefined ? [] : [0]) + ); + } + + let options = filterConfig.options; + + return new RadioButton( + options.map( + (option) => + new FixedInputElement(option.question.Clone(), option.osmTags) + ) + ); } } diff --git a/UI/BigComponents/LayerControlPanel.ts b/UI/BigComponents/LayerControlPanel.ts index c8837fbcc..c87e97cee 100644 --- a/UI/BigComponents/LayerControlPanel.ts +++ b/UI/BigComponents/LayerControlPanel.ts @@ -4,45 +4,56 @@ import LayerSelection from "./LayerSelection"; import Combine from "../Base/Combine"; import ScrollableFullScreen from "../Base/ScrollableFullScreen"; import Translations from "../i18n/Translations"; -import {UIEventSource} from "../../Logic/UIEventSource"; +import { UIEventSource } from "../../Logic/UIEventSource"; import BaseUIElement from "../BaseUIElement"; import Toggle from "../Input/Toggle"; -import {ExportDataButton} from "./ExportDataButton"; +import { ExportDataButton } from "./ExportDataButton"; +import FilterView from "./FilterView"; export default class LayerControlPanel extends ScrollableFullScreen { + constructor(isShown: UIEventSource) { + super( + LayerControlPanel.GenTitle, + LayerControlPanel.GeneratePanel, + "layers", + isShown + ); + } - constructor(isShown: UIEventSource) { - super(LayerControlPanel.GenTitle, LayerControlPanel.GeneratePanel, "layers", isShown); + private static GenTitle(): BaseUIElement { + return Translations.t.general.layerSelection.title + .Clone() + .SetClass("text-2xl break-words font-bold p-2"); + } + + private static GeneratePanel(): BaseUIElement { + const elements: BaseUIElement[] = []; + + if (State.state.layoutToUse.data.enableBackgroundLayerSelection) { + const backgroundSelector = new BackgroundSelector(); + backgroundSelector.SetStyle("margin:1em"); + backgroundSelector.onClick(() => {}); + elements.push(backgroundSelector); } - private static GenTitle(): BaseUIElement { - return Translations.t.general.layerSelection.title.Clone().SetClass("text-2xl break-words font-bold p-2") - } + elements.push( + new Toggle( + new FilterView(State.state.filteredLayers), + undefined, + State.state.filteredLayers.map( + (layers) => layers.length > 1 || layers[0].layerDef.filters.length > 0 + ) + ) + ); - private static GeneratePanel(): BaseUIElement { - const elements: BaseUIElement[] = [] + elements.push( + new Toggle( + new ExportDataButton(), + undefined, + State.state.featureSwitchEnableExport + ) + ); - if (State.state.layoutToUse.data.enableBackgroundLayerSelection) { - const backgroundSelector = new BackgroundSelector(); - backgroundSelector.SetStyle("margin:1em"); - backgroundSelector.onClick(() => { - }); - elements.push(backgroundSelector) - } - - elements.push(new Toggle( - new LayerSelection(State.state.filteredLayers), - undefined, - State.state.filteredLayers.map(layers => layers.length > 1) - )) - - elements.push(new Toggle( - new ExportDataButton(), - undefined, - State.state.featureSwitchEnableExport - )) - - return new Combine(elements).SetClass("flex flex-col") - } - -} \ No newline at end of file + return new Combine(elements).SetClass("flex flex-col"); + } +} diff --git a/assets/layers/public_bookcase/public_bookcase.json b/assets/layers/public_bookcase/public_bookcase.json index 1efe04b3a..19d267640 100644 --- a/assets/layers/public_bookcase/public_bookcase.json +++ b/assets/layers/public_bookcase/public_bookcase.json @@ -71,9 +71,7 @@ "ru": "Книжный шкаф", "it": "Microbiblioteca" }, - "tags": [ - "amenity=public_bookcase" - ], + "tags": ["amenity=public_bookcase"], "preciseInput": { "preferredBackground": "photo" } @@ -107,10 +105,7 @@ "mappings": [ { "if": { - "and": [ - "noname=yes", - "name=" - ] + "and": ["noname=yes", "name="] }, "then": { "en": "This bookcase doesn't have a name", @@ -316,18 +311,12 @@ "it": "Fa parte della rete 'Little Free Library'" }, "if": { - "and": [ - "brand=Little Free Library", - "nobrand=" - ] + "and": ["brand=Little Free Library", "nobrand="] } }, { "if": { - "and": [ - "nobrand=yes", - "brand=" - ] + "and": ["nobrand=yes", "brand="] }, "then": { "en": "This public bookcase is not part of a bigger network", @@ -368,11 +357,7 @@ "it": "Questa microbiblioteca non fa parte di una rete" }, "if": { - "and": [ - "nobrand=yes", - "brand=", - "ref=" - ] + "and": ["nobrand=yes", "brand=", "ref="] } } ] @@ -424,11 +409,18 @@ ], "deletion": { "softDeletionTags": { - "and": [ - "disused:amenity=public_bookcase", - "amenity=" - ] + "and": ["disused:amenity=public_bookcase", "amenity="] }, "neededChangesets": 5 - } -} \ No newline at end of file + }, + "filter": [ + { + "options": [ + { + "question": "Kinderboeken aanwezig?", + "osmTags": "books~.*children.*" + } + ] + } + ] +}