From 895aa132ec23ae17fbb129f8d01b722898fe4b7b Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 27 Oct 2020 01:01:34 +0100 Subject: [PATCH] Huge refactoring (WIP) --- Customizations/AllKnownLayouts.ts | 45 +- Customizations/JSON/FromJSON.ts | 309 +--------- Customizations/JSON/LayerConfig.ts | 114 ++++ Customizations/JSON/LayerConfigJson.ts | 4 +- Customizations/JSON/TagRenderingConfig.ts | 112 ++++ Customizations/LayerDefinition.ts | 133 ----- Customizations/Layout.ts | 49 +- Customizations/OnlyShowIf.ts | 98 ---- Customizations/Questions/OsmLink.ts | 29 - Customizations/Questions/WikipediaLink.ts | 50 -- Customizations/SharedLayers.ts | 50 ++ Customizations/SharedTagRenderings.ts | 20 + Customizations/StreetWidth/StreetWidth.ts | 107 ---- Customizations/StreetWidth/Widths.ts | 312 ---------- Customizations/TagRenderingOptions.ts | 148 ----- Customizations/UIElementConstructor.ts | 21 - InitUiElements.ts | 48 +- Logic/FilteredLayer.ts | 73 ++- Logic/ImageSearcher.ts | 1 - Logic/Osm/Changes.ts | 7 +- Logic/Tags.ts | 6 +- Logic/UpdateFromOverpass.ts | 5 +- State.ts | 2 - UI/Image/ImageCarousel.ts | 18 +- UI/Img.ts | 10 - UI/Input/ValidatedTextField.ts | 7 + UI/Popup/EditableTagRendering.ts | 83 +++ UI/Popup/FeatureInfoBox.ts | 159 +---- UI/Popup/QuestionBox.ts | 78 +++ UI/Popup/SaveButton.ts | 21 +- UI/Popup/TagRendering.ts | 544 ------------------ UI/Popup/TagRenderingAnswer.ts | 28 + UI/Popup/TagRenderingQuestion.ts | 251 ++++++++ UI/SimpleAddUI.ts | 19 +- UI/UserBadge.ts | 4 +- UI/WelcomeMessage.ts | 3 +- UI/i18n/Locale.ts | 16 + UI/i18n/Translations.ts | 16 + .../layers/nature_reserve/nature_reserve.json | 16 +- assets/osm-logo-us.svg | 7 + assets/questions/questions.json | 32 ++ assets/themes/aed/aed.json | 6 +- assets/themes/artwork/artwork.json | 2 +- assets/themes/fritures/fritures.json | 2 +- assets/themes/toilets/toilets.json | 2 +- createLayouts.ts | 3 +- css/openinghourstable.css | 4 +- css/tagrendering.css | 109 ++++ customGenerator.ts | 3 - index.css | 104 +--- index.html | 1 + index.ts | 6 +- test.html | 1 + test.ts | 50 +- test/Tag.spec.ts | 19 +- 55 files changed, 1177 insertions(+), 2190 deletions(-) create mode 100644 Customizations/JSON/LayerConfig.ts create mode 100644 Customizations/JSON/TagRenderingConfig.ts delete mode 100644 Customizations/LayerDefinition.ts delete mode 100644 Customizations/OnlyShowIf.ts delete mode 100644 Customizations/Questions/OsmLink.ts delete mode 100644 Customizations/Questions/WikipediaLink.ts create mode 100644 Customizations/SharedLayers.ts create mode 100644 Customizations/SharedTagRenderings.ts delete mode 100644 Customizations/StreetWidth/StreetWidth.ts delete mode 100644 Customizations/StreetWidth/Widths.ts delete mode 100644 Customizations/TagRenderingOptions.ts delete mode 100644 Customizations/UIElementConstructor.ts create mode 100644 UI/Popup/EditableTagRendering.ts create mode 100644 UI/Popup/QuestionBox.ts delete mode 100644 UI/Popup/TagRendering.ts create mode 100644 UI/Popup/TagRenderingAnswer.ts create mode 100644 UI/Popup/TagRenderingQuestion.ts create mode 100644 assets/osm-logo-us.svg create mode 100644 assets/questions/questions.json create mode 100644 css/tagrendering.css diff --git a/Customizations/AllKnownLayouts.ts b/Customizations/AllKnownLayouts.ts index 99af393..fea2667 100644 --- a/Customizations/AllKnownLayouts.ts +++ b/Customizations/AllKnownLayouts.ts @@ -1,4 +1,3 @@ -import {LayerDefinition} from "./LayerDefinition"; import {Layout} from "./Layout"; import {FromJSON} from "./JSON/FromJSON"; import * as bookcases from "../assets/themes/bookcases/Bookcases.json"; @@ -18,14 +17,15 @@ import * as benches from "../assets/themes/benches/benches.json"; import * as charging_stations from "../assets/themes/charging_stations/charging_stations.json" import {PersonalLayout} from "../Logic/PersonalLayout"; -import {StreetWidth} from "./StreetWidth/StreetWidth"; +import LayerConfig from "./JSON/LayerConfig"; +import SharedLayers from "./SharedLayers"; export class AllKnownLayouts { - public static allLayers: Map = undefined; + public static allLayers: Map = undefined; private static GenerateCycloFix(): Layout { - const layout = FromJSON.LayoutFromJSON(cyclofix) + const layout = Layout.LayoutFromJSON(cyclofix, SharedLayers.sharedLayers) const now = new Date(); const m = now.getMonth() + 1; const day = new Date().getDay() + 1; @@ -33,7 +33,7 @@ export class AllKnownLayouts { if (date === "31/10" || date === "1/11" || date === "2/11") { // Around Halloween/Fiesta de muerte/Allerzielen, we remember the dead layout.layers.push( - FromJSON.sharedLayers.get("ghost_bike") + SharedLayers.sharedLayers.get("ghost_bike") ); } @@ -42,7 +42,7 @@ export class AllKnownLayouts { } private static GenerateBuurtNatuur(): Layout { - const layout = FromJSON.LayoutFromJSON(buurtnatuur); + const layout = Layout.LayoutFromJSON(buurtnatuur, SharedLayers.sharedLayers); layout.enableMoreQuests = false; layout.enableShareScreen = false; layout.hideFromOverview = true; @@ -50,7 +50,7 @@ export class AllKnownLayouts { } private static GenerateBikeMonitoringStations(): Layout { - const layout = FromJSON.LayoutFromJSON(bike_monitoring_stations); + const layout = Layout.LayoutFromJSON(bike_monitoring_stations, SharedLayers.sharedLayers); layout.hideFromOverview = true; return layout; } @@ -60,37 +60,36 @@ export class AllKnownLayouts { public static layoutsList: Layout[] = [ new PersonalLayout(), - FromJSON.LayoutFromJSON(shops), - FromJSON.LayoutFromJSON(bookcases), - FromJSON.LayoutFromJSON(aed), - FromJSON.LayoutFromJSON(toilets), - FromJSON.LayoutFromJSON(artworks), + Layout.LayoutFromJSON(shops, SharedLayers.sharedLayers), + Layout.LayoutFromJSON(bookcases, SharedLayers.sharedLayers), + Layout.LayoutFromJSON(aed, SharedLayers.sharedLayers), + Layout.LayoutFromJSON(toilets, SharedLayers.sharedLayers), + Layout.LayoutFromJSON(artworks, SharedLayers.sharedLayers), AllKnownLayouts.GenerateCycloFix(), - FromJSON.LayoutFromJSON(ghostbikes), - FromJSON.LayoutFromJSON(nature), - FromJSON.LayoutFromJSON(cyclestreets), - FromJSON.LayoutFromJSON(maps), - FromJSON.LayoutFromJSON(fritures), - FromJSON.LayoutFromJSON(benches), - FromJSON.LayoutFromJSON(charging_stations), + Layout.LayoutFromJSON(ghostbikes, SharedLayers.sharedLayers), + Layout.LayoutFromJSON(nature, SharedLayers.sharedLayers), + Layout.LayoutFromJSON(cyclestreets, SharedLayers.sharedLayers), + Layout.LayoutFromJSON(maps, SharedLayers.sharedLayers), + Layout.LayoutFromJSON(fritures, SharedLayers.sharedLayers), + Layout.LayoutFromJSON(benches, SharedLayers.sharedLayers), + Layout.LayoutFromJSON(charging_stations, SharedLayers.sharedLayers), AllKnownLayouts.GenerateBuurtNatuur(), AllKnownLayouts.GenerateBikeMonitoringStations(), - new StreetWidth(), // The ugly duckling ]; public static allSets: Map = AllKnownLayouts.AllLayouts(); private static AllLayouts(): Map { - this.allLayers = new Map(); + this.allLayers = new Map(); for (const layout of this.layoutsList) { for (let i = 0; i < layout.layers.length; i++) { let layer = layout.layers[i]; if (typeof (layer) === "string") { - layer = layout.layers[i] = FromJSON.sharedLayers.get(layer); + layer = layout.layers[i] = SharedLayers.sharedLayers.get(layer); if(layer === undefined){ - console.log("Defined layers are ", FromJSON.sharedLayers.keys()) + console.log("Defined layers are ", SharedLayers.sharedLayers.keys()) throw `Layer ${layer} was not found or defined - probably a type was made` } } diff --git a/Customizations/JSON/FromJSON.ts b/Customizations/JSON/FromJSON.ts index 662529f..1a95047 100644 --- a/Customizations/JSON/FromJSON.ts +++ b/Customizations/JSON/FromJSON.ts @@ -1,102 +1,11 @@ -import {Layout} from "../Layout"; -import {LayoutConfigJson} from "./LayoutConfigJson"; import {AndOrTagConfigJson} from "./TagConfigJson"; import {And, Or, RegexTag, Tag, TagsFilter} from "../../Logic/Tags"; -import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; -import {TagRenderingOptions} from "../TagRenderingOptions"; import Translation from "../../UI/i18n/Translation"; -import {LayerConfigJson} from "./LayerConfigJson"; -import {LayerDefinition, Preset} from "../LayerDefinition"; -import {TagDependantUIElementConstructor} from "../UIElementConstructor"; -import Combine from "../../UI/Base/Combine"; -import * as drinkingWater from "../../assets/layers/drinking_water/drinking_water.json"; -import * as ghostbikes from "../../assets/layers/ghost_bike/ghost_bike.json" -import * as viewpoint from "../../assets/layers/viewpoint/viewpoint.json" -import * as bike_parking from "../../assets/layers/bike_parking/bike_parking.json" -import * as bike_repair_station from "../../assets/layers/bike_repair_station/bike_repair_station.json" -import * as birdhides from "../../assets/layers/bird_hide/birdhides.json" -import * as nature_reserve from "../../assets/layers/nature_reserve/nature_reserve.json" -import * as bike_cafes from "../../assets/layers/bike_cafe/bike_cafes.json" -import * as bike_monitoring_station from "../../assets/layers/bike_monitoring_station/bike_monitoring_station.json" -import * as cycling_themed_objects from "../../assets/layers/cycling_themed_object/cycling_themed_objects.json" -import * as bike_shops from "../../assets/layers/bike_shop/bike_shop.json" -import * as maps from "../../assets/layers/maps/maps.json" -import * as information_boards from "../../assets/layers/information_board/information_board.json" + import {Utils} from "../../Utils"; -import State from "../../State"; export class FromJSON { - public static sharedLayers: Map = FromJSON.getSharedLayers(); - - private static getSharedLayers() { - - // We inject a function into state while we are busy - State.FromBase64 = FromJSON.FromBase64; - - const sharedLayers = new Map(); - - const sharedLayersList = [ - FromJSON.Layer(drinkingWater), - FromJSON.Layer(ghostbikes), - FromJSON.Layer(viewpoint), - FromJSON.Layer(bike_parking), - FromJSON.Layer(bike_repair_station), - FromJSON.Layer(bike_monitoring_station), - FromJSON.Layer(birdhides), - FromJSON.Layer(nature_reserve), - FromJSON.Layer(bike_cafes), - FromJSON.Layer(cycling_themed_objects), - FromJSON.Layer(bike_shops), - FromJSON.Layer(maps), - FromJSON.Layer(information_boards) - ]; - - for (const layer of sharedLayersList) { - sharedLayers.set(layer.id, layer); - } - - return sharedLayers; - } - - public static FromBase64(layoutFromBase64: string): Layout { - return FromJSON.LayoutFromJSON(JSON.parse(atob(layoutFromBase64))); - } - - public static LayoutFromJSON(json: LayoutConfigJson): Layout { - const tr = FromJSON.Translation; - - const layers = json.layers.map(FromJSON.Layer); - const roaming: TagDependantUIElementConstructor[] = json.roamingRenderings?.map((tr, i) => FromJSON.TagRendering(tr, "Roaming rendering "+i)) ?? []; - for (const layer of layers) { - layer.elementsToShow.push(...roaming); - } - - const layout = new Layout( - json.id, - typeof (json.language) === "string" ? [json.language] : json.language, - tr(json.title ?? "Title not defined"), - layers, - json.startZoom, - json.startLat, - json.startLon, - new Combine(["

", tr(json.title), "

", tr(json.description)]), - undefined, - undefined, - tr(json.descriptionTail) - - ); - - layout.defaultBackground = json.defaultBackgroundId ?? "osm"; - layout.widenFactor = json.widenFactor ?? 0.07; - layout.icon = json.icon; - layout.maintainer = json.maintainer; - layout.version = json.version; - layout.socialImage = json.socialImage; - layout.description = tr(json.shortDescription) ?? tr(json.description)?.FirstSentence(); - layout.changesetMessage = json.changesetmessage; - return layout; - } public static Translation(json: string | any): Translation { if (json === undefined) { @@ -122,104 +31,6 @@ export class FromJSON { return transl; } - public static TagRendering(json: TagRenderingConfigJson | string, propertyeName: string): TagDependantUIElementConstructor { - return FromJSON.TagRenderingWithDefault(json, propertyeName, undefined); - } - - public static TagRenderingWithDefault(json: TagRenderingConfigJson | string, propertyName, defaultValue: string): TagDependantUIElementConstructor { - if (json === undefined) { - if(defaultValue !== undefined){ - return FromJSON.TagRendering(defaultValue, propertyName); - } - throw `Tagrendering ${propertyName} is undefined...` - } - - - if (typeof json === "string") { - switch (json) { - case "pictures": { - json = "{image_carousel()}{image_upload()}"; - break; - } - case "images": { - json = "{image_carousel()}{image_upload()}"; - } - } - - return new TagRenderingOptions({ - freeform: { - key: "id", - renderTemplate: json, - template: "$$$" - } - }); - } - // It's the question that drives us, neo - const question = FromJSON.Translation(json.question); - - let template = FromJSON.Translation(json.render); - - let freeform = undefined; - if (json.freeform?.key && json.freeform.key !== "") { - // Setup the freeform - if (template === undefined) { - console.error("Freeform.key is defined, but render is not. This is not allowed.", json) - throw `Freeform is defined in tagrendering ${propertyName}, but render is not. This is not allowed.` - } - - freeform = { - template: `$${json.freeform.type ?? "string"}$`, - renderTemplate: template, - key: json.freeform.key - }; - if (json.freeform.addExtraTags) { - freeform.extraTags = new And(json.freeform.addExtraTags.map(FromJSON.SimpleTag)) - } - } else if (json.render) { - // Template (aka rendering) is defined, but freeform.key is not. We allow an input as string - freeform = { - template: undefined, // Template to ask is undefined -> we block asking for this key - renderTemplate: template, - key: "id" // every object always has an id - } - } - - const mappings = json.mappings?.map((mapping, i) => { - const k = FromJSON.Tag(mapping.if, `IN mapping #${i} of tagrendering ${propertyName}`) - - if (question !== undefined && !mapping.hideInAnswer && !k.isUsableAsAnswer()) { - throw `Invalid mapping in ${propertyName}.${i}: this mapping uses a regex tag or an OR, but is also answerable. Either mark 'Not an answer option' or only use '=' to map key/values.` - } - - return { - k: k, - txt: FromJSON.Translation(mapping.then), - hideInAnswer: mapping.hideInAnswer - }; - } - ); - - if (template === undefined && (mappings === undefined || mappings.length === 0)) { - console.error(`Empty tagrendering detected in ${propertyName}: no mappings nor template given`, json) - throw `Empty tagrendering ${propertyName} detected: no mappings nor template given` - } - - - let rendering = new TagRenderingOptions({ - question: question, - freeform: freeform, - mappings: mappings, - multiAnswer: json.multiAnswer - }); - - if (json.condition) { - const condition = FromJSON.Tag(json.condition, `In tagrendering ${propertyName}.condition`); - return rendering.OnlyShowIf(condition); - } - - return rendering; - } - public static SimpleTag(json: string): Tag { const tag = Utils.SplitFirst(json, "="); return new Tag(tag[0], tag[1]); @@ -227,7 +38,7 @@ export class FromJSON { public static Tag(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter { if(json === undefined){ - throw "Error while parsing a tag: nothing defined. Make sure all the tags are defined and at least one tag is present in a complex expression" + throw `Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression` } if (typeof (json) == "string") { const tag = json as string; @@ -286,120 +97,4 @@ export class FromJSON { return new Or(json.or.map(t => FromJSON.Tag(t, context))); } } - - public static Layer(json: LayerConfigJson | string): LayerDefinition { - if (typeof (json) === "string") { - const cached = FromJSON.sharedLayers.get(json); - if (cached) { - return cached; - } - throw `Layer ${json} not yet loaded...` - } - try { - return FromJSON.LayerUncaught(json); - } catch (e) { - throw `While parsing layer ${json.id}: ${e}` - } - } - - private static LayerUncaught(json: LayerConfigJson): LayerDefinition { - - const tr = FromJSON.Translation; - const overpassTags = FromJSON.Tag(json.overpassTags, "overpasstags for layer "+json.id); - const icon = FromJSON.TagRenderingWithDefault(json.icon, "icon", "./assets/bug.svg"); - const iconSize = FromJSON.TagRenderingWithDefault(json.iconSize, "iconSize", "40,40,center"); - const color = FromJSON.TagRenderingWithDefault(json.color, "color", "#0000ff"); - const width = FromJSON.TagRenderingWithDefault(json.width, "width", "10"); - if (json.title === "Layer") { - json.title = {}; - } - let title = FromJSON.TagRendering(json.title, "Popup title"); - - - let tagRenderingDefs = json.tagRenderings ?? []; - let hasImageElement = false; - for (const tagRenderingDef of tagRenderingDefs) { - if (typeof tagRenderingDef !== "string") { - continue; - } - let str = tagRenderingDef as string; - if (tagRenderingDef.indexOf("images") >= 0 || str.indexOf("pictures") >= 0) { - hasImageElement = true; - break; - } - } - if (!hasImageElement) { - tagRenderingDefs = ["images", ...tagRenderingDefs]; - } - let tagRenderings = tagRenderingDefs.map((tr, i) => FromJSON.TagRendering(tr, "Tagrendering #"+i)); - - - const renderTags = {"id": "node/-1"} - const presets: Preset[] = json?.presets?.map(preset => { - return ({ - title: tr(preset.title), - description: tr(preset.description), - tags: preset.tags.map(FromJSON.SimpleTag) - }); - }) ?? []; - - function style(tags) { - const iconSizeStr = - iconSize.GetContent(tags).txt.split(","); - const iconwidth = Number(iconSizeStr[0]); - const iconheight = Number(iconSizeStr[1]); - const iconmode = iconSizeStr[2]; - const iconAnchor = [iconwidth / 2, iconheight / 2] // x, y - // If iconAnchor is set to [0,0], then the top-left of the icon will be placed at the geographical location - if (iconmode.indexOf("left") >= 0) { - iconAnchor[0] = 0; - } - if (iconmode.indexOf("right") >= 0) { - iconAnchor[0] = iconwidth; - } - - if (iconmode.indexOf("top") >= 0) { - iconAnchor[1] = 0; - } - if (iconmode.indexOf("bottom") >= 0) { - iconAnchor[1] = iconheight; - } - - // the anchor is always set from the center of the point - // x, y with x going right and y going down if the values are bigger - const popupAnchor = [0, 3 - iconAnchor[1]]; - - return { - color: color.GetContent(tags).txt, - weight: width.GetContent(tags).txt, - icon: { - iconUrl: icon.GetContent(tags).txt, - iconSize: [iconwidth, iconheight], - popupAnchor: popupAnchor, - iconAnchor: iconAnchor - }, - } - } - - const layer = new LayerDefinition( - json.id, - { - name: tr(json.name), - description: tr(json.description), - icon: icon.GetContent(renderTags).txt, - overpassFilter: overpassTags, - - title: title, - minzoom: json.minzoom, - presets: presets, - elementsToShow: tagRenderings, - style: style, - wayHandling: json.wayHandling - - } - ); - layer.maxAllowedOverlapPercentage = json.hideUnderlayingFeaturesMinPercentage ?? 0; - return layer; - } - } \ No newline at end of file diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts new file mode 100644 index 0000000..b280d2a --- /dev/null +++ b/Customizations/JSON/LayerConfig.ts @@ -0,0 +1,114 @@ +import Translation from "../../UI/i18n/Translation"; +import TagRenderingConfig from "./TagRenderingConfig"; +import {Tag, TagsFilter} from "../../Logic/Tags"; +import {LayerConfigJson} from "./LayerConfigJson"; +import Translations from "../../UI/i18n/Translations"; +import {FromJSON} from "./FromJSON"; +import SharedTagRenderings from "../SharedTagRenderings"; +import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; + +export default class LayerConfig { + id: string; + + name: Translation + + description: Translation; + overpassTags: TagsFilter; + + minzoom: number; + + title: TagRenderingConfig; + + titleIcons: TagRenderingConfig[]; + + icon?: TagRenderingConfig; + iconSize?: TagRenderingConfig; + color?: TagRenderingConfig; + width?: TagRenderingConfig; + + + wayHandling: number; + + static WAYHANDLING_DEFAULT = 0; + static WAYHANDLING_CENTER_ONLY = 1; + static WAYHANDLING_CENTER_AND_WAY = 2; + + hideUnderlayingFeaturesMinPercentage?: number; + + presets: { + title: Translation, + tags: Tag[], + description?: Translation, + }[]; + + tagRenderings: TagRenderingConfig []; + + constructor(json: LayerConfigJson, context?: string) { + context = context + "." + json.id; + + this.id = json.id; + this.name = Translations.T(json.name); + this.description = Translations.T(json.name); + this.overpassTags = FromJSON.Tag(json.overpassTags, context + ".overpasstags"); + this.minzoom = json.minzoom; + this.wayHandling = json.wayHandling ?? 0; + this.hideUnderlayingFeaturesMinPercentage = json.hideUnderlayingFeaturesMinPercentage ?? 0; + this.title = new TagRenderingConfig(json.title); + this.presets = (json.presets ?? []).map(pr => ({ + title: Translations.T(pr.title), + tags: pr.tags.map(t => FromJSON.SimpleTag(t)), + description: Translations.T(pr.description) + })) + + + /** + * Converts a list of tagRenderingCOnfigJSON in to TagRenderingConfig + * A string is interpreted as a name to call + * @param tagRenderings + */ + function trs(tagRenderings?: (string | TagRenderingConfigJson)[]) { + if (tagRenderings === undefined) { + return []; + } + return tagRenderings.map( + (renderingJson, i) => { + if (typeof renderingJson === "string") { + const shared = SharedTagRenderings.SharedTagRendering[renderingJson]; + if (shared !== undefined) { + return shared; + } + throw `Predefined tagRendering ${renderingJson} not found in ${context}`; + } + return new TagRenderingConfig(renderingJson, `${context}.tagrendering[${i}]`); + }); + } + + this.tagRenderings = trs(json.tagRenderings); + this.titleIcons = trs(json.titleIcons ?? ["wikipedialink","osmlink"]); + + + function tr(key, deflt) { + const v = json[key]; + if (v === undefined) { + return new TagRenderingConfig(deflt); + } + if (typeof v === "string") { + const shared = SharedTagRenderings.SharedTagRendering[v]; + if (shared) { + console.log("Got shared TR:", v, "-->", shared) + return shared; + } + } + return new TagRenderingConfig(v, context + "." + key); + } + + + this.title = tr("title", ""); + this.icon = tr("icon", "./assets/bug.svg"); + this.iconSize = tr("iconSize", "40,40,center"); + this.color = tr("color", "#0000ff"); + this.width = tr("width", "7"); + + + } +} \ No newline at end of file diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts index 1eac673..cc93853 100644 --- a/Customizations/JSON/LayerConfigJson.ts +++ b/Customizations/JSON/LayerConfigJson.ts @@ -37,9 +37,11 @@ export interface LayerConfigJson { /** - * The title shown in a popup for elements of this layer + * The title shown in a popup for elements of this layer. */ title: string | TagRenderingConfigJson; + + titleIcons?: (string | TagRenderingConfigJson)[]; /** * The icon for an element. diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts new file mode 100644 index 0000000..0cb8e95 --- /dev/null +++ b/Customizations/JSON/TagRenderingConfig.ts @@ -0,0 +1,112 @@ +import Translation from "../../UI/i18n/Translation"; +import {TagsFilter} from "../../Logic/Tags"; +import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; +import Translations from "../../UI/i18n/Translations"; +import {FromJSON} from "./FromJSON"; +import ValidatedTextField from "../../UI/Input/ValidatedTextField"; + +/*** + * The parsed version of TagRenderingConfigJSON + * Identical data, but with some methods and validation + */ +export default class TagRenderingConfig { + + render?: Translation; + question?: Translation; + condition?: TagsFilter; + + freeform?: { + key: string, + type: string, + addExtraTags: TagsFilter[]; + }; + + multiAnswer: boolean; + + mappings?: { + if: TagsFilter, + then: Translation + hideInAnswer: boolean + }[] + + constructor(json: string | TagRenderingConfigJson, context?: string) { + + if(json === undefined){ + throw "Initing a TagRenderingConfig with undefined in "+context; + } + if (typeof json === "string") { + this.render = Translations.T(json); + this.multiAnswer = false; + return; + } + + this.render = Translations.T(json.render); + this.question = Translations.T(json.question); + this.condition = FromJSON.Tag(json.condition ?? {"and": []}, `${context}.condition`); + if (json.freeform) { + this.freeform = { + key: json.freeform.key, + type: json.freeform.type ?? "string", + addExtraTags: json.freeform.addExtraTags?.map((tg, i) => + FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [] + } + if (ValidatedTextField.AllTypes[this.freeform.type] === undefined) { + throw `Freeform.key ${this.freeform.key} is an invalid type` + } + } + + this.multiAnswer = json.multiAnswer ?? false + if (json.mappings) { + this.mappings = json.mappings.map((mapping, i) => { + + if (mapping.then === undefined) { + throw "Invalid mapping: if without body" + } + return { + if: FromJSON.Tag(mapping.if, `${context}.mapping[${i}]`), + then: Translations.T(mapping.then), + hideInAnswer: mapping.hideInAnswer ?? false + }; + }); + } + + if (this.question && this.freeform?.key === undefined && this.mappings === undefined) { + throw `A question is defined, but no mappings nor freeform (key) are. The question is ${this.question.txt} at ${context}` + } + + if (json.multiAnswer) { + if ((this.mappings?.length ?? 0) === 0) { + throw "MultiAnswer is set, but no mappings are defined" + } + + } + } + + /** + * Gets the correct rendering value (or undefined if not known) + * @constructor + */ + public GetRenderValue(tags: any): Translation { + if (this.mappings !== undefined && !this.multiAnswer) { + for (const mapping of this.mappings) { + if (mapping.if === undefined) { + return mapping.then; + } + if (mapping.if.matchesProperties(tags)) { + return mapping.then; + } + } + } + + if (this.freeform?.key === undefined){ + return this.render; + } + + if (tags[this.freeform.key] !== undefined) { + return this.render; + } + return undefined; + } + + +} \ No newline at end of file diff --git a/Customizations/LayerDefinition.ts b/Customizations/LayerDefinition.ts deleted file mode 100644 index 77e1ef3..0000000 --- a/Customizations/LayerDefinition.ts +++ /dev/null @@ -1,133 +0,0 @@ -import {Tag, TagsFilter} from "../Logic/Tags"; -import {UIElement} from "../UI/UIElement"; -import {TagDependantUIElementConstructor} from "./UIElementConstructor"; -import {TagRenderingOptions} from "./TagRenderingOptions"; -import Translation from "../UI/i18n/Translation"; - -export interface Preset { - tags: Tag[], - title: string | UIElement, - description?: string | UIElement, - icon?: string | TagRenderingOptions -} - -export class LayerDefinition { - - - /** - * This name is used in the 'hide or show this layer'-buttons - */ - name: string | Translation; - - /*** - * This is shown under the 'add new' button to indicate what kind of feature one is adding. - */ - description: string | Translation - - /** - * These tags are added whenever a new point is added by the user on the map. - * This is the ideal place to add extra info, such as "fixme=added by MapComplete, geometry should be checked" - */ - presets: Preset[] - /** - * Not really used anymore - * This is meant to serve as icon in the buttons - */ - icon: string | TagRenderingOptions; - /** - * Only show this layer starting at this zoom level - */ - minzoom: number; - - /** - * This tagfilter is used to query overpass. - * Examples are: - * - * new Tag("amenity","drinking_water") - * - * or a query for bicycle pumps which have two tagging schemes: - * new Or([ - * new Tag("service:bicycle:pump","yes") , - * new And([ - * new Tag("amenity","compressed_air"), - * new Tag("bicycle","yes")]) - * ]) - */ - overpassFilter: TagsFilter; - public readonly id: string; - - /** - * This UIElement is rendered as title element in the popup - */ - title: TagDependantUIElementConstructor | UIElement | string; - /** - * These are the questions/shown attributes in the popup - */ - elementsToShow: TagDependantUIElementConstructor[]; - - /** - * A simple styling for the geojson element - * color is the color for areas and ways - * icon is the Leaflet icon - * Note that this is passed entirely to leaflet, so other leaflet attributes work too - */ - style: (tags: any) => { - color: string, - weight?: number, - icon: { - iconUrl: string, - iconSize?: [number, number], - popupAnchor?: [number,number], - iconAnchor?: [number,number] - }, - }; - - /** - * If an object of the next layer is contained for this many percent in this feature, it is eaten and not shown - */ - maxAllowedOverlapPercentage: number = undefined; - - /** - * If true, then ways (and polygons) will be converted to a 'point' at the center instead before further processing - */ - wayHandling: number = 0; - - static WAYHANDLING_DEFAULT = 0; - static WAYHANDLING_CENTER_ONLY = 1; - static WAYHANDLING_CENTER_AND_WAY = 2; - - constructor(id: string, options: { - name: string | Translation, - description: string | Translation, - presets: Preset[], - icon: string, - minzoom: number, - overpassFilter: TagsFilter, - title?: TagDependantUIElementConstructor, - elementsToShow?: TagDependantUIElementConstructor[], - maxAllowedOverlapPercentage?: number, - wayHandling?: number, - widenFactor?: number, - style?: (tags: any) => { - color: string, - icon: any - } - } = undefined) { - this.id = id; - if (options === undefined) { - return; - } - this.name = options.name; - this.description = options.description; - this.maxAllowedOverlapPercentage = options.maxAllowedOverlapPercentage ?? 0; - this.presets = options.presets; - this.icon = options.icon; - this.minzoom = options.minzoom; - this.overpassFilter = options.overpassFilter; - this.title = options.title; - this.elementsToShow = options.elementsToShow; - this.style = options.style; - this.wayHandling = options.wayHandling ?? LayerDefinition.WAYHANDLING_DEFAULT; - } - -} \ No newline at end of file diff --git a/Customizations/Layout.ts b/Customizations/Layout.ts index 46252c6..8c7886b 100644 --- a/Customizations/Layout.ts +++ b/Customizations/Layout.ts @@ -1,9 +1,12 @@ -import {LayerDefinition} from "./LayerDefinition"; import {UIElement} from "../UI/UIElement"; import Translations from "../UI/i18n/Translations"; import Combine from "../UI/Base/Combine"; import State from "../State"; import Translation from "../UI/i18n/Translation"; +import LayerConfig from "./JSON/LayerConfig"; +import {LayoutConfigJson} from "./JSON/LayoutConfigJson"; +import TagRenderingConfig from "./JSON/TagRenderingConfig"; +import {FromJSON} from "./JSON/FromJSON"; /** * A layout is a collection of settings of the global view (thus: welcome text, title, selection of layers). @@ -23,7 +26,7 @@ export class Layout { */ public customCss: string = undefined; - public layers: (LayerDefinition | string)[]; + public layers: LayerConfig[]; public welcomeMessage: UIElement; public gettingStartedPlzLogin: UIElement; public welcomeBackMessage: UIElement; @@ -52,11 +55,51 @@ export class Layout { public widenFactor: number = 0.07; public defaultBackground: string = "osm"; + public static LayoutFromJSON(json: LayoutConfigJson, sharedLayers): Layout { + const tr = FromJSON.Translation; + const layers = json.layers.map(jsonLayer => { + if(typeof jsonLayer === "string"){ + return sharedLayers[jsonLayer]; + } + return new LayerConfig(jsonLayer, "theme."+json.id); + }); + const roaming: TagRenderingConfig[] = json.roamingRenderings?.map((tr, i) => + new TagRenderingConfig(tr, `theme.${json.id}.roamingRendering[${i}]`)) ?? []; + for (const layer of layers) { + layer.tagRenderings.push(...roaming); + } + + const layout = new Layout( + json.id, + typeof (json.language) === "string" ? [json.language] : json.language, + tr(json.title ?? "Title not defined"), + layers, + json.startZoom, + json.startLat, + json.startLon, + new Combine(["

", tr(json.title), "

", tr(json.description)]), + undefined, + undefined, + tr(json.descriptionTail) + + ); + + layout.defaultBackground = json.defaultBackgroundId ?? "osm"; + layout.widenFactor = json.widenFactor ?? 0.07; + layout.icon = json.icon; + layout.maintainer = json.maintainer; + layout.version = json.version; + layout.socialImage = json.socialImage; + layout.description = tr(json.shortDescription) ?? tr(json.description)?.FirstSentence(); + layout.changesetMessage = json.changesetmessage; + return layout; + } + constructor( id: string, supportedLanguages: string[], title: Translation | string, - layers: (LayerDefinition | string)[], + layers: LayerConfig[], startzoom: number, startLat: number, startLon: number, diff --git a/Customizations/OnlyShowIf.ts b/Customizations/OnlyShowIf.ts deleted file mode 100644 index a9c4a43..0000000 --- a/Customizations/OnlyShowIf.ts +++ /dev/null @@ -1,98 +0,0 @@ -import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor"; -import {TagsFilter, TagUtils} from "../Logic/Tags"; -import {UIElement} from "../UI/UIElement"; -import {UIEventSource} from "../Logic/UIEventSource"; -import Translation from "../UI/i18n/Translation"; - -/** - * Wrapper around another TagDependandElement, which only shows if the filters match - */ -export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{ - private readonly _tagsFilter: TagsFilter; - private readonly _embedded: TagDependantUIElementConstructor; - - constructor(tagsFilter: TagsFilter, embedded: TagDependantUIElementConstructor) { - this._tagsFilter = tagsFilter; - this._embedded = embedded; - } - - construct(tags: UIEventSource): TagDependantUIElement { - return new OnlyShowIf(tags, - this._embedded.construct(tags), - this._tagsFilter); - } - - IsKnown(properties: any): boolean { - if(!this.Matches(properties)){ - return true; - } - return this._embedded.IsKnown(properties); - } - - IsQuestioning(properties: any): boolean { - if(!this.Matches(properties)){ - return false; - } - return this._embedded.IsQuestioning(properties); - } - - GetContent(tags: any): Translation { - if(!this.IsKnown(tags)){ - return undefined; - } - return this._embedded.GetContent(tags); - } - - private Matches(properties: any) : boolean{ - return this._tagsFilter.matches(TagUtils.proprtiesToKV(properties)); - } - -} - -class OnlyShowIf extends UIElement implements TagDependantUIElement { - private readonly _embedded: TagDependantUIElement; - private readonly _filter: TagsFilter; - - constructor( - tags: UIEventSource, - embedded: TagDependantUIElement, - filter: TagsFilter) { - super(tags); - this._filter = filter; - this._embedded = embedded; - } - - private Matches() : boolean{ - return this._filter.matches(TagUtils.proprtiesToKV(this._source.data)); - } - - InnerRender(): string { - if (this.Matches()) { - return this._embedded.Render(); - } else { - return ""; - } - } - - IsKnown(): boolean { - if(!this.Matches()){ - return false; - } - return this._embedded.IsKnown(); - } - - IsSkipped(): boolean { - if(!this.Matches()){ - return false; - } - return this._embedded.IsSkipped(); - } - - IsQuestioning(): boolean { - if(!this.Matches()){ - return false; - } - return this._embedded.IsQuestioning(); - } - -} \ No newline at end of file diff --git a/Customizations/Questions/OsmLink.ts b/Customizations/Questions/OsmLink.ts deleted file mode 100644 index d70fad0..0000000 --- a/Customizations/Questions/OsmLink.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {Img} from "../../UI/Img"; -import {RegexTag} from "../../Logic/Tags"; -import {TagRenderingOptions} from "../TagRenderingOptions"; - - -export class OsmLink extends TagRenderingOptions { - - static options = { - freeform: { - key: "id", - template: "$$$", - renderTemplate: - "" + - Img.osmAbstractLogo + - "", - placeholder: "", - }, - mappings: [ - {k: new RegexTag("id", /node\/-.+/), txt: ""} - ] - - } - - constructor() { - super(OsmLink.options); - } - - -} \ No newline at end of file diff --git a/Customizations/Questions/WikipediaLink.ts b/Customizations/Questions/WikipediaLink.ts deleted file mode 100644 index 8342afa..0000000 --- a/Customizations/Questions/WikipediaLink.ts +++ /dev/null @@ -1,50 +0,0 @@ -import {TagRenderingOptions} from "../TagRenderingOptions"; - - -export class WikipediaLink extends TagRenderingOptions { - - private static FixLink(value: string): string { - if (value === undefined) { - return undefined; - } - // @ts-ignore - if (value.startsWith("https")) { - return value; - } else { - - const splitted = value.split(":"); - const language = splitted[0]; - splitted.shift(); - const page = splitted.join(":"); - return 'https://' + language + '.wikipedia.org/wiki/' + page; - } - } - - static options = { - priority: 10, - // question: "Wat is het overeenstemmende wkipedia-artikel?", - tagsPreprocessor: (tags) => { - if (tags.wikipedia !== undefined) { - tags.wikipedia = WikipediaLink.FixLink(tags.wikipedia); - } - }, - freeform: { - key: "wikipedia", - template: "$$$", - renderTemplate: - "" + - "wikipedia" + - "", - - placeholder: "" - - }, - - } - - constructor() { - super(WikipediaLink.options); - } - - -} \ No newline at end of file diff --git a/Customizations/SharedLayers.ts b/Customizations/SharedLayers.ts new file mode 100644 index 0000000..0f140b0 --- /dev/null +++ b/Customizations/SharedLayers.ts @@ -0,0 +1,50 @@ + +import * as drinkingWater from "../assets/layers/drinking_water/drinking_water.json"; +import * as ghostbikes from "../assets/layers/ghost_bike/ghost_bike.json" +import * as viewpoint from "../assets/layers/viewpoint/viewpoint.json" +import * as bike_parking from "../assets/layers/bike_parking/bike_parking.json" +import * as bike_repair_station from "../assets/layers/bike_repair_station/bike_repair_station.json" +import * as birdhides from "../assets/layers/bird_hide/birdhides.json" +import * as nature_reserve from "../assets/layers/nature_reserve/nature_reserve.json" +import * as bike_cafes from "../assets/layers/bike_cafe/bike_cafes.json" +import * as bike_monitoring_station from "../assets/layers/bike_monitoring_station/bike_monitoring_station.json" +import * as cycling_themed_objects from "../assets/layers/cycling_themed_object/cycling_themed_objects.json" +import * as bike_shops from "../assets/layers/bike_shop/bike_shop.json" +import * as maps from "../assets/layers/maps/maps.json" +import * as information_boards from "../assets/layers/information_board/information_board.json" +import LayerConfig from "./JSON/LayerConfig"; + +export default class SharedLayers { + + + + + public static sharedLayers: Map = SharedLayers.getSharedLayers(); + + private static getSharedLayers(){ + const sharedLayersList = [ + new LayerConfig(drinkingWater, "shared_layers"), + new LayerConfig(ghostbikes, "shared_layers"), + new LayerConfig(viewpoint, "shared_layers"), + new LayerConfig(bike_parking, "shared_layers"), + new LayerConfig(bike_repair_station, "shared_layers"), + new LayerConfig(bike_monitoring_station, "shared_layers"), + new LayerConfig(birdhides, "shared_layers"), + new LayerConfig(nature_reserve, "shared_layers"), + new LayerConfig(bike_cafes, "shared_layers"), + new LayerConfig(cycling_themed_objects, "shared_layers"), + new LayerConfig(bike_shops, "shared_layers"), + new LayerConfig(maps, "shared_layers"), + new LayerConfig(information_boards, "shared_layers") + ]; + + const sharedLayers = new Map(); + for (const layer of sharedLayersList) { + sharedLayers.set(layer.id, layer); + sharedLayers[layer.id] = layer; + } + return sharedLayers; + } + + +} \ No newline at end of file diff --git a/Customizations/SharedTagRenderings.ts b/Customizations/SharedTagRenderings.ts new file mode 100644 index 0000000..9cd62ee --- /dev/null +++ b/Customizations/SharedTagRenderings.ts @@ -0,0 +1,20 @@ +import * as questions from "../assets/questions/questions.json"; +import TagRenderingConfig from "./JSON/TagRenderingConfig"; + +export default class SharedTagRenderings { + + public static SharedTagRendering = SharedTagRenderings.generatedSharedFields(); + + private static generatedSharedFields() { + const dict = {} + for (const key in questions) { + try { + dict[key] = new TagRenderingConfig(questions[key]) + } catch (e) { + console.error("COULD NOT PARSE", key, " FROM QUESTIONS:", e) + } + } + return dict; + } + +} diff --git a/Customizations/StreetWidth/StreetWidth.ts b/Customizations/StreetWidth/StreetWidth.ts deleted file mode 100644 index b58d2a7..0000000 --- a/Customizations/StreetWidth/StreetWidth.ts +++ /dev/null @@ -1,107 +0,0 @@ -import {Layout} from "../Layout"; -import {Widths} from "./Widths"; - -export class StreetWidth extends Layout{ - - private static meetMethode = ` - - - We meten de ruimte die gedeeld wordt door auto's, fietsers en -in sommige gevallen- voetgangers. - We meten dus van _verhoogde_ stoeprand tot stoeprand omdat dit de ruimte is die wordt gedeeld door auto's en fietsers. - Daarnaast zoeken we ook een smaller stuk van de weg waar dat smallere stuk toch minstens 2m zo smal blijft. - Een obstakel (zoals een trap, elektriciteitkast) negeren we omdat dit de meting te fel beinvloed. - - In een aantal straten is er geen verhoogde stoep. In dit geval meten we van muur tot muur, omdat dit de gedeelde ruimte is. - We geven ook altijd een aanduiding of er al dan niet een voetpad aanwezig (en aan welke kant indien er maar één is), want indien er geen is heeft de voetganger ook ruimte nodig. - - (In sommige straten zijn er wel 'voetpadsuggesties' door een meter in andere kasseien te leggen, bv. met een kleurtje. Dit rekenen we niet als voetpad. - - Ook het parkeren van auto's wordt opgemeten. - Als er een parallele parkeerstrook is, dan duiden we dit aan en nemen we de parkeerstrook mee in de straatbreedte. - Als er een witte lijn is, dan negeren we dit. Deze witte lijnen duiden immers vaak een té smalle parkeerplaats aan - bv. 1.6m. - Een auto is tegenwoordig al snel 1.8m tot zelfs 2.0m, dus dan springt die auto gemakkelijk 20 tot 30cm uit op de baan. - - Staan de auto's schuin geparkeerd of dwarsgeparkeerd? - Ook hier kan men het argument maken dat auto's er soms overspringen, maar dat is hier te variabel om in kaart te brengen. - Daarnaast gebeurt het minder dat auto's overspringen én zijn deze gevallen relatief zeldzaam in de binnenstad. - - Concreet: - - Sla de 'parkeren'-vraag over - - Maak een foto en stuur die door naar Pieter (+ vermelding straatnaam of dergelijke) - - Meet de breedte vanaf de afbakening van de parkeerstrook. - - Ook bij andere lastige gevallen: maak een foto en vraag Pieter - - - - Instellen van de lasermeter - =========================== - - 1) Zet de lasermeter aan met de rode knop - 2) Het icoontje linksboven indiceert vanaf waar de laser meet - de voorkant of de achterkant van het apparaatje. - Dit kan aangepast worden met het knopje links-onderaan. - Kies wat je het liefste hebt - 3) Het icoontje bovenaan-midden indiceert de stand van de laser: directe afstand, of afstand over de grond. - Dit MOET een driehoekje tonen. - Indien niet: duw op het knopje links-bovenaan totdat dit een rechte driehoek toont - 4) Duw op de rode knop. Het lasertje gaat branden - 5) Hou het meetbakje boven de stoeprand (met de juiste rand), richt de laser op de andere stoep - 6) Duw opnieuw op de rode knop om te meten (de laser flikkert en gaat uit) - 7) Lees de afstand af op het scherm. Let op: in 'hoekstand' is dit niet de onderste waarde, maar die er net boven. - - ` - - - - - - - - constructor() { - super( "width", - ["nl"], - "Straatbreedtes in Brugge", - [new Widths( - 2, - 1.5, - 0.75 - - )], - 15, - 51.20875, - 3.22435, - "

De straat is opgebruikt

" + - "

Er is steeds meer druk op de openbare ruimte. Voetgangers, fietsers, steps, auto's, bussen, bestelwagens, buggies, cargobikes, ... willen allemaal hun deel van de openbare ruimte.

" + - "" + - "

In deze studie nemen we Brugge onder de loep en kijken we hoe breed elke straat is én hoe breed elke straat zou moeten zijn voor een veilig én vlot verkeer.

" + - "

Legende

" + - "    Straat te smal voor veilig verkeer
"+ - "    Straat is breed genoeg veilig verkeer
"+ - "    Straat zonder voetpad, te smal als ook voetgangers plaats krijgen
"+ - "    Woonerf, autoluw, autoloos of enkel plaatselijk verkeer
" + - "
" + - "
" + - "Een gestippelde lijn is een straat waar ook voor fietsers éénrichtingsverkeer geldt.
" + - "Klik op een straat om meer informatie te zien."+ - "

Hoe gaan we verder?

" + - "Verschillende ingrepen kunnen de stad teruggeven aan de inwoners en de stad leefbaarder en levendiger maken.
" + - "Denk aan:" + - "
    " + - "
  • De autovrije zone's uitbreiden
  • " + - "
  • De binnenstad fietszone maken
  • " + - "
  • Het aantal woonerven uitbreiden
  • " + - "
  • Grotere auto's meer belasten - ze nemen immers meer parkeerruimte in.
  • " + - "
  • Laat toeristen verplicht parkeren onder het zand; een (fiets)taxi kan hen naar hun hotel brengen
  • " + - "
  • Voorzie in elke straat enkele parkeerplaatsen voor kortparkeren. Zo kunnen leveringen, iemand afzetten,... gebeuren zonder op het voetpad en fietspad te parkeren
  • " + - "
"); - this.icon = "./assets/bug.svg"; - this.enableSearch = false; - this.enableUserBadge = false; - this.enableAdd = false; - this.hideFromOverview = true; - this.enableMoreQuests = false; - this.enableShareScreen = false; - this.defaultBackground = "Stadia.AlidadeSmoothDark" - this.enableBackgroundLayers = false; - } -} \ No newline at end of file diff --git a/Customizations/StreetWidth/Widths.ts b/Customizations/StreetWidth/Widths.ts deleted file mode 100644 index c0b23df..0000000 --- a/Customizations/StreetWidth/Widths.ts +++ /dev/null @@ -1,312 +0,0 @@ -import {LayerDefinition} from "../LayerDefinition"; -import {And, Or, RegexTag, Tag} from "../../Logic/Tags"; -import {TagRenderingOptions} from "../TagRenderingOptions"; -import {FromJSON} from "../JSON/FromJSON"; - -export class Widths extends LayerDefinition { - - private readonly cyclistWidth: number; - private readonly carWidth: number; - private readonly pedestrianWidth: number; - - private readonly _bothSideParking = new Tag("parking:lane:both", "parallel"); - private readonly _noSideParking = new Tag("parking:lane:both", "no_parking"); - private readonly _otherParkingMode = - new Or([ - new Tag("parking:lane:both", "perpendicular"), - new Tag("parking:lane:left", "perpendicular"), - new Tag("parking:lane:right", "perpendicular"), - new Tag("parking:lane:both", "diagonal"), - new Tag("parking:lane:left", "diagonal"), - new Tag("parking:lane:right", "diagonal"), - ]) - - - private readonly _leftSideParking = - new And([new Tag("parking:lane:left", "parallel"), new Tag("parking:lane:right", "no_parking")]); - private readonly _rightSideParking = - new And([new Tag("parking:lane:right", "parallel"), new Tag("parking:lane:left", "no_parking")]); - - - private _sidewalkBoth = new Tag("sidewalk", "both"); - private _sidewalkLeft = new Tag("sidewalk", "left"); - private _sidewalkRight = new Tag("sidewalk", "right"); - private _sidewalkNone = new Tag("sidewalk", "none"); - - - private readonly _oneSideParking = new Or([this._leftSideParking, this._rightSideParking]); - - private readonly _notCarfree = - FromJSON.Tag({"and":[ - "highway!~pedestrian|living_street", - "access!~destination", - "motor_vehicle!~destination|no" - ]}); - - private calcProps(properties) { - let parkingStateKnown = true; - let parallelParkingCount = 0; - - if (this._oneSideParking.matchesProperties(properties)) { - parallelParkingCount = 1; - } else if (this._bothSideParking.matchesProperties(properties)) { - parallelParkingCount = 2; - } else if (this._noSideParking.matchesProperties(properties)) { - parallelParkingCount = 0; - } else if (this._otherParkingMode.matchesProperties(properties)) { - parallelParkingCount = 0; - } else { - parkingStateKnown = false; - console.log("No parking data for ", properties.name, properties.id, properties) - } - - - let pedestrianFlowNeeded; - if (this._sidewalkBoth.matchesProperties(properties)) { - pedestrianFlowNeeded = 0; - } else if (this._sidewalkNone.matchesProperties(properties)) { - pedestrianFlowNeeded = 2; - } else if (this._sidewalkLeft.matchesProperties(properties) || this._sidewalkRight.matches(properties)) { - pedestrianFlowNeeded = 1; - } else { - pedestrianFlowNeeded = -1; - } - - - let onewayCar = properties.oneway === "yes"; - let onewayBike = properties["oneway:bicycle"] === "yes" || - (onewayCar && properties["oneway:bicycle"] === undefined) - - let cyclingAllowed = - !(properties.bicycle === "use_sidepath" - || properties.bicycle === "no"); - - let carWidth = (onewayCar ? 1 : 2) * this.carWidth; - let cyclistWidth = 0; - if (cyclingAllowed) { - cyclistWidth = (onewayBike ? 1 : 2) * this.cyclistWidth; - } - - const width = parseFloat(properties["width:carriageway"]); - - - const targetWidthIgnoringPedestrians = - carWidth + - cyclistWidth + - parallelParkingCount * this.carWidth; - - const targetWidth = targetWidthIgnoringPedestrians + Math.max(0, pedestrianFlowNeeded) * this.pedestrianWidth; - - return { - parkingLanes: parallelParkingCount, - parkingStateKnown: parkingStateKnown, - width: width, - targetWidth: targetWidth, - targetWidthIgnoringPedestrians: targetWidthIgnoringPedestrians, - onewayBike: onewayBike, - pedestrianFlowNeeded: pedestrianFlowNeeded, - cyclingAllowed: cyclingAllowed - } - } - - - constructor(carWidth: number, - cyclistWidth: number, - pedestrianWidth: number) { - super("width"); - this.carWidth = carWidth; - this.cyclistWidth = cyclistWidth; - this.pedestrianWidth = pedestrianWidth; - this.minzoom = 12; - - function r(n: number) { - const pre = Math.floor(n); - const post = Math.floor((n * 10) % 10); - return "" + pre + "." + post; - } - - this.name = "widths"; - this.overpassFilter = new RegexTag("width:carriageway", /.*/); - - this.title = new TagRenderingOptions({ - freeform: { - renderTemplate: "{name}", - template: "$$$", - key: "name" - } - }) - - const self = this; - this.style = (properties) => { - - let c = "#f00"; - - - const props = self.calcProps(properties); - if (props.width >= props.targetWidthIgnoringPedestrians) { - c = "#fa0" - } - if (props.width >= props.targetWidth || !props.cyclingAllowed) { - c = "#0c0"; - } - - if (!props.parkingStateKnown && properties["note:width:carriageway"] === undefined) { - c = "#f0f" - } - - if (!this._notCarfree.matchesProperties(properties)) { - c = "#aaa"; - } - - - // Mark probably wrong data - if (props.width > 15) { - c = "#f0f" - } - - let dashArray = undefined; - if (props.onewayBike) { - dashArray = [5, 6] - } - return { - icon: null, - color: c, - weight: 5, - dashArray: dashArray - } - } - - this.elementsToShow = [ - new TagRenderingOptions({ - mappings: [ - { - k: this._bothSideParking, - txt: "Auto's kunnen langs beide zijden parkeren.Dit gebruikt " + r(this.carWidth * 2) + "m
" - }, - { - k: this._oneSideParking, - txt: "Auto's kunnen langs één kant parkeren.
Dit gebruikt " + r(this.carWidth) + "m
" - }, - { - k: this._otherParkingMode, - txt: "Deze straat heeft dwarsparkeren of diagonaalparkeren aan minstens één zijde. Deze parkeerruimte is niet opgenomen in de straatbreedte." - }, - {k: this._noSideParking, txt: "Auto's mogen hier niet parkeren"}, - ], - freeform: { - key: "note:width:carriageway", - renderTemplate: "{note:width:carriageway}", - template: "$$$", - } - }).OnlyShowIf(this._notCarfree), - - - new TagRenderingOptions({ - mappings: [ - { - k: this._sidewalkNone, - txt: "Deze straat heeft geen voetpaden. Voetgangers hebben hier " + r(this.pedestrianWidth * 2) + "m nodig" - }, - { - k: new Or([this._sidewalkLeft, this._sidewalkRight]), - txt: "Deze straat heeft een voetpad aan één kant. Voetgangers hebben hier " + r(this.pedestrianWidth) + "m nodig" - }, - {k: this._sidewalkBoth, txt: "Deze straat heeft voetpad aan beide zijden."}, - ], - freeform: { - key: "note:width:carriageway", - renderTemplate: "{note:width:carriageway}", - template: "$$$", - } - }).OnlyShowIf(this._notCarfree), - - - new TagRenderingOptions({ - mappings: [ - { - k: new Tag("bicycle", "use_sidepath"), - txt: "Er is een afgescheiden, verplicht te gebruiken fietspad. Fietsen op dit wegsegment hoeft dus niet" - }, - { - k: new Tag("bicycle", "no"), - txt: "Fietsen is hier niet toegestaan" - }, - { - k: new Tag("oneway:bicycle", "yes"), - txt: "Eenrichtingsverkeer, óók voor fietsers. Dit gebruikt " + r(this.carWidth + this.cyclistWidth) + "m" - }, - { - k: new And([new Tag("oneway", "yes"), new Tag("oneway:bicycle", "no")]), - txt: "Tweerichtingverkeer voor fietsers, eenrichting voor auto's Dit gebruikt " + r(this.carWidth + 2 * this.cyclistWidth) + "m" - }, - { - k: new Tag("oneway", "yes"), - txt: "Eenrichtingsverkeer voor iedereen. Dit gebruikt " + (this.carWidth + this.cyclistWidth) + "m" - }, - { - k: null, - txt: "Tweerichtingsverkeer voor iedereen. Dit gebruikt " + r(2 * this.carWidth + 2 * this.cyclistWidth) + "m" - } - ] - }).OnlyShowIf(this._notCarfree), - - new TagRenderingOptions( - { - tagsPreprocessor: (tags) => { - const props = self.calcProps(tags); - tags.targetWidth = r(props.targetWidth); - tags.short = ""; - if (props.width < props.targetWidth) { - tags.short = `Dit is ${r(props.targetWidth - props.width)}m te weinig` - } - console.log("SHORT", tags.short) - }, - mappings: [ - { - k: null, - txt: "De totale nodige ruimte voor vlot en veilig verkeer is dus {targetWidth}m
{short}" - } - ] - } - ).OnlyShowIf(this._notCarfree), - - - new TagRenderingOptions({ - mappings: [ - {k:new Tag("highway","living_street"),txt: "Dit is een woonerf"}, - {k:new Tag("highway","pedestrian"),txt: "Deze weg is autovrij"} - ] - }), - - new TagRenderingOptions({ - mappings: [ - { - k: new Tag("sidewalk", "none"), - txt: "De afstand van huis tot huis is {width:carriageway}m" - }, - { - k: new Tag("sidewalk", "left"), - txt: "De afstand van huis tot voetpad is {width:carriageway}m" - }, - { - k: new Tag("sidewalk", "right"), - txt: "De afstand van huis tot voetpad is {width:carriageway}m" - }, - { - k: new Tag("sidewalk", "both"), - txt: "De afstand van voetpad tot voetpad is {width:carriageway}m" - }, - { - k: new Tag("sidewalk", ""), - txt: "De straatbreedte is {width:carriageway}m" - } - - ] - }) - - - ] - - } - -} \ No newline at end of file diff --git a/Customizations/TagRenderingOptions.ts b/Customizations/TagRenderingOptions.ts deleted file mode 100644 index 4081d5e..0000000 --- a/Customizations/TagRenderingOptions.ts +++ /dev/null @@ -1,148 +0,0 @@ -import {TagDependantUIElement, TagDependantUIElementConstructor} from "./UIElementConstructor"; -import {TagsFilter, TagUtils} from "../Logic/Tags"; -import {OnlyShowIfConstructor} from "./OnlyShowIf"; -import {UIEventSource} from "../Logic/UIEventSource"; -import Translation from "../UI/i18n/Translation"; -import Translations from "../UI/i18n/Translations"; - -export class TagRenderingOptions implements TagDependantUIElementConstructor { - - /** - * Notes: by not giving a 'question', one disables the question form alltogether - */ - public options: { - question?: string | Translation; - freeform?: { - key: string; - tagsPreprocessor?: (tags: any) => any; - template: string | Translation; - renderTemplate: string | Translation; - placeholder?: string | Translation; - extraTags?: TagsFilter - }; - multiAnswer?: boolean, - mappings?: { k: TagsFilter; txt: string | Translation; substitute?: boolean, hideInAnwser?: boolean }[] - }; - - constructor(options: { - - - /** - * This is the string that is shown in the popup if this tag is missing. - * - * If 'question' is undefined, then the question is never asked at all - * If the question is "" (empty string) then the question is - */ - question?: Translation | string, - - /** - * What is the priority of the question. - * By default, in the popup of a feature, only one question is shown at the same time. If multiple questions are unanswered, the question with the highest priority is asked first - */ - priority?: number, - - - /** - * Mappings convert a well-known tag combination into a user friendly text. - * It converts e.g. 'access=yes' into 'this area can be accessed' - * - * If there are multiple tags that should be matched, And can be used. All tags in AND will be added when the question is picked (and the corresponding text will only be shown if all tags are present). - * If AND is used, it is best practice to make sure every used tag is in every option (with empty string) to erase extra tags. - * - * If a 'k' is null, then this one is shown by default. It can be used to force a default value, e.g. to show that the name of a POI is not (yet) known . - * A mapping where 'k' is null will not be shown as option in the radio buttons. - * - * - */ - mappings?: { k: TagsFilter, txt: Translation | string, priority?: number, substitute?: boolean, hideInAnswer?: boolean }[], - - /** - * If true, use checkboxes to answer instead of radiobuttons - */ - multiAnswer?: boolean, - - /** - * If one wants to render a freeform tag (thus no predefined key/values) or if there are a few well-known tags with a freeform object, - * use this. - * In the question, it'll offer a textfield - */ - freeform?: { - key: string, - template: string | Translation, - renderTemplate: string | Translation - placeholder?: string | Translation, - extraTags?: TagsFilter, - }, - - - /** - * In some very rare cases, tags have to be rewritten before displaying - * This function can be used for that. - * This function is ran on a _copy_ of the original properties - */ - tagsPreprocessor?: ((tags: any) => void) - }) { - this.options = options; - } - - OnlyShowIf(tagsFilter: TagsFilter): TagDependantUIElementConstructor { - return new OnlyShowIfConstructor(tagsFilter, this); - } - - - IsQuestioning(tags: any): boolean { - const tagsKV = TagUtils.proprtiesToKV(tags); - - for (const oneOnOneElement of this.options.mappings ?? []) { - if (oneOnOneElement.k === null || oneOnOneElement.k.matches(tagsKV)) { - return false; - } - } - if (this.options.freeform !== undefined && tags[this.options.freeform.key] !== undefined) { - return false; - } - return this.options.question !== undefined; - } - - GetContent(tags: any): Translation { - const tagsKV = TagUtils.proprtiesToKV(tags); - - for (const oneOnOneElement of this.options.mappings ?? []) { - if (oneOnOneElement.k === null || oneOnOneElement.k.matches(tagsKV)) { - return Translations.WT(oneOnOneElement.txt); - } - } - if (this.options.freeform !== undefined) { - let template = Translations.WT(this.options.freeform.renderTemplate); - return template.Subs(tags); - } - - console.warn("No content defined for", tags, " with mapping", this); - return undefined; - } - - - public static tagRendering: (tags: UIEventSource, - options: { - priority?: number; - question?: string | Translation; - freeform?: { - key: string; - tagsPreprocessor?: (tags: any) => any; - template: string | Translation; - renderTemplate: string | Translation; - placeholder?: string | Translation; extraTags?: TagsFilter - }, - multiAnswer?: boolean, - mappings?: { k: TagsFilter; txt: string | Translation; priority?: number; substitute?: boolean, hideInAnswer?: boolean }[] - }) => TagDependantUIElement; - - construct(tags: UIEventSource): TagDependantUIElement { - return TagRenderingOptions.tagRendering(tags, this.options); - } - - IsKnown(properties: any): boolean { - return !this.IsQuestioning(properties); - } - -} \ No newline at end of file diff --git a/Customizations/UIElementConstructor.ts b/Customizations/UIElementConstructor.ts deleted file mode 100644 index 64b55ae..0000000 --- a/Customizations/UIElementConstructor.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {UIElement} from "../UI/UIElement"; -import {UIEventSource} from "../Logic/UIEventSource"; -import Translation from "../UI/i18n/Translation"; - -export interface TagDependantUIElementConstructor { - - construct(tags: UIEventSource): TagDependantUIElement; - IsKnown(properties: any): boolean; - IsQuestioning(properties: any): boolean; - GetContent(tags: any): Translation; - -} - -export abstract class TagDependantUIElement extends UIElement { - - abstract IsKnown(): boolean; - - abstract IsQuestioning(): boolean; - - abstract IsSkipped() : boolean; -} \ No newline at end of file diff --git a/InitUiElements.ts b/InitUiElements.ts index 9baf444..9e97780 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -11,9 +11,7 @@ import {Basemap} from "./Logic/Leaflet/Basemap"; import State from "./State"; import {WelcomeMessage} from "./UI/WelcomeMessage"; import {Img} from "./UI/Img"; -import {DropDown} from "./UI/Input/DropDown"; import {LayerSelection} from "./UI/LayerSelection"; -import {Preset} from "./Customizations/LayerDefinition"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; import {UpdateFromOverpass} from "./Logic/UpdateFromOverpass"; import {UIEventSource} from "./Logic/UIEventSource"; @@ -37,6 +35,7 @@ import {Utils} from "./Utils"; import BackgroundSelector from "./UI/BackgroundSelector"; import AvailableBaseLayers from "./Logic/AvailableBaseLayers"; import {FeatureInfoBox} from "./UI/Popup/FeatureInfoBox"; +import SharedLayers from "./Customizations/SharedLayers"; export class InitUiElements { @@ -162,15 +161,14 @@ export class InitUiElements { if (typeof layer === "string") { continue; } - const applicable = layer.overpassFilter.matches(TagUtils.proprtiesToKV(data)); + const applicable = layer.overpassTags.matches(TagUtils.proprtiesToKV(data)); if (applicable) { // This layer is the layer that gives the questions const featureBox = new FeatureInfoBox( feature.feature, State.state.allElements.getElement(data.id), - layer.title, - layer.elementsToShow, + layer ); State.state.fullScreenMessage.setData(featureBox); @@ -215,6 +213,10 @@ export class InitUiElements { } + public static FromBase64(layoutFromBase64: string): Layout { + return Layout.LayoutFromJSON(JSON.parse(atob(layoutFromBase64)), SharedLayers.sharedLayers); + } + static LoadLayoutFromHash(userLayoutParam: UIEventSource) { try { @@ -235,7 +237,7 @@ export class InitUiElements { hashFromLocalStorage.setData(hash); dedicatedHashFromLocalStorage.setData(hash); } - const layoutToUse = FromJSON.FromBase64(hash); + const layoutToUse = InitUiElements.FromBase64(hash); userLayoutParam.setData(layoutToUse.id); return layoutToUse; } catch (e) { @@ -338,18 +340,6 @@ export class InitUiElements { } - static CreateLanguagePicker(label: string | UIElement = "") { - - if (State.state.layoutToUse.data.supportedLanguages.length <= 1) { - return undefined; - } - - return new DropDown(label, State.state.layoutToUse.data.supportedLanguages.map(lang => { - return {value: lang, shown: lang} - } - ), Locale.language); - } - private static GenerateLayerControlPanel() { @@ -476,7 +466,6 @@ export class InitUiElements { static InitLayers() { const flayers: FilteredLayer[] = [] - const presets: Preset[] = []; const state = State.state; @@ -491,27 +480,10 @@ export class InitUiElements { return new FeatureInfoBox( feature, tagsES, - layer.title, - layer.elementsToShow, + layer, ) }; - for (const preset of layer.presets ?? []) { - - if (preset.icon === undefined) { - const tags = {}; - for (const tag of preset.tags) { - const k = tag.key; - if (typeof (k) === "string") { - tags[k] = tag.value; - } - } - preset.icon = layer.style(tags)?.icon?.iconUrl; - } - - presets.push(preset); - } - const flayer: FilteredLayer = FilteredLayer.fromDefinition(layer, generateInfo); flayers.push(flayer); @@ -523,8 +495,6 @@ export class InitUiElements { } State.state.filteredLayers.setData(flayers); - State.state.presets.setData(presets); - } } \ No newline at end of file diff --git a/Logic/FilteredLayer.ts b/Logic/FilteredLayer.ts index 52eb462..0ee5f8d 100644 --- a/Logic/FilteredLayer.ts +++ b/Logic/FilteredLayer.ts @@ -4,10 +4,9 @@ import * as L from "leaflet" import {Layer} from "leaflet" import {GeoOperations} from "./GeoOperations"; import {UIElement} from "../UI/UIElement"; -import {LayerDefinition} from "../Customizations/LayerDefinition"; import State from "../State"; -import CodeGrid from "./Web/CodeGrid"; +import LayerConfig from "../Customizations/JSON/LayerConfig"; /*** * A filtered layer is a layer which offers a 'set-data' function @@ -23,11 +22,11 @@ export class FilteredLayer { public readonly name: string | UIElement; public readonly filters: TagsFilter; public readonly isDisplayed: UIEventSource = new UIEventSource(true); - private readonly combinedIsDisplayed : UIEventSource; - public readonly layerDef: LayerDefinition; + private readonly combinedIsDisplayed: UIEventSource; + public readonly layerDef: LayerConfig; private readonly _maxAllowedOverlap: number; - private readonly _style: (properties) => { color: string, weight?: number, icon: { iconUrl: string, iconSize?: [number, number], popupAnchor?: [number,number], iconAnchor?: [number,number] } }; + private readonly _style: (properties) => { color: string, weight?: number, icon: { iconUrl: string, iconSize?: [number, number], popupAnchor?: [number, number], iconAnchor?: [number, number] } }; /** The featurecollection from overpass @@ -46,7 +45,7 @@ export class FilteredLayer { constructor( - layerDef: LayerDefinition, + layerDef: LayerConfig, showOnPopup: ((tags: UIEventSource, feature: any) => UIElement) ) { this.layerDef = layerDef; @@ -54,22 +53,56 @@ export class FilteredLayer { this._wayHandling = layerDef.wayHandling; this._showOnPopup = showOnPopup; this._style = (tags) => { - if(layerDef.style === undefined){ - return {icon: {iconUrl: "./assets/bug.svg"}, color: "#000"}; - } - - const obj = layerDef.style(tags); - if(obj.weight && typeof (obj.weight) === "string"){ - obj.weight = Number(obj.weight);// Weight MUST be a number, otherwise leaflet does weird things. see https://github.com/Leaflet/Leaflet/issues/6075 - if(isNaN(obj.weight)){ - obj.weight = undefined; + + const iconUrl = layerDef.icon?.GetRenderValue(tags)?.txt ?? "./assets/bug.svg"; + const iconSize = (layerDef.iconSize?.GetRenderValue(tags)?.txt ?? "40,40,center").split(","); + + function num(str, deflt = 40) { + const n = Number(str); + if (isNaN(n)) { + return deflt; } + return n; } - return obj; + + const iconW = num(iconSize[0]); + const iconH = num(iconSize[1]); + const mode = iconSize[2] ?? "center" + + let anchorW = iconW / 2; + let anchorH = iconH / 2; + if (mode === "left") { + anchorW = 0; + } + if (mode === "right") { + anchorW = iconW; + } + + if (mode === "top") { + anchorH = 0; + } + if (mode === "bottom") { + anchorH = iconH; + } + + + const color = layerDef.color?.GetRenderValue(tags)?.txt ?? "#00f"; + let weight = num(layerDef.width?.GetRenderValue(tags)?.txt, 5); + return { + icon: + { + iconUrl: iconUrl, + iconSize: [iconW, iconH], + iconAnchor: [anchorW, anchorH], + popupAnchor: [0, 3 - anchorH] + }, + color: color, + weight: weight + }; }; this.name = name; - this.filters = layerDef.overpassFilter; - this._maxAllowedOverlap = layerDef.maxAllowedOverlapPercentage; + this.filters = layerDef.overpassTags; + this._maxAllowedOverlap = layerDef.hideUnderlayingFeaturesMinPercentage; const self = this; this.combinedIsDisplayed = this.isDisplayed.map(isDisplayed => { return isDisplayed && State.state.locationControl.data.zoom >= self.layerDef.minzoom @@ -111,9 +144,9 @@ export class FilteredLayer { const tags = TagUtils.proprtiesToKV(feature.properties); const centerPoint = GeoOperations.centerpoint(feature); if (feature.geometry.type !== "Point") { - if (this._wayHandling === LayerDefinition.WAYHANDLING_CENTER_AND_WAY) { + if (this._wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY) { selfFeatures.push(centerPoint); - } else if (this._wayHandling === LayerDefinition.WAYHANDLING_CENTER_ONLY) { + } else if (this._wayHandling === LayerConfig.WAYHANDLING_CENTER_ONLY) { feature = centerPoint; } } diff --git a/Logic/ImageSearcher.ts b/Logic/ImageSearcher.ts index 1d403e5..b210c25 100644 --- a/Logic/ImageSearcher.ts +++ b/Logic/ImageSearcher.ts @@ -103,7 +103,6 @@ export class ImageSearcher extends UIEventSource<{key: string, url: string}[]> { } private LoadImages(imagePrefix: string, loadAdditional: boolean): void { - console.log("Loading images from",this._tags) const imageTag = this._tags.data[imagePrefix]; if (imageTag !== undefined) { const bareImages = imageTag.split(";"); diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index 949b576..db305ac 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -6,18 +6,19 @@ import {OsmNode, OsmObject} from "./OsmObject"; import {And, Tag, TagsFilter} from "../Tags"; import State from "../../State"; import {Utils} from "../../Utils"; +import {UIEventSource} from "../UIEventSource"; export class Changes { private static _nextId = -1; // New assined ID's are negative - addTag(elementId: string, tagsFilter: TagsFilter) { + addTag(elementId: string, tagsFilter: TagsFilter, + tags?: UIEventSource) { const changes = this.tagToChange(tagsFilter); - if (changes.length == 0) { return; } - const eventSource = State.state.allElements.getElement(elementId); + const eventSource = tags ?? State.state?.allElements.getElement(elementId); const elementTags = eventSource.data; const pending : {elementId:string, key: string, value: string}[] = []; for (const change of changes) { diff --git a/Logic/Tags.ts b/Logic/Tags.ts index b7d54cb..fafd170 100644 --- a/Logic/Tags.ts +++ b/Logic/Tags.ts @@ -359,9 +359,10 @@ export class TagUtils { return new And([]); } const keyValues = {} // Map string -> string[] - tagsFilters = [...tagsFilters] + tagsFilters = [...tagsFilters] // copy all while (tagsFilters.length > 0) { - const tagsFilter = tagsFilters.pop(); + // Queue + const tagsFilter = tagsFilters.shift(); if (tagsFilter === undefined) { continue; @@ -388,7 +389,6 @@ export class TagUtils { for (const key in keyValues) { and.push(new Tag(key, Utils.Dedup(keyValues[key]).join(";"))); } - return new And(and); } diff --git a/Logic/UpdateFromOverpass.ts b/Logic/UpdateFromOverpass.ts index 7f37051..8cd6bbe 100644 --- a/Logic/UpdateFromOverpass.ts +++ b/Logic/UpdateFromOverpass.ts @@ -4,7 +4,6 @@ import {FilteredLayer} from "./FilteredLayer"; import {Bounds} from "./Bounds"; import {Overpass} from "./Osm/Overpass"; import State from "../State"; -import {LayerDefinition} from "../Customizations/LayerDefinition"; import MetaTagging from "./MetaTagging"; export class UpdateFromOverpass { @@ -34,7 +33,7 @@ export class UpdateFromOverpass { if(location?.zoom === undefined){ return false; } - let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => (layer as LayerDefinition).minzoom ?? 18)); + let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom ?? 18)); return location.zoom >= minzoom; }, [state.layoutToUse] ); @@ -80,7 +79,7 @@ export class UpdateFromOverpass { if (previouslyLoaded) { continue; } - filters.push(layer.overpassFilter); + filters.push(layer.overpassTags); } if (filters.length === 0) { return undefined; diff --git a/State.ts b/State.ts index c2fb00c..ac4c45a 100644 --- a/State.ts +++ b/State.ts @@ -1,7 +1,6 @@ import {UIElement} from "./UI/UIElement"; import {Layout} from "./Customizations/Layout"; import {Utils} from "./Utils"; -import {Preset} from "./Customizations/LayerDefinition"; import {ElementStorage} from "./Logic/ElementStorage"; import {Changes} from "./Logic/Osm/Changes"; import {OsmConnection} from "./Logic/Osm/OsmConnection"; @@ -70,7 +69,6 @@ export default class State { public filteredLayers: UIEventSource = new UIEventSource([]) - public presets: UIEventSource = new UIEventSource([]) /** * The message that should be shown at the center of the screen diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index 2967695..b6d0c1d 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -2,12 +2,11 @@ import {UIElement} from "../UIElement"; import {ImageSearcher} from "../../Logic/ImageSearcher"; import {SlideShow} from "./SlideShow"; import {UIEventSource} from "../../Logic/UIEventSource"; -import {TagDependantUIElement} from "../../Customizations/UIElementConstructor"; import Combine from "../Base/Combine"; import DeleteImage from "./DeleteImage"; -export class ImageCarousel extends TagDependantUIElement { +export class ImageCarousel extends UIElement{ public readonly slideshow: SlideShow; @@ -40,19 +39,4 @@ export class ImageCarousel extends TagDependantUIElement { IsKnown(): boolean { return true; } - - IsQuestioning(): boolean { - return false; - } - - IsSkipped(): boolean { - return false; - } - - Priority(): number { - return 0; - } - - - } \ No newline at end of file diff --git a/UI/Img.ts b/UI/Img.ts index 95ce591..e472611 100644 --- a/UI/Img.ts +++ b/UI/Img.ts @@ -17,16 +17,6 @@ export class Img { static readonly checkmark = ``; static readonly no_checkmark = ``; - static osmAbstractLogo: string = - ""; - - static closedFilterButton: string = ` diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index dafacd3..2c89ef6 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -49,6 +49,9 @@ export default class ValidatedTextField { ValidatedTextField.tp( "string", "A basic string"), + ValidatedTextField.tp( + "text", + "A string, but allows input of longer strings more comfortably (a text area)"), ValidatedTextField.tp( "date", "A date", @@ -171,6 +174,9 @@ export default class ValidatedTextField { return new DropDown("", values) } + /** + * {string (typename) --> TextFieldDef} + */ public static AllTypes = ValidatedTextField.allTypesDict(); public static InputForType(type: string, options?: { @@ -186,6 +192,7 @@ export default class ValidatedTextField { const tp: TextFieldDef = ValidatedTextField.AllTypes[type] const isValidTp = tp.isValid; let isValid; + options.textArea = options.textArea ?? type === "text"; if (options.isValid) { const optValid = options.isValid; isValid = (str, country) => { diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts new file mode 100644 index 0000000..bd9f566 --- /dev/null +++ b/UI/Popup/EditableTagRendering.ts @@ -0,0 +1,83 @@ +import {UIElement} from "../UIElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import TagRenderingQuestion from "./TagRenderingQuestion"; +import Translations from "../i18n/Translations"; +import Combine from "../Base/Combine"; +import TagRenderingAnswer from "./TagRenderingAnswer"; +import State from "../../State"; + +export default class EditableTagRendering extends UIElement { + private _tags: UIEventSource; + private _configuration: TagRenderingConfig; + + private _editMode: UIEventSource = new UIEventSource(false); + private _editButton: UIElement; + + private _question: UIElement; + private _answer: UIElement; + + constructor(tags: UIEventSource, + configuration: TagRenderingConfig) { + super(tags); + this._tags = tags; + this._configuration = configuration; + + this.ListenTo(this._editMode); + this.ListenTo(State.state?.osmConnection?.userDetails) + + const self = this; + + this._answer = new TagRenderingAnswer(tags, configuration); + + this._answer.SetStyle("width:100%;") + + if (this._configuration.question !== undefined) { + // 2.3em total width + this._editButton = new FixedUiElement( + "edit") + .onClick(() => { + self._editMode.setData(true); + }); + + + // And at last, set up the skip button + const cancelbutton = + Translations.t.general.cancel.Clone() + .SetClass("cancel") + .onClick(() => { + self._editMode.setData(false) + }); + + this._question = new TagRenderingQuestion(tags, configuration, + () => { + self._editMode.setData(false) + }, + cancelbutton) + } + } + + + InnerRender(): string { + + if (this._editMode.data) { + return this._question.Render(); + } + + if(this._configuration.GetRenderValue(this._tags.data)=== undefined){ + return ""; + } + + if(!this._configuration?.condition?.matchesProperties(this._tags.data)){ + return ""; + } + + return new Combine([this._answer, + (State.state?.osmConnection?.userDetails?.data?.loggedIn ?? true) ? this._editButton : undefined + ]).SetClass("answer") + .Render(); + } + +} \ No newline at end of file diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 48eb4a1..b65c53f 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -1,150 +1,47 @@ -import {VerticalCombine} from "../Base/VerticalCombine"; import {UIElement} from "../UIElement"; -import Combine from "../Base/Combine"; -import {WikipediaLink} from "../../Customizations/Questions/WikipediaLink"; -import {OsmLink} from "../../Customizations/Questions/OsmLink"; import {UIEventSource} from "../../Logic/UIEventSource"; -import {TagRenderingOptions} from "../../Customizations/TagRenderingOptions"; -import State from "../../State"; -import {And} from "../../Logic/Tags"; -import {TagDependantUIElement, TagDependantUIElementConstructor} from "../../Customizations/UIElementConstructor"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import Translations from "../i18n/Translations"; +import LayerConfig from "../../Customizations/JSON/LayerConfig"; +import EditableTagRendering from "./EditableTagRendering"; +import QuestionBox from "./QuestionBox"; +import Combine from "../Base/Combine"; +import TagRenderingAnswer from "./TagRenderingAnswer"; export class FeatureInfoBox extends UIElement { + private _tags: UIEventSource; + private _layerConfig: LayerConfig; - /** - * The actual GEOJSON-object, with geometry and stuff - */ - private _feature: any; - /** - * The tags, wrapped in a global event source - */ - private readonly _tagsES: UIEventSource; - private readonly _title: UIElement; - private readonly _infoboxes: TagDependantUIElement[]; - - private readonly _oneSkipped = Translations.t.general.oneSkippedQuestion.Clone(); - private readonly _someSkipped = Translations.t.general.skippedQuestions.Clone(); + private _title : UIElement; + private _titleIcons: UIElement; + private _renderings: UIElement[]; + private _questionBox : UIElement; constructor( feature: any, - tagsES: UIEventSource, - title: TagDependantUIElementConstructor | UIElement | string, - elementsToShow: TagDependantUIElementConstructor[], + tags: UIEventSource, + layerConfig: LayerConfig ) { - super(tagsES); - this._feature = feature; - this._tagsES = tagsES - if(tagsES === undefined){ - throw "No Tags event source given" - } - this.ListenTo(State.state.osmConnection.userDetails); - this.SetClass("featureinfobox"); - const tags = this._tagsES; - - this._infoboxes = []; - elementsToShow = elementsToShow ?? [] - - const self = this; - for (const tagRenderingOption of elementsToShow) { - self._infoboxes.push( - tagRenderingOption.construct(tags)); - } - function initTags() { - self._infoboxes.splice(0, self._infoboxes.length); - for (const tagRenderingOption of elementsToShow) { - self._infoboxes.push( - tagRenderingOption.construct(tags)); - } - self.Update(); - } - - this._someSkipped.onClick(initTags) - this._oneSkipped.onClick(initTags) + super(); + this._tags = tags; + this._layerConfig = layerConfig; - let renderedTitle: UIElement; - title = title ?? new TagRenderingOptions( - { - mappings: [{k: new And([]), txt: ""}] - } - ) - if (typeof (title) == "string") { - renderedTitle = new FixedUiElement(title); - } else if (title instanceof UIElement) { - renderedTitle = title; - } else { - renderedTitle = title.construct(tags); - } + this._title = new TagRenderingAnswer(tags, layerConfig.title) + .SetClass("featureinfobox-title"); + this._titleIcons = new Combine( + layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon))) + .SetClass("featureinfobox-icons"); + this._renderings = layerConfig.tagRenderings.map(tr => new EditableTagRendering(tags, tr)); + this._questionBox = new QuestionBox(tags, layerConfig.tagRenderings); - - renderedTitle - .SetStyle("width: calc(100% - 50px - 0.2em);") - .SetClass("title-font") - - const osmLink = new OsmLink() - .construct(tags) - .SetStyle("width: 24px; display:block;") - const wikipedialink = new WikipediaLink() - .construct(tags) - .SetStyle("width: 24px; display:block;") - - this._title = new Combine([ - renderedTitle, - wikipedialink, - osmLink]).SetStyle("display:flex;"); } InnerRender(): string { - - - const info = []; - const questions: TagDependantUIElement[] = []; - let skippedQuestions = 0; - for (const infobox of this._infoboxes) { - if (infobox.IsKnown()) { - info.push(infobox); - } else if (infobox.IsQuestioning()) { - questions.push(infobox); - } else if (infobox.IsSkipped()) { - // This question is neither known nor questioning -> it was skipped - skippedQuestions++; - } - - } - - let questionElement: UIElement; - - if (questions.length > 0) { - // We select the most important question and render that one - let mostImportantQuestion; - for (const question of questions) { - - if (mostImportantQuestion === undefined) { - mostImportantQuestion = question; - break; - } - } - questionElement = mostImportantQuestion; - } else if (skippedQuestions == 1) { - questionElement = this._oneSkipped; - } else if (skippedQuestions > 0) { - questionElement = this._someSkipped; - } - - const infoboxcontents = new Combine( - [new VerticalCombine(info).SetClass("infobox-information") - , questionElement ?? ""]); - return new Combine([ - this._title, - "
", - infoboxcontents, - "
"]) - .Render(); + new Combine([this._title, this._titleIcons]) + .SetClass("featureinfobox-titlebar"), + ...this._renderings, + this._questionBox + ]).Render(); } - - } diff --git a/UI/Popup/QuestionBox.ts b/UI/Popup/QuestionBox.ts new file mode 100644 index 0000000..1488e58 --- /dev/null +++ b/UI/Popup/QuestionBox.ts @@ -0,0 +1,78 @@ +import {UIElement} from "../UIElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; +import TagRenderingQuestion from "./TagRenderingQuestion"; +import Translations from "../i18n/Translations"; + + +/** + * Generates all the questions, one by one + */ +export default class QuestionBox extends UIElement { + private _tags: UIEventSource; + + private _tagRenderings: TagRenderingConfig[]; + private _tagRenderingQuestions: UIElement[]; + + private _skippedQuestions: UIEventSource = new UIEventSource([]) + private _skippedQuestionsButton: UIElement; + + constructor(tags: UIEventSource, tagRenderings: TagRenderingConfig[]) { + super(tags); + this.ListenTo(this._skippedQuestions); + this._tags = tags; + const self = this; + this._tagRenderings = tagRenderings.filter(tr => tr.question !== undefined); + this._tagRenderingQuestions = this._tagRenderings + .map((tagRendering, i) => new TagRenderingQuestion(this._tags, tagRendering, + () => { + // We save + self._skippedQuestions.data.push(i) + self._skippedQuestions.ping(); + }, + Translations.t.general.skip.Clone() + .SetClass("cancel") + .onClick(() => { + self._skippedQuestions.data.push(i); + self._skippedQuestions.ping(); + }) + )); + + + this._skippedQuestionsButton = Translations.t.general.skippedQuestions.Clone() + .onClick(() => { + self._skippedQuestions.setData([]); + }) + } + + InnerRender(): string { + for (let i = 0; i < this._tagRenderingQuestions.length; i++) { + let tagRendering = this._tagRenderings[i]; + if(tagRendering.condition && + !tagRendering.condition.matchesProperties(this._tags.data)){ + // Filtered away by the condition + continue; + } + + if (tagRendering.GetRenderValue(this._tags.data) !== undefined) { + // This value is known + continue; + } + + + if (this._skippedQuestions.data.indexOf(i) >= 0) { + continue; + } + + // this value is NOT known + return this._tagRenderingQuestions[i].Render(); + } + + if (this._skippedQuestions.data.length > 0) { + return this._skippedQuestionsButton.Render(); + } + + return ""; + } + +} \ No newline at end of file diff --git a/UI/Popup/SaveButton.ts b/UI/Popup/SaveButton.ts index ae4bfeb..52a78de 100644 --- a/UI/Popup/SaveButton.ts +++ b/UI/Popup/SaveButton.ts @@ -1,9 +1,12 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import {UIElement} from "../UIElement"; import Translations from "../i18n/Translations"; +import State from "../../State"; export class SaveButton extends UIElement { + private _value: UIEventSource; + private _friendlyLogin: UIElement; constructor(value: UIEventSource) { super(value); @@ -11,16 +14,22 @@ export class SaveButton extends UIElement { throw "No event source for savebutton, something is wrong" } this._value = value; + + this._friendlyLogin = Translations.t.general.loginToStart.Clone() + .SetClass("login-button-friendly") + .onClick(() => State.state.osmConnection.AttemptLogin()) } InnerRender(): string { - if (this._value.data === undefined || - this._value.data === null - || this._value.data === "" - ) { - return ""+Translations.t.general.save.Render()+"" + let clss = "save"; + + if(State.state !== undefined && !State.state.osmConnection.userDetails.data.loggedIn){ + return this._friendlyLogin.Render(); } - return ""+Translations.t.general.save.Render()+""; + if ((this._value.data ?? "") === "") { + clss = "save-non-active"; + } + return Translations.t.general.save.Clone().SetClass(clss).Render(); } } \ No newline at end of file diff --git a/UI/Popup/TagRendering.ts b/UI/Popup/TagRendering.ts deleted file mode 100644 index b19b848..0000000 --- a/UI/Popup/TagRendering.ts +++ /dev/null @@ -1,544 +0,0 @@ -import {UIElement} from "../UIElement"; -import Translation from "../i18n/Translation"; -import {VariableUiElement} from "../Base/VariableUIElement"; -import InputElementMap from "../Input/InputElementMap"; -import CheckBoxes from "../Input/Checkboxes"; -import Combine from "../Base/Combine"; -import {And, Tag, TagsFilter, TagUtils} from "../../Logic/Tags"; -import {InputElement} from "../Input/InputElement"; -import {SaveButton} from "./SaveButton"; -import {RadioButton} from "../Input/RadioButton"; -import {FixedInputElement} from "../Input/FixedInputElement"; -import {UIEventSource} from "../../Logic/UIEventSource"; -import ValidatedTextField from "../Input/ValidatedTextField"; -import {TagRenderingOptions} from "../../Customizations/TagRenderingOptions"; -import State from "../../State"; -import {SubstitutedTranslation} from "../SpecialVisualizations"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import Translations from "../i18n/Translations"; -import {TagDependantUIElement} from "../../Customizations/UIElementConstructor"; -import Locale from "../i18n/Locale"; - -export class TagRendering extends UIElement implements TagDependantUIElement { - - - private readonly _question: string | Translation; - private readonly _mapping: { k: TagsFilter, txt: string | Translation }[]; - - private readonly currentTags: UIEventSource; - - - private readonly _freeform: { - key: string, - template: string | UIElement, - renderTemplate: string | Translation, - placeholder?: string | UIElement, - extraTags?: TagsFilter - }; - - - private readonly _questionElement: InputElement; - - private readonly _saveButton: UIElement; - private readonly _friendlyLogin: UIElement; - - private readonly _skipButton: UIElement; - private readonly _editButton: UIElement; - - private readonly _appliedTags: UIElement; - - private readonly _questionSkipped: UIEventSource = new UIEventSource(false); - - private readonly _editMode: UIEventSource = new UIEventSource(false); - - static injectFunction() { - // This is a workaround as not to import tagrendering into TagREnderingOptions - TagRenderingOptions.tagRendering = (tags, options) => new TagRendering(tags, options); - return true; - } - - constructor(tags: UIEventSource, options: { - question?: string | Translation, - freeform?: { - key: string, - template: string | Translation, - renderTemplate: string | Translation, - placeholder?: string | Translation, - extraTags?: TagsFilter, - }, - tagsPreprocessor?: ((tags: any) => any), - multiAnswer?: boolean, - mappings?: { k: TagsFilter, txt: string | Translation, substitute?: boolean, hideInAnswer?: boolean }[] - }) { - super(tags); - if (tags === undefined) { - throw "No tags given for a tagrendering..." - } - if (options.question !== undefined) { - if ((options.mappings?.length ?? 0) === 0 && options.freeform.key === undefined) { - throw "Error: question without mappings or key" - } - } - this.ListenTo(Locale.language); - this.ListenTo(this._editMode); - this.ListenTo(this._questionSkipped); - this.ListenTo(State.state?.osmConnection?.userDetails); - - const self = this; - - this.currentTags = tags.map(tags => { - - if (options.tagsPreprocessor === undefined) { - return tags; - } - // we clone the tags... - let newTags = {}; - for (const k in tags) { - newTags[k] = tags[k]; - } - // ... in order to safely edit them here - options.tagsPreprocessor(newTags); - return newTags; - } - ); - tags.addCallback(() => self.currentTags.ping()); - - if (options.question !== undefined) { - this._question = options.question; - } - - this._mapping = []; - this._freeform = options.freeform; - - - for (const choice of options.mappings ?? []) { - - let choiceSubbed = { - k: choice.k?.substituteValues(this.currentTags.data), - txt: choice.txt, - } - - - this._mapping.push({ - k: choiceSubbed.k, - txt: choiceSubbed.txt - }); - } - - // Prepare the actual input element -> pick an appropriate implementation - - this._questionElement = this.InputElementFor(options) ?? - new FixedInputElement("No input possible", new Tag("a", "b")); - const save = () => { - const selection = self._questionElement.GetValue().data; - console.log("Tagrendering: saving tags ", selection); - if (selection) { - State.state?.changes?.addTag(tags.data.id, selection); - } - self._editMode.setData(false); - } - - this._appliedTags = new VariableUiElement( - self._questionElement.GetValue().map( - (tags: TagsFilter) => { - const csCount = State.state?.osmConnection?.userDetails?.data?.csCount ?? 1000; - if (csCount < State.userJourney.tagsVisibleAt) { - return ""; - } - - if (tags === undefined) { - return Translations.t.general.noTagsSelected.SetClass("subtle").Render(); - } - if (csCount < State.userJourney.tagsVisibleAndWikiLinked) { - const tagsStr = tags.asHumanString(false, true); - return new FixedUiElement(tagsStr).SetClass("subtle").Render(); - } - return tags.asHumanString(true, true); - } - ) - ).ListenTo(self._questionElement); - - const cancel = () => { - self._questionSkipped.setData(true); - self._editMode.setData(false); - self._source.ping(); // Send a ping upstream to render the next question - } - - // Setup the save button and it's action - this._saveButton = new SaveButton(this._questionElement.GetValue()) - .onClick(save); - - this._friendlyLogin = Translations.t.general.loginToStart.Clone() - .SetClass("login-button-friendly") - .onClick(() => State.state.osmConnection.AttemptLogin()) - - this._editButton = new FixedUiElement(""); - if (this._question !== undefined) { - // 2.3em total width - this._editButton = new FixedUiElement( - "edit") - .onClick(() => { - self._editMode.setData(true); - self._questionElement.GetValue().setData(self.CurrentValue()); - }); - } - - const cancelContents = this._editMode.map((isEditing) => { - const tr = Translations.t.general; - const text = isEditing ? tr.cancel : tr.skip; - return text - .SetStyle("display: inline-block;border: solid black 0.5px;padding: 0.2em 0.3em;border-radius: 1.5em;") - .Render(); - }, [Locale.language]); - // And at last, set up the skip button - this._skipButton = new VariableUiElement(cancelContents).onClick(cancel); - - } - - - private InputElementFor(options: { - freeform?: { - key: string, - template: string | Translation, - renderTemplate: string | Translation, - placeholder?: string | Translation, - extraTags?: TagsFilter, - }, - multiAnswer?: boolean, - mappings?: { k: TagsFilter, txt: string | Translation, substitute?: boolean, hideInAnswer?: boolean }[] - }): - InputElement { - - - let freeformElement: InputElement = undefined; - if (options.freeform !== undefined) { - freeformElement = this.InputForFreeForm(options.freeform); - } - - if (options.mappings === undefined || options.mappings.length === 0) { - return freeformElement; - } - - - const elements: InputElement[] = []; - - for (const mapping of options.mappings) { - if (mapping.k === null) { - continue; - } - if (mapping.hideInAnswer) { - continue; - } - elements.push(this.InputElementForMapping(mapping, mapping.substitute)); - } - - if (freeformElement !== undefined) { - elements.push(freeformElement); - } - - if (!options.multiAnswer) { - return new RadioButton(elements, false); - } else { - const possibleTags = elements.map(el => el.GetValue().data); - const checkBoxes = new CheckBoxes(elements); - - - const inputEl = new InputElementMap(checkBoxes, - (t0, t1) => { - return t0?.isEquivalent(t1) ?? false - }, - (indices) => { - if (indices.length === 0) { - return undefined; - } - let tags: TagsFilter[] = indices.map(i => elements[i].GetValue().data); - return TagUtils.FlattenMultiAnswer(tags); - }, - (tags: TagsFilter) => { - const splitUpValues = TagUtils.SplitMultiAnswer(tags, possibleTags, this._freeform?.key, this._freeform?.extraTags); - const indices: number[] = [] - - for (let i = 0; i < splitUpValues.length; i++) { - let splitUpValue = splitUpValues[i]; - - for (let j = 0; j < elements.length; j++) { - let inputElement = elements[j]; - if (inputElement.IsValid(splitUpValue)) { - indices.push(j); - inputElement.GetValue().setData(splitUpValue); - break; - } - } - } - return indices; - }, - [freeformElement?.GetValue()] - ); - - freeformElement?.GetValue()?.addCallbackAndRun(value => { - const es = checkBoxes.GetValue(); - const i = elements.length - 1; - const index = es.data.indexOf(i); - if (value === undefined) { - if (index >= 0) { - es.data.splice(index, 1); - es.ping(); - } - } else if (index < 0) { - es.data.push(i); - es.ping(); - } - }); - - return inputEl; - } - } - - - private InputElementForMapping(mapping: { k: TagsFilter, txt: (string | Translation) }, substituteValues: boolean): FixedInputElement { - if (substituteValues) { - return new FixedInputElement(this.ApplyTemplate(mapping.txt), - mapping.k.substituteValues(this.currentTags.data), - (t0, t1) => t0.isEquivalent(t1) - ); - } - - let txt = this.ApplyTemplate(mapping.txt); - if (txt.Render().indexOf("= 0) { - txt.SetClass("question-option-with-border"); - } - const inputEl = new FixedInputElement(txt, mapping.k, - (t0, t1) => t1.isEquivalent(t0)); - - return inputEl; - } - - - private InputForFreeForm(freeform: { - key: string, - template: string | Translation, - renderTemplate: string | Translation, - placeholder?: string | Translation, - extraTags?: TagsFilter, - }): InputElement { - if (freeform?.template === undefined) { - return undefined; - } - - const prepost = Translations.W(freeform.template).InnerRender() - .replace("$$$", "$string$") - .split("$"); - let type = prepost[1]; - - let isTextArea = false; - if (type === "text") { - isTextArea = true; - type = "string"; - } - - if (ValidatedTextField.AllTypes[type] === undefined) { - console.error("Type:", type, ValidatedTextField.AllTypes) - throw "Unkown type: " + type; - } - - - const pickString = - (string: any) => { - if (string === "" || string === undefined) { - return undefined; - } - - const tag = new Tag(freeform.key, string); - - if (freeform.extraTags === undefined) { - return tag; - } - return new And([ - tag, - freeform.extraTags - ] - ); - }; - - const toString = (tag) => { - if (tag instanceof And) { - for (const subtag of tag.and) { - if (subtag instanceof Tag && subtag.key === freeform.key) { - return subtag.value; - } - } - - return undefined; - } else if (tag instanceof Tag) { - return tag.value - } - return undefined; - } - - return ValidatedTextField.Mapped(pickString, toString, { - placeholder: freeform.placeholder, - type: type, - isValid: (str) => (str.length <= 255), - textArea: isTextArea, - country: this._source.data._country - }) - } - - - IsKnown(): boolean { - const tags = TagUtils.proprtiesToKV(this._source.data); - - for (const oneOnOneElement of this._mapping) { - if (oneOnOneElement.k === null || oneOnOneElement.k === undefined || oneOnOneElement.k.matches(tags)) { - return true; - } - } - return this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined; - } - - IsSkipped(): boolean { - return this._questionSkipped.data; - } - - private CurrentValue(): TagsFilter { - const tags = TagUtils.proprtiesToKV(this._source.data); - - for (const oneOnOneElement of this._mapping) { - if (oneOnOneElement.k !== null && oneOnOneElement.k.matches(tags)) { - return oneOnOneElement.k; - } - } - if (this._freeform === undefined) { - return undefined; - } - - return new Tag(this._freeform.key, this._source.data[this._freeform.key]); - } - - - IsQuestioning(): boolean { - if (this.IsKnown()) { - return false; - } - if (this._question === undefined || - this._question === "" || - (this._freeform?.template === undefined && (this._mapping?.length ?? 0) == 0)) { - // We don't ask this question in the first place - return false; - } - if (this._questionSkipped.data) { - // We don't ask for this question anymore, skipped by user - return false; - } - return true; - } - - private RenderAnswer(): UIElement { - const tags = TagUtils.proprtiesToKV(this._source.data); - - - for (const oneOnOneElement of this._mapping) { - if (oneOnOneElement.k === undefined || oneOnOneElement.k.matches(tags)) { - // We have found a matching key -> we use this template - return this.ApplyTemplate(oneOnOneElement.txt); - } - } - - if (this._freeform !== undefined && this._source.data[this._freeform.key] !== undefined) { - return this.ApplyTemplate(this._freeform.renderTemplate); - } - - return new FixedUiElement(""); - } - - - private CreateComponent(): UIElement { - - - if (this.IsQuestioning() - && (State.state !== undefined) // If State.state is undefined, we are testing/custom theme building -> show regular save - && !State.state.osmConnection.userDetails.data.loggedIn) { - - const question = - this.ApplyTemplate(this._question).SetClass('question-text'); - return new Combine(["
", - question, - "
", - this._questionElement, - this._friendlyLogin, "
" - ]); - } - - if (this.IsQuestioning() || this._editMode.data) { - // Not yet known or questioning, we have to ask a question - return new Combine([ - this.ApplyTemplate(this._question).SetStyle('question-text'), - "
", - "
", this._questionElement, "
", - this._skipButton, - this._saveButton, - "
", - this._appliedTags - ]).SetClass('question'); - } - - if (this.IsKnown()) { - - const answer = this.RenderAnswer(); - - if (answer.IsEmpty()) { - return new FixedUiElement(""); - } - - - const answerStyle = " display: inline-block;" + - " margin: 0.1em;" + - " width: 100%;" + - " font-size: large;" - - if (State.state === undefined || // state undefined -> we are custom testing - State.state?.osmConnection?.userDetails?.data?.loggedIn && this._question !== undefined) { - answer.SetStyle("display:inline-block;width:calc(100% - 2.3em);") - return new Combine([ - answer, - this._editButton]) - .SetStyle(answerStyle); - } - - return answer.SetStyle(answerStyle); - } - console.error("Invalid tagrendering: fallthrough",this) - return new FixedUiElement(""); - } - - InnerRender(): string { - return this.CreateComponent().Render(); - } - - - protected InnerUpdate(htmlElement: HTMLElement) { - this._editButton.Update(); - } - - private readonly answerCache = {} - // Makes sure that the elements receive updates - // noinspection JSMismatchedCollectionQueryUpdate - private readonly substitutedElements : UIElement[]= []; - - private ApplyTemplate(template: string | Translation): UIElement { - const tr = Translations.WT(template); - if(tr === undefined){ - return undefined; - } - if (this.answerCache[tr.id]) { - return this.answerCache[tr.id]; - } - // We have to cache these elemnts, otherwise it is to slow - const el = new SubstitutedTranslation(tr, this.currentTags); - this.answerCache[tr.id] = el; - this.substitutedElements.push(el); - return el; - } - -} diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts new file mode 100644 index 0000000..30cb789 --- /dev/null +++ b/UI/Popup/TagRenderingAnswer.ts @@ -0,0 +1,28 @@ +import {UIEventSource} from "../../Logic/UIEventSource"; +import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; +import {UIElement} from "../UIElement"; +import {SubstitutedTranslation} from "../SpecialVisualizations"; + +/*** + * Displays the correct value for a known tagrendering + */ +export default class TagRenderingAnswer extends UIElement { + private _tags: UIEventSource; + private _configuration: TagRenderingConfig; + + constructor(tags: UIEventSource, + configuration: TagRenderingConfig) { + super(tags); + this._tags = tags; + this._configuration = configuration; + } + + InnerRender(): string { + const tr = this._configuration.GetRenderValue(this._tags.data); + if(tr === undefined){ + return ""; + } + return new SubstitutedTranslation(tr, this._tags).Render(); + } + +} \ No newline at end of file diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts new file mode 100644 index 0000000..1dd6e44 --- /dev/null +++ b/UI/Popup/TagRenderingQuestion.ts @@ -0,0 +1,251 @@ +import {UIElement} from "../UIElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Combine from "../Base/Combine"; +import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; +import {InputElement} from "../Input/InputElement"; +import {And, Tag, TagsFilter, TagUtils} from "../../Logic/Tags"; +import ValidatedTextField from "../Input/ValidatedTextField"; +import Translation from "../i18n/Translation"; +import {FixedInputElement} from "../Input/FixedInputElement"; +import {SubstitutedTranslation} from "../SpecialVisualizations"; +import {RadioButton} from "../Input/RadioButton"; +import {Utils} from "../../Utils"; +import CheckBoxes from "../Input/Checkboxes"; +import InputElementMap from "../Input/InputElementMap"; +import {SaveButton} from "./SaveButton"; +import State from "../../State"; +import {Changes} from "../../Logic/Osm/Changes"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import Translations from "../i18n/Translations"; +import {FixedUiElement} from "../Base/FixedUiElement"; + +/** + * Shows the question element. + * Note that the value _migh_ already be known, e.g. when selected or when changing the value + */ +export default class TagRenderingQuestion extends UIElement { + private _tags: UIEventSource; + private _configuration: TagRenderingConfig; + + private _saveButton: UIElement; + + private _inputElement: InputElement; + private _cancelButton: UIElement; + private _appliedTags: UIElement; + private _question: UIElement; + + constructor(tags: UIEventSource, + configuration: TagRenderingConfig, + afterSave?: () => void, + cancelButton?: UIElement) { + super(tags); + this._tags = tags; + this._configuration = configuration; + this._cancelButton = cancelButton; + this._question = new SubstitutedTranslation(this._configuration.question, tags) + .SetClass("question-text"); + if (configuration === undefined) { + throw "A question is needed for a question visualization" + } + + + this._inputElement = this.GenerateInputElement() + const self = this; + const save = () => { + const selection = self._inputElement.GetValue().data; + if (selection) { + (State.state?.changes ?? new Changes()) + .addTag(tags.data.id, selection, tags); + } + + if (afterSave) { + afterSave(); + } + } + + + this._saveButton = new SaveButton(this._inputElement.GetValue()) + .onClick(save) + + + this._appliedTags = new VariableUiElement( + self._inputElement.GetValue().map( + (tags: TagsFilter) => { + const csCount = State.state?.osmConnection?.userDetails?.data?.csCount ?? 1000; + if (csCount < State.userJourney.tagsVisibleAt) { + return ""; + } + + if (tags === undefined) { + return Translations.t.general.noTagsSelected.SetClass("subtle").Render(); + } + if (csCount < State.userJourney.tagsVisibleAndWikiLinked) { + const tagsStr = tags.asHumanString(false, true); + return new FixedUiElement(tagsStr).SetClass("subtle").Render(); + } + return tags.asHumanString(true, true); + } + ) + ) + + } + + private GenerateInputElement(): InputElement { + const ff = this.GenerateFreeform(); + const self = this; + let mappings = + (this._configuration.mappings ?? []).map(mapping => self.GenerateMappingElement(mapping)); + mappings = Utils.NoNull(mappings); + + if (mappings.length == 0) { + return ff; + } + + mappings = Utils.NoNull([...mappings, ff]); + mappings.forEach(el => el.SetClass("question-option-with-border")) + + if (this._configuration.multiAnswer) { + return this.GenerateMultiAnswer(mappings, ff) + } else { + return new RadioButton(mappings, false) + } + + } + + private GenerateMultiAnswer(elements: InputElement[], freeformField: InputElement): InputElement { + const possibleTags = elements.map(el => el.GetValue().data); + const checkBoxes = new CheckBoxes(elements); + const inputEl = new InputElementMap( + checkBoxes, + (t0, t1) => { + return t0?.isEquivalent(t1) ?? false + }, + (indices) => { + if (indices.length === 0) { + return undefined; + } + const tags: TagsFilter[] = indices.map(i => elements[i].GetValue().data); + return TagUtils.FlattenMultiAnswer(tags); + }, + (tags: TagsFilter) => { + const splitUpValues = TagUtils.SplitMultiAnswer(tags, possibleTags, this._configuration.freeform?.key, new And(this._configuration.freeform?.addExtraTags)); + const indices: number[] = [] + + for (let i = 0; i < splitUpValues.length; i++) { + let splitUpValue = splitUpValues[i]; + + for (let j = 0; j < elements.length; j++) { + let inputElement = elements[j]; + if (inputElement.IsValid(splitUpValue)) { + indices.push(j); + inputElement.GetValue().setData(splitUpValue); + break; + } + } + } + console.log(indices) + return indices; + }, + elements.map(el => el.GetValue()) + ); + + + freeformField?.GetValue()?.addCallbackAndRun(value => { + const es = checkBoxes.GetValue(); + const i = elements.length - 1; + const index = es.data.indexOf(i); + if (value === undefined) { + if (index >= 0) { + es.data.splice(index, 1); + es.ping(); + } + } else if (index < 0) { + es.data.push(i); + es.ping(); + } + }); + + return inputEl; + } + + private GenerateMappingElement(mapping: { + if: TagsFilter, + then: Translation, + hideInAnswer: boolean + }): InputElement { + if (mapping.hideInAnswer) { + return undefined; + } + return new FixedInputElement( + new SubstitutedTranslation(mapping.then, this._tags), + mapping.if, + (t0, t1) => t1.isEquivalent(t0)); + } + + + private GenerateFreeform(): InputElement { + const freeform = this._configuration.freeform; + if (freeform === undefined) { + return undefined; + } + + const pickString = + (string: any) => { + if (string === "" || string === undefined) { + return undefined; + } + + const tag = new Tag(freeform.key, string); + + if (freeform.addExtraTags === undefined) { + return tag; + } + return new And([ + tag, + ...freeform.addExtraTags + ] + ); + }; + + const toString = (tag) => { + if (tag instanceof And) { + for (const subtag of tag.and) { + if (subtag instanceof Tag && subtag.key === freeform.key) { + return subtag.value; + } + } + + return undefined; + } else if (tag instanceof Tag) { + return tag.value + } + return undefined; + } + + const textField = ValidatedTextField.InputForType(this._configuration.freeform.type, { + isValid: (str) => (str.length <= 255), + country: this._tags.data._country + }); + + textField.GetValue().setData(this._tags.data[this._configuration.freeform.key]); + + return new InputElementMap( + textField, (a, b) => a === b || (a?.isEquivalent(b) ?? false), + pickString, toString + ); + + } + + + InnerRender(): string { + return new Combine([ + this._question, + this._inputElement, "
", + this._cancelButton, + this._saveButton, "
", + this._appliedTags]) + .SetClass("question") + .Render() + } + +} \ No newline at end of file diff --git a/UI/SimpleAddUI.ts b/UI/SimpleAddUI.ts index be1c406..21b03ec 100644 --- a/UI/SimpleAddUI.ts +++ b/UI/SimpleAddUI.ts @@ -8,7 +8,6 @@ import Locale from "./i18n/Locale"; import State from "../State"; import {UIEventSource} from "../Logic/UIEventSource"; -import {Utils} from "../Utils"; /** * Asks to add a feature at the last clicked location, at least if zoom is sufficient @@ -52,21 +51,9 @@ export class SimpleAddUI extends UIElement { for (const preset of layer.layerDef.presets) { - let icon: string = "./assets/bug.svg"; - if (preset.icon !== undefined) { - - if (typeof (preset.icon) !== "string") { - const tags = Utils.MergeTags(TagUtils.KVtoProperties(preset.tags), {id:"node/-1"}); - icon = preset.icon.GetContent(tags).txt; - if(icon.startsWith("$")){ - icon = undefined; - } - } else { - icon = preset.icon; - } - } else { - console.warn("No icon defined for preset ", preset, "in layer ", layer.layerDef.id) - } + let icon: string = layer.layerDef.icon.GetRenderValue( + TagUtils.KVtoProperties(preset.tags ?? [])).txt ?? + "./assets/bug.svg"; const csCount = State.state.osmConnection.userDetails.data.csCount; let tagInfo = ""; diff --git a/UI/UserBadge.ts b/UI/UserBadge.ts index c05e8ce..7ae6d0b 100644 --- a/UI/UserBadge.ts +++ b/UI/UserBadge.ts @@ -6,8 +6,8 @@ import Translations from "./i18n/Translations"; import {UserDetails} from "../Logic/Osm/OsmConnection"; import State from "../State"; import {UIEventSource} from "../Logic/UIEventSource"; -import {InitUiElements} from "../InitUiElements"; import Combine from "./Base/Combine"; +import Locale from "./i18n/Locale"; /** * Handles and updates the user badge @@ -23,7 +23,7 @@ export class UserBadge extends UIElement { constructor() { super(State.state.osmConnection.userDetails); this._userDetails = State.state.osmConnection.userDetails; - this._languagePicker = (InitUiElements.CreateLanguagePicker() ?? new FixedUiElement("")) + this._languagePicker = (Locale.CreateLanguagePicker(State.state.layoutToUse.data.supportedLanguages) ?? new FixedUiElement("")) .SetStyle("display:inline-block;width:min-content;"); this._loginButton = Translations.t.general.loginWithOpenStreetMap diff --git a/UI/WelcomeMessage.ts b/UI/WelcomeMessage.ts index b328547..781789f 100644 --- a/UI/WelcomeMessage.ts +++ b/UI/WelcomeMessage.ts @@ -3,7 +3,6 @@ import Locale from "../UI/i18n/Locale"; import State from "../State"; import Translations from "./i18n/Translations"; import Combine from "./Base/Combine"; -import {InitUiElements} from "../InitUiElements"; export class WelcomeMessage extends UIElement { @@ -18,7 +17,7 @@ export class WelcomeMessage extends UIElement { constructor() { super(State.state.osmConnection.userDetails); this.ListenTo(Locale.language); - this.languagePicker = InitUiElements.CreateLanguagePicker(Translations.t.general.pickLanguage); + this.languagePicker = Locale.CreateLanguagePicker(State.state.layoutToUse.data.supportedLanguages, Translations.t.general.pickLanguage); const layout = State.state.layoutToUse.data; this.description =Translations.W(layout.welcomeMessage); diff --git a/UI/i18n/Locale.ts b/UI/i18n/Locale.ts index 1b4a343..e58d6ad 100644 --- a/UI/i18n/Locale.ts +++ b/UI/i18n/Locale.ts @@ -1,11 +1,13 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import {UIElement} from "../UIElement"; import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource"; +import {DropDown} from "../Input/DropDown"; export default class Locale { public static language: UIEventSource = Locale.setup(); + private static setup() { const source = LocalStorageSource.Get('language', "en"); if (!UIElement.runningFromConsole) { @@ -17,6 +19,20 @@ export default class Locale { return source; } + public static CreateLanguagePicker( + languages : string[] , + label: string | UIElement = "") { + + if (languages.length <= 1) { + return undefined; + } + + return new DropDown(label, languages.map(lang => { + return {value: lang, shown: lang} + } + ), Locale.language); + } + } diff --git a/UI/i18n/Translations.ts b/UI/i18n/Translations.ts index 2542da4..d52aed4 100644 --- a/UI/i18n/Translations.ts +++ b/UI/i18n/Translations.ts @@ -1056,6 +1056,22 @@ export default class Translations { return s; } + + static T(t: string | any): Translation { + if(t === undefined){ + return undefined; + } + if(typeof t === "string"){ + return new Translation({"*":t}); + } + if(t.render !== undefined){ + const msg = "Creating a translation, but this object contains a 'render'-field. Use the translation directly" + console.error(msg, t); + throw msg + } + return new Translation(t); + } + private static wtcache = {} public static WT(s: string | Translation): Translation { if(s === undefined){ diff --git a/assets/layers/nature_reserve/nature_reserve.json b/assets/layers/nature_reserve/nature_reserve.json index 850864f..7db0483 100644 --- a/assets/layers/nature_reserve/nature_reserve.json +++ b/assets/layers/nature_reserve/nature_reserve.json @@ -276,7 +276,8 @@ "en": "{curator} is the curator of this nature reserve" }, "freeform": { - "key": "curator" + "key": "curator", + "type": "string" } }, { @@ -329,16 +330,17 @@ } }, {"#": "Surface are", - "render": { - "en": "Surface area: {_surface:ha}Ha", - "mappings": { - "if": "_surface:ha=0", - "then": "" + "render": { + "en": "Surface area: {_surface:ha}Ha", + "mappings": { + "if": "_surface:ha=0", + "then": "" + } } } - } ], "hideUnderlayingFeaturesMinPercentage": 10, + "wayHandling": 1, "icon": { "render": "./assets/themes/buurtnatuur/nature_reserve.svg" }, diff --git a/assets/osm-logo-us.svg b/assets/osm-logo-us.svg new file mode 100644 index 0000000..21ada51 --- /dev/null +++ b/assets/osm-logo-us.svg @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/assets/questions/questions.json b/assets/questions/questions.json new file mode 100644 index 0000000..d84ef8e --- /dev/null +++ b/assets/questions/questions.json @@ -0,0 +1,32 @@ +{ + "images": { + "render": "{image_carousel()}{image_upload()}" + }, + + "osmlink": { + "render": "OSM", + "mappings":[{ + "if": "id~=-", + "then": "Uploading..." + }] + }, + + "wikipedialink": { + "render": "WP", + "condition": "wikipedia~*" + }, + + "website": { + "question": { + "en": "What is the website of {name}?", + "nl": "Wat is de website van {name}?", + "fr": "Quel est le site internet de {name}?", + "gl": "Cal é a páxina web de {name}?" + }, + "render": "{website}", + "freeform": { + "key": "website", + "type": "url" + } + } +} \ No newline at end of file diff --git a/assets/themes/aed/aed.json b/assets/themes/aed/aed.json index 1250c23..e64be5f 100644 --- a/assets/themes/aed/aed.json +++ b/assets/themes/aed/aed.json @@ -13,7 +13,7 @@ "description": { "en": "On this map, one can find and mark nearby defibrillators", "ca": "En aquest mapa , qualsevol pot trobar i marcar els desfibril·ladors externs automàtics més propers", - "en": "En este mapa , cualquiera puede encontrar y marcar los desfibriladores externos automáticos más cercanos", + "es": "En este mapa , cualquiera puede encontrar y marcar los desfibriladores externos automáticos más cercanos", "fr": "Sur cette carte, vous pouvez trouver et améliorer les informations sur les défibrillateurs", "nl": "Op deze kaart kan je informatie over AEDs vinden en verbeteren", "de": "Auf dieser Karte kann man nahe gelegene Defibrillatoren finden und markieren" @@ -71,12 +71,12 @@ } ], "tagRenderings": [ - "pictures", + "images", { "question": { "en": "Is this defibrillator located indoors?", "ca": "Està el desfibril·lador a l'interior?", - "en": "¿Esté el desfibrilador en interior?", + "es": "¿Esté el desfibrilador en interior?", "fr": "Ce défibrillateur est-il disposé en intérieur ?", "nl": "Hangt deze defibrillator binnen of buiten?", "de": "Befindet sich dieser Defibrillator im Gebäude?" diff --git a/assets/themes/artwork/artwork.json b/assets/themes/artwork/artwork.json index 5bb99e8..3fc6ef5 100644 --- a/assets/themes/artwork/artwork.json +++ b/assets/themes/artwork/artwork.json @@ -81,7 +81,7 @@ } ], "tagRenderings": [ - "pictures", + "images", { "render": { "en": "This is a {artwork_type}", diff --git a/assets/themes/fritures/fritures.json b/assets/themes/fritures/fritures.json index 41f66d0..a57157e 100644 --- a/assets/themes/fritures/fritures.json +++ b/assets/themes/fritures/fritures.json @@ -63,7 +63,7 @@ "nl": "Wat is de naam van deze frituur?", "fr": "Quel est le nom de cette friterie?" }, - "feeform": { + "freeform": { "key": "name" } }, diff --git a/assets/themes/toilets/toilets.json b/assets/themes/toilets/toilets.json index 3c532ae..3faa3e0 100644 --- a/assets/themes/toilets/toilets.json +++ b/assets/themes/toilets/toilets.json @@ -77,7 +77,7 @@ } ], "tagRenderings": [ - "pictures", + "images", { "question": { "en": "Are these toilets publicly accessible?", diff --git a/createLayouts.ts b/createLayouts.ts index 1b0ae63..33f9a11 100644 --- a/createLayouts.ts +++ b/createLayouts.ts @@ -9,9 +9,8 @@ import Locale from "./UI/i18n/Locale"; import svg2img from 'promise-svg2img'; import Translation from "./UI/i18n/Translation"; import Translations from "./UI/i18n/Translations"; -import {TagRendering} from "./UI/Popup/TagRendering"; -TagRendering.injectFunction(); + console.log("Building the layouts") function enc(str: string): string { diff --git a/css/openinghourstable.css b/css/openinghourstable.css index 09eeb05..b5cde5e 100644 --- a/css/openinghourstable.css +++ b/css/openinghourstable.css @@ -3,6 +3,7 @@ width: 100%; height: 100%; text-align: center; + word-break: normal; } .oh-table th { @@ -169,7 +170,7 @@ /**** Opening hours visualization table ****/ .ohviz-table { - + word-break: normal; } .ohviz-range { @@ -279,6 +280,7 @@ .ohviz-weekday { padding-left: 0.5em; + word-break: normal; } diff --git a/css/tagrendering.css b/css/tagrendering.css new file mode 100644 index 0000000..71713b2 --- /dev/null +++ b/css/tagrendering.css @@ -0,0 +1,109 @@ +.featureinfobox-title { + background-color: deeppink; +} +.featureinfobox-icons img{ + max-height: 1.5em; +} +.featureinfobox-icons { + background-color: red; +} + +.featureinfobox-titlebar{ + font-size: large; + font-weight: bold; + display: flex; + justify-content: space-between; +} + +.answer { + display: flex; + width: 100%; + font-size: large; + justify-content: space-between; + word-break: break-word; +} + +.question .form-text-field > input { + width: 100%; + box-sizing: border-box; +} + +.question { + display: block; + margin-top: 1em; + background-color: #e5f5ff; + padding: 1em; + border-radius: 1em; + font-size: larger; + +} + +.question-text { + font-size: larger; + font-weight: bold; + margin-bottom: 0.5em; +} + +.question-text img { + max-width: 100%; +} + +.question-subtext { + font-size: medium; + font-weight: normal; +} + +.question-option-with-border { + border: 2px solid lightgray; + border-radius: 0.5em; + display: inline-block; + width: 90%; + box-sizing: border-box; + padding: 0.5em; +} + +input:checked + label .question-option-with-border { + border: 2px solid black; +} + + +.save { + display: inline-block; + border: solid white 2px; + background-color: #3a3aeb; + color: white; + padding: 0.2em 0.6em; + font-size: x-large; + font-weight: bold; + border-radius: 1.5em; +} + +.cancel { + display: inline-block; + border: solid black 0.5px; + padding: 0.2em 0.3em; + border-radius: 1.5em; +} + +.login-button-friendly { + display: inline-block; + border: solid white 2px; + background-color: #3a3aeb; + color: white; + padding: 0.2em 0.6em; + font-size: large; + font-weight: bold; + border-radius: 1.5em; + box-sizing: border-box; + width: 100%; +} + +.save-non-active { + display: inline-block; + border: solid lightgrey 2px; + color: grey; + padding: 0.2em 0.3em; + font-size: x-large; + font-weight: bold; + border-radius: 1.5em; +} \ No newline at end of file diff --git a/customGenerator.ts b/customGenerator.ts index 1d28b46..ba8c564 100644 --- a/customGenerator.ts +++ b/customGenerator.ts @@ -4,7 +4,6 @@ import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson"; import {OsmConnection} from "./Logic/Osm/OsmConnection"; import CustomGeneratorPanel from "./UI/CustomGenerator/CustomGeneratorPanel"; import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; -import {TagRendering} from "./UI/Popup/TagRendering"; let layout = GenerateEmpty.createEmptyLayout(); if (window.location.hash.length > 10) { @@ -17,8 +16,6 @@ if (window.location.hash.length > 10) { } } -TagRendering.injectFunction(); - const connection = new OsmConnection(false, new UIEventSource(undefined), "customGenerator", false); new CustomGeneratorPanel(connection, layout) diff --git a/index.css b/index.css index 9d0e87f..24a56b8 100644 --- a/index.css +++ b/index.css @@ -338,115 +338,15 @@ body { width: 40em !important; max-height: 80vh; overflow-y: auto; + overflow-x: hidden; } - - -.featureinfoboxtitle span { - width: unset !important; -} - -.question .form-text-field > input { - width: 100%; - box-sizing: border-box; -} - -.osm-logo path { - fill: #7ebc6f; -} - - -.infoboxcontents { - margin: 1em 0.5em 0.5em; - -} - - -.infobox-information { - width: 100%; - margin-top: 1em; -} - -.question { - display: block; - margin-top: 1em; - background-color: #e5f5ff; - padding: 1em; - border-radius: 1em; - font-size: larger; - -} - -.question-text { - font-size: larger; - font-weight: bold; -} - -.question-text img { - max-width: 100%; -} - -.question-subtext { - font-size: medium; - font-weight: normal; -} - -.question-option-with-border{ - border: 2px solid lightgray; - border-radius: 0.5em; - display: inline-block; - width: 90%; - box-sizing: border-box; - padding: 1em; -} - -input:checked+label .question-option-with-border{ - border: 2px solid black; -} - - -/**** The save button *****/ - -.save { - display: inline-block; - border: solid white 2px; - background-color: #3a3aeb; - color: white; - padding: 0.2em 0.6em; - font-size: x-large; - font-weight: bold; - border-radius: 1.5em; -} - -.login-button-friendly { - display: inline-block; - border: solid white 2px; - background-color: #3a3aeb; - color: white; - padding: 0.2em 0.6em; - font-size: large; - font-weight: bold; - border-radius: 1.5em; - box-sizing: border-box; - width: 100%; -} - -.save-non-active { - display: inline-block; - border: solid lightgrey 2px; - color: grey; - padding: 0.2em 0.3em; - font-size: x-large; - font-weight: bold; - border-radius: 1.5em; -} - /****** ShareScreen *****/ .literal-code { display: inline-block; background-color: lightgray; padding: 0.5em; - word-break: break-all; + word-break: break-word; color: black; box-sizing: border-box; } diff --git a/index.html b/index.html index 130f182..aaa8d15 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,7 @@ + diff --git a/index.ts b/index.ts index 015f88b..da649a1 100644 --- a/index.ts +++ b/index.ts @@ -6,9 +6,7 @@ import {QueryParameters} from "./Logic/Web/QueryParameters"; import {UIEventSource} from "./Logic/UIEventSource"; import * as $ from "jquery"; import {FromJSON} from "./Customizations/JSON/FromJSON"; -import {TagRendering} from "./UI/Popup/TagRendering"; - -TagRendering.injectFunction(); +import SharedLayers from "./Customizations/SharedLayers"; let defaultLayout = "bookcases" // --------------------- Special actions based on the parameters ----------------- @@ -96,7 +94,7 @@ if (layoutFromBase64.startsWith("wiki:")) { .firstChild.textContent; try { console.log("DOWNLOADED:",layoutJson); - const layout = FromJSON.LayoutFromJSON(JSON.parse(layoutJson)); + const layout = Layout.LayoutFromJSON(JSON.parse(layoutJson), SharedLayers.sharedLayers); layout.id = layoutFromBase64; InitUiElements.InitAll(layout, layoutFromBase64, testing, layoutFromBase64, btoa(layoutJson)); } catch (e) { diff --git a/test.html b/test.html index 611c29d..fe0a777 100644 --- a/test.html +++ b/test.html @@ -6,6 +6,7 @@ +