diff --git a/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts b/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts index 05a5d8f32..6e6b4bfec 100644 --- a/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts +++ b/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts @@ -4,6 +4,7 @@ import SimpleFeatureSource from "./Sources/SimpleFeatureSource" import { Feature } from "geojson" import { Utils } from "../../Utils" import { UIEventSource } from "../UIEventSource" +import { feature } from "@turf/turf" /** * In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled) @@ -19,7 +20,7 @@ export default class PerLayerFeatureSourceSplitter< upstream: FeatureSource, options?: { constructStore?: (features: UIEventSource, layer: FilteredLayer) => T - handleLeftovers?: (featuresWithoutLayer: any[]) => void + handleLeftovers?: (featuresWithoutLayer: Feature[]) => void } ) { const knownLayers = new Map() @@ -35,9 +36,6 @@ export default class PerLayerFeatureSourceSplitter< } upstream.features.addCallbackAndRunD((features) => { - if (features === undefined) { - return - } if (layers === undefined) { return } @@ -82,7 +80,7 @@ export default class PerLayerFeatureSourceSplitter< const src = layerSources.get(id) if (Utils.sameList(src.data, features)) { - return + continue } src.setData(features) } diff --git a/Logic/FeatureSource/Sources/FeatureSourceMerger.ts b/Logic/FeatureSource/Sources/FeatureSourceMerger.ts index 7221b2a03..f6b429d97 100644 --- a/Logic/FeatureSource/Sources/FeatureSourceMerger.ts +++ b/Logic/FeatureSource/Sources/FeatureSourceMerger.ts @@ -1,6 +1,7 @@ import { Store, UIEventSource } from "../../UIEventSource" import FeatureSource, { IndexedFeatureSource } from "../FeatureSource" import { Feature } from "geojson" +import { Utils } from "../../../Utils" /** * @@ -35,20 +36,21 @@ export default class FeatureSourceMerger implements IndexedFeatureSource { } protected addData(featuress: Feature[][]) { + featuress = Utils.NoNull(featuress) let somethingChanged = false const all: Map = new Map() + const unseen = new Set() // We seed the dictionary with the previously loaded features const oldValues = this.features.data ?? [] for (const oldValue of oldValues) { all.set(oldValue.properties.id, oldValue) + unseen.add(oldValue.properties.id) } for (const features of featuress) { - if (features === undefined) { - continue - } for (const f of features) { const id = f.properties.id + unseen.delete(id) if (!all.has(id)) { // This is a new feature somethingChanged = true @@ -67,6 +69,9 @@ export default class FeatureSourceMerger implements IndexedFeatureSource { } } + somethingChanged ||= unseen.size > 0 + unseen.forEach((id) => all.delete(id)) + if (!somethingChanged) { // We don't bother triggering an update return diff --git a/Logic/FeatureSource/Sources/LayoutSource.ts b/Logic/FeatureSource/Sources/LayoutSource.ts index 7e7f43fb6..0bd6ca211 100644 --- a/Logic/FeatureSource/Sources/LayoutSource.ts +++ b/Logic/FeatureSource/Sources/LayoutSource.ts @@ -27,7 +27,7 @@ export default class LayoutSource extends FeatureSourceMerger { ) { const { bounds, zoom } = mapProperties // remove all 'special' layers - layers = layers.filter((flayer) => flayer.source !== null) + layers = layers.filter((layer) => layer.source !== null && layer.source !== undefined) const geojsonlayers = layers.filter((layer) => layer.source.geojsonSource !== undefined) const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined) @@ -122,7 +122,8 @@ export default class LayoutSource extends FeatureSourceMerger { { zoom, bounds, - layoutToUse: featureSwitches.layoutToUse, + layers: osmLayers, + widenFactor: featureSwitches.layoutToUse.widenFactor, overpassUrl: featureSwitches.overpassUrl, overpassTimeout: featureSwitches.overpassTimeout, overpassMaxZoom: featureSwitches.overpassMaxZoom, diff --git a/Logic/FeatureSource/Sources/OverpassFeatureSource.ts b/Logic/FeatureSource/Sources/OverpassFeatureSource.ts index 5e13f02cc..6cfe1ef9d 100644 --- a/Logic/FeatureSource/Sources/OverpassFeatureSource.ts +++ b/Logic/FeatureSource/Sources/OverpassFeatureSource.ts @@ -3,7 +3,6 @@ import FeatureSource from "../FeatureSource" import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import { Or } from "../../Tags/Or" -import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig" import { Overpass } from "../../Osm/Overpass" import { Utils } from "../../../Utils" import { TagsFilter } from "../../Tags/TagsFilter" @@ -26,7 +25,8 @@ export default class OverpassFeatureSource implements FeatureSource { private readonly state: { readonly zoom: Store - readonly layoutToUse: LayoutConfig + readonly layers: LayerConfig[] + readonly widenFactor: number readonly overpassUrl: Store readonly overpassTimeout: Store readonly bounds: Store @@ -37,7 +37,8 @@ export default class OverpassFeatureSource implements FeatureSource { constructor( state: { - readonly layoutToUse: LayoutConfig + readonly layers: LayerConfig[] + readonly widenFactor: number readonly zoom: Store readonly overpassUrl: Store readonly overpassTimeout: Store @@ -117,7 +118,7 @@ export default class OverpassFeatureSource implements FeatureSource { let lastUsed = 0 const layersToDownload = [] - for (const layer of this.state.layoutToUse.layers) { + for (const layer of this.state.layers) { if (typeof layer === "string") { throw "A layer was not expanded!" } @@ -130,6 +131,14 @@ export default class OverpassFeatureSource implements FeatureSource { if (layer.doNotDownload) { continue } + if (layer.source === null) { + // This is a special layer. Should not have been here + console.warn( + "OverpassFeatureSource received a layer for which the source is null:", + layer.id + ) + continue + } if (layer.source.geojsonSource !== undefined) { // Not our responsibility to download this layer! continue @@ -151,7 +160,7 @@ export default class OverpassFeatureSource implements FeatureSource { do { try { bounds = this.state.bounds.data - ?.pad(this.state.layoutToUse.widenFactor) + ?.pad(this.state.widenFactor) ?.expandToTileBounds(this.padToZoomLevel?.data) if (bounds === undefined) { @@ -195,6 +204,7 @@ export default class OverpassFeatureSource implements FeatureSource { // Some metatags are delivered by overpass _without_ underscore-prefix; we fix them below // TODO FIXME re-enable this data.features.forEach((f) => SimpleMetaTaggers.objectMetaInfo.applyMetaTagsOnFeature(f)) + console.log("Overpass returned", data.features.length, "features") self.features.setData(data.features) return [bounds, date, layersToDownload] } catch (e) { diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index 6ca8ef63c..f97e8096f 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -263,7 +263,11 @@ export abstract class Store implements Readable { public subscribe(run: Subscriber & ((value: T) => void), invalidate?): Unsubscriber { // We don't need to do anything with 'invalidate', see // https://github.com/sveltejs/svelte/issues/3859 - return this.addCallbackAndRun(run) + + // Note: run is wrapped in an anonymous function. 'Run' returns the value. If this value happens to be true, it would unsubscribe + return this.addCallbackAndRun((v) => { + run(v) + }) } } diff --git a/Models/ThemeConfig/Conversion/Validation.ts b/Models/ThemeConfig/Conversion/Validation.ts index 443890639..c480b7f69 100644 --- a/Models/ThemeConfig/Conversion/Validation.ts +++ b/Models/ThemeConfig/Conversion/Validation.ts @@ -3,7 +3,7 @@ import { LayerConfigJson } from "../Json/LayerConfigJson" import LayerConfig from "../LayerConfig" import { Utils } from "../../../Utils" import Constants from "../../Constants" -import { Translation, TypedTranslation } from "../../../UI/i18n/Translation" +import { Translation } from "../../../UI/i18n/Translation" import { LayoutConfigJson } from "../Json/LayoutConfigJson" import LayoutConfig from "../LayoutConfig" import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" @@ -16,7 +16,6 @@ import FilterConfigJson from "../Json/FilterConfigJson" import DeleteConfig from "../DeleteConfig" import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" import Validators from "../../../UI/InputElement/Validators" -import xml2js from "xml2js" class ValidateLanguageCompleteness extends DesugaringStep { private readonly _languages: string[] @@ -631,14 +630,14 @@ class MiscTagRenderingChecks extends DesugaringStep { } const freeformType = json["freeform"]?.["type"] if (freeformType) { - if (Validators.AvailableTypes().indexOf(freeformType) < 0) { + if (Validators.availableTypes.indexOf(freeformType) < 0) { throw ( "At " + context + ".freeform.type is an unknown type: " + freeformType + "; try one of " + - Validators.AvailableTypes().join(", ") + Validators.availableTypes.join(", ") ) } } @@ -943,9 +942,9 @@ export class ValidateFilter extends DesugaringStep { for (let i = 0; i < option.fields.length; i++) { const field = option.fields[i] const type = field.type ?? "string" - if (Validators.AvailableTypes().find((t) => t === type) === undefined) { + if (Validators.availableTypes.find((t) => t === type) === undefined) { const err = `Invalid filter: ${type} is not a valid textfield type (at ${context}.fields[${i}])\n\tTry one of ${Array.from( - Validators.AvailableTypes() + Validators.availableTypes ).join(",")}` errors.push(err) } diff --git a/Models/ThemeViewState.ts b/Models/ThemeViewState.ts index f175a6e94..1a770b73e 100644 --- a/Models/ThemeViewState.ts +++ b/Models/ThemeViewState.ts @@ -119,10 +119,20 @@ export default class ThemeViewState implements SpecialVisualizationState { const indexedElements = this.indexedFeatures this.featureProperties = new FeaturePropertiesStore(indexedElements) const perLayer = new PerLayerFeatureSourceSplitter( - Array.from(this.layerState.filteredLayers.values()), + Array.from(this.layerState.filteredLayers.values()).filter( + (l) => l.layerDef.source !== null + ), indexedElements, { constructStore: (features, layer) => new GeoIndexedStoreForLayer(features, layer), + handleLeftovers: (features) => { + console.warn( + "Got ", + features.length, + "leftover features, such as", + features[0].properties + ) + }, } ) this.perLayer = perLayer.perLayer @@ -141,16 +151,7 @@ export default class ThemeViewState implements SpecialVisualizationState { (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), [fs.layer.isDisplayed] ) - doShowLayer.addCallbackAndRunD((doShow) => - console.log( - "Layer", - fs.layer.layerDef.id, - "is", - doShow, - this.mapProperties.zoom.data, - fs.layer.layerDef.minzoom - ) - ) + new ShowDataLayer(this.map, { layer: fs.layer.layerDef, features: filtered, diff --git a/UI/Base/Tr.svelte b/UI/Base/Tr.svelte index 53e92bbae..236379e48 100644 --- a/UI/Base/Tr.svelte +++ b/UI/Base/Tr.svelte @@ -9,7 +9,7 @@ import FromHtml from "./FromHtml.svelte"; export let t: Translation; - export let tags: Record | undefined; + export let tags: Record | undefined = undefined; // Text for the current language let txt: string | undefined; diff --git a/UI/BigComponents/Geosearch.svelte b/UI/BigComponents/Geosearch.svelte index bc1f30f30..3b472bd0e 100644 --- a/UI/BigComponents/Geosearch.svelte +++ b/UI/BigComponents/Geosearch.svelte @@ -78,6 +78,7 @@ {:else } keypr.key === "Enter" ? performSearch() : undefined} diff --git a/UI/BigComponents/LeftControls.ts b/UI/BigComponents/LeftControls.ts index 7d37489e0..d2bd766b3 100644 --- a/UI/BigComponents/LeftControls.ts +++ b/UI/BigComponents/LeftControls.ts @@ -83,9 +83,6 @@ export default class LeftControls extends Combine { "filters", guiState.filterViewIsOpened ) - const toggledFilter = new MapControlButton(Svg.layers_svg()).onClick(() => - guiState.filterViewIsOpened.setData(true) - ) state.featureSwitchFilter.addCallbackAndRun((f) => { Hotkeys.RegisterHotkey( { nomod: "B" }, @@ -96,8 +93,6 @@ export default class LeftControls extends Combine { ) }) - const filterButton = new Toggle(toggledFilter, undefined, state.featureSwitchFilter) - const mapSwitch = new Toggle( new BackgroundMapSwitch(state, state.backgroundLayer, { enableHotkeys: true }), undefined, diff --git a/UI/BigComponents/SelectedElementView.svelte b/UI/BigComponents/SelectedElementView.svelte index ee380537f..26bc288f1 100644 --- a/UI/BigComponents/SelectedElementView.svelte +++ b/UI/BigComponents/SelectedElementView.svelte @@ -4,6 +4,7 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; import type { SpecialVisualizationState } from "../SpecialVisualization"; import TagRenderingAnswer from "../Popup/TagRenderingAnswer.svelte"; + import TagRenderingQuestion from "../Popup/TagRenderingQuestion.svelte"; export let selectedElement: Feature; export let layer: LayerConfig; @@ -41,7 +42,7 @@

- +

@@ -57,7 +58,11 @@
{#each layer.tagRenderings as config (config.id)} - + {#if config.IsKnown($tags)} + + {:else} + + {/if} {/each}
diff --git a/UI/InputElement/ValidatedInput.svelte b/UI/InputElement/ValidatedInput.svelte new file mode 100644 index 000000000..ef4ac7d82 --- /dev/null +++ b/UI/InputElement/ValidatedInput.svelte @@ -0,0 +1,43 @@ + + +{#if validator.textArea} + +{:else } +
+ + {#if !$isValid} + + {/if} +
+{/if} diff --git a/UI/InputElement/Validator.ts b/UI/InputElement/Validator.ts index 711fae53a..858622ebe 100644 --- a/UI/InputElement/Validator.ts +++ b/UI/InputElement/Validator.ts @@ -17,10 +17,17 @@ export abstract class Validator { * What HTML-inputmode to use */ public readonly inputmode?: string + public readonly textArea: boolean - constructor(name: string, explanation: string | BaseUIElement, inputmode?: string) { + constructor( + name: string, + explanation: string | BaseUIElement, + inputmode?: string, + textArea?: false | boolean + ) { this.name = name this.inputmode = inputmode + this.textArea = textArea ?? false if (this.name.endsWith("textfield")) { this.name = this.name.substr(0, this.name.length - "TextField".length) } @@ -46,7 +53,7 @@ export abstract class Validator { } } - public isValid(string: string, requestCountry: () => string): boolean { + public isValid(string: string, requestCountry?: () => string): boolean { return true } diff --git a/UI/InputElement/Validators.ts b/UI/InputElement/Validators.ts index 25594d3b1..67f0ee807 100644 --- a/UI/InputElement/Validators.ts +++ b/UI/InputElement/Validators.ts @@ -19,8 +19,29 @@ import BaseUIElement from "../BaseUIElement" import Combine from "../Base/Combine" import Title from "../Base/Title" +export type ValidatorType = typeof Validators.availableTypes[number] + export default class Validators { - private static readonly AllValidators: ReadonlyArray = [ + public static readonly availableTypes = [ + "string", + "text", + "date", + "nat", + "int", + "distance", + "direction", + "wikidata", + "pnat", + "float", + "pfloat", + "email", + "url", + "phone", + "opening_hours", + "color", + ] as const + + public static readonly AllValidators: ReadonlyArray = [ new StringValidator(), new TextValidator(), new DateValidator(), @@ -38,8 +59,16 @@ export default class Validators { new OpeningHoursValidator(), new ColorValidator(), ] - public static allTypes: Map = Validators.allTypesDict() + private static _byType = Validators._byTypeConstructor() + + private static _byTypeConstructor(): Map { + const map = new Map() + for (const validator of Validators.AllValidators) { + map.set(validator.name, validator) + } + return map + } public static HelpText(): BaseUIElement { const explanations: BaseUIElement[] = Validators.AllValidators.map((type) => new Combine([new Title(type.name, 3), type.explanation]).SetClass("flex flex-col") @@ -51,15 +80,7 @@ export default class Validators { ]).SetClass("flex flex-col") } - public static AvailableTypes(): string[] { - return Validators.AllValidators.map((tp) => tp.name) - } - - private static allTypesDict(): Map { - const types = new Map() - for (const tp of Validators.AllValidators) { - types.set(tp.name, tp) - } - return types + static get(type: ValidatorType): Validator { + return Validators._byType.get(type) } } diff --git a/UI/InputElement/Validators/DirectionValidator.ts b/UI/InputElement/Validators/DirectionValidator.ts index 34ae9cd7e..5f9618454 100644 --- a/UI/InputElement/Validators/DirectionValidator.ts +++ b/UI/InputElement/Validators/DirectionValidator.ts @@ -9,7 +9,17 @@ export default class DirectionValidator extends IntValidator { ) } + isValid(str): boolean { + if (str.endsWith("°")) { + str = str.substring(0, str.length - 1) + } + return super.isValid(str) + } + reformat(str): string { + if (str.endsWith("°")) { + str = str.substring(0, str.length - 1) + } const n = Number(str) % 360 return "" + n } diff --git a/UI/InputElement/Validators/TextValidator.ts b/UI/InputElement/Validators/TextValidator.ts index 52d30271d..9c38fa814 100644 --- a/UI/InputElement/Validators/TextValidator.ts +++ b/UI/InputElement/Validators/TextValidator.ts @@ -2,6 +2,6 @@ import { Validator } from "../Validator" export default class TextValidator extends Validator { constructor() { - super("text", "A longer piece of text. Uses an textArea instead of a textField", "text") + super("text", "A longer piece of text. Uses an textArea instead of a textField", "text", true) } } diff --git a/UI/Map/ShowDataLayer.ts b/UI/Map/ShowDataLayer.ts index 6724680c2..94194aadb 100644 --- a/UI/Map/ShowDataLayer.ts +++ b/UI/Map/ShowDataLayer.ts @@ -245,8 +245,17 @@ class LineRenderingLayer { }) this._visibility?.addCallbackAndRunD((visible) => { - map.setLayoutProperty(linelayer, "visibility", visible ? "visible" : "none") - map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none") + try { + map.setLayoutProperty(linelayer, "visibility", visible ? "visible" : "none") + map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none") + } catch (e) { + console.warn( + "Error while setting visiblity of layers ", + linelayer, + polylayer, + e + ) + } }) } else { src.setData({ @@ -323,6 +332,7 @@ export default class ShowDataLayer { }) }) } + public static showRange( map: Store, features: FeatureSource, diff --git a/UI/Popup/TagExplanation.svelte b/UI/Popup/TagExplanation.svelte new file mode 100644 index 000000000..57d9367ab --- /dev/null +++ b/UI/Popup/TagExplanation.svelte @@ -0,0 +1,14 @@ + + +{#if tagsFilter !== undefined} + +{/if} diff --git a/UI/Popup/TagRenderingQuestion.svelte b/UI/Popup/TagRenderingQuestion.svelte new file mode 100644 index 000000000..ac5f14cb6 --- /dev/null +++ b/UI/Popup/TagRenderingQuestion.svelte @@ -0,0 +1,53 @@ + + +{#if config.question !== undefined} +
+ +
+ + {config.id} +
+ +
+ + {#if config.questionhint} +
+ +
+ {/if} + + {#if config.freeform?.key && !(config.mappings?.length > 0)} + + + {/if} + + {#if config.mappings !== undefined} +
+ {#each config.mappings as mapping} + {#if mapping.hideInAnswer === true || !(mapping.hideInAnswer) || (console.log(tags) || true) || !(mapping.hideInAnswer?.matchesProperties($tags)) } + + {/if} + {/each} +
+ {/if} + +
+{/if} diff --git a/UI/ThemeViewGUI.svelte b/UI/ThemeViewGUI.svelte index 63018f0c9..02ad5d7af 100644 --- a/UI/ThemeViewGUI.svelte +++ b/UI/ThemeViewGUI.svelte @@ -41,7 +41,7 @@
- +
@@ -64,7 +64,7 @@
mapproperties.zoom.update(z => z+1)}> - + mapproperties.zoom.update(z => z-1)}> @@ -81,7 +81,7 @@ + >
@@ -168,11 +168,14 @@ {#if $selectedElement !== undefined && $selectedLayer !== undefined} -
+
+
+ +
{/if}