refactoring: more fixes, first attempt at tagRenderingAnswer
This commit is contained in:
parent
aaaaf1948d
commit
29372c465e
24 changed files with 278 additions and 113 deletions
|
@ -4,6 +4,7 @@ import SimpleFeatureSource from "./Sources/SimpleFeatureSource"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import { UIEventSource } from "../UIEventSource"
|
import { UIEventSource } from "../UIEventSource"
|
||||||
|
import { feature } from "@turf/turf"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled)
|
* 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,
|
upstream: FeatureSource,
|
||||||
options?: {
|
options?: {
|
||||||
constructStore?: (features: UIEventSource<Feature[]>, layer: FilteredLayer) => T
|
constructStore?: (features: UIEventSource<Feature[]>, layer: FilteredLayer) => T
|
||||||
handleLeftovers?: (featuresWithoutLayer: any[]) => void
|
handleLeftovers?: (featuresWithoutLayer: Feature[]) => void
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const knownLayers = new Map<string, T>()
|
const knownLayers = new Map<string, T>()
|
||||||
|
@ -35,9 +36,6 @@ export default class PerLayerFeatureSourceSplitter<
|
||||||
}
|
}
|
||||||
|
|
||||||
upstream.features.addCallbackAndRunD((features) => {
|
upstream.features.addCallbackAndRunD((features) => {
|
||||||
if (features === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (layers === undefined) {
|
if (layers === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -82,7 +80,7 @@ export default class PerLayerFeatureSourceSplitter<
|
||||||
const src = layerSources.get(id)
|
const src = layerSources.get(id)
|
||||||
|
|
||||||
if (Utils.sameList(src.data, features)) {
|
if (Utils.sameList(src.data, features)) {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
src.setData(features)
|
src.setData(features)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Store, UIEventSource } from "../../UIEventSource"
|
import { Store, UIEventSource } from "../../UIEventSource"
|
||||||
import FeatureSource, { IndexedFeatureSource } from "../FeatureSource"
|
import FeatureSource, { IndexedFeatureSource } from "../FeatureSource"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
|
import { Utils } from "../../../Utils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -35,20 +36,21 @@ export default class FeatureSourceMerger implements IndexedFeatureSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected addData(featuress: Feature[][]) {
|
protected addData(featuress: Feature[][]) {
|
||||||
|
featuress = Utils.NoNull(featuress)
|
||||||
let somethingChanged = false
|
let somethingChanged = false
|
||||||
const all: Map<string, Feature> = new Map()
|
const all: Map<string, Feature> = new Map()
|
||||||
|
const unseen = new Set<string>()
|
||||||
// We seed the dictionary with the previously loaded features
|
// We seed the dictionary with the previously loaded features
|
||||||
const oldValues = this.features.data ?? []
|
const oldValues = this.features.data ?? []
|
||||||
for (const oldValue of oldValues) {
|
for (const oldValue of oldValues) {
|
||||||
all.set(oldValue.properties.id, oldValue)
|
all.set(oldValue.properties.id, oldValue)
|
||||||
|
unseen.add(oldValue.properties.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const features of featuress) {
|
for (const features of featuress) {
|
||||||
if (features === undefined) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for (const f of features) {
|
for (const f of features) {
|
||||||
const id = f.properties.id
|
const id = f.properties.id
|
||||||
|
unseen.delete(id)
|
||||||
if (!all.has(id)) {
|
if (!all.has(id)) {
|
||||||
// This is a new feature
|
// This is a new feature
|
||||||
somethingChanged = true
|
somethingChanged = true
|
||||||
|
@ -67,6 +69,9 @@ export default class FeatureSourceMerger implements IndexedFeatureSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
somethingChanged ||= unseen.size > 0
|
||||||
|
unseen.forEach((id) => all.delete(id))
|
||||||
|
|
||||||
if (!somethingChanged) {
|
if (!somethingChanged) {
|
||||||
// We don't bother triggering an update
|
// We don't bother triggering an update
|
||||||
return
|
return
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default class LayoutSource extends FeatureSourceMerger {
|
||||||
) {
|
) {
|
||||||
const { bounds, zoom } = mapProperties
|
const { bounds, zoom } = mapProperties
|
||||||
// remove all 'special' layers
|
// 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 geojsonlayers = layers.filter((layer) => layer.source.geojsonSource !== undefined)
|
||||||
const osmLayers = 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,
|
zoom,
|
||||||
bounds,
|
bounds,
|
||||||
layoutToUse: featureSwitches.layoutToUse,
|
layers: osmLayers,
|
||||||
|
widenFactor: featureSwitches.layoutToUse.widenFactor,
|
||||||
overpassUrl: featureSwitches.overpassUrl,
|
overpassUrl: featureSwitches.overpassUrl,
|
||||||
overpassTimeout: featureSwitches.overpassTimeout,
|
overpassTimeout: featureSwitches.overpassTimeout,
|
||||||
overpassMaxZoom: featureSwitches.overpassMaxZoom,
|
overpassMaxZoom: featureSwitches.overpassMaxZoom,
|
||||||
|
|
|
@ -3,7 +3,6 @@ import FeatureSource from "../FeatureSource"
|
||||||
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
|
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
|
||||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||||
import { Or } from "../../Tags/Or"
|
import { Or } from "../../Tags/Or"
|
||||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
|
|
||||||
import { Overpass } from "../../Osm/Overpass"
|
import { Overpass } from "../../Osm/Overpass"
|
||||||
import { Utils } from "../../../Utils"
|
import { Utils } from "../../../Utils"
|
||||||
import { TagsFilter } from "../../Tags/TagsFilter"
|
import { TagsFilter } from "../../Tags/TagsFilter"
|
||||||
|
@ -26,7 +25,8 @@ export default class OverpassFeatureSource implements FeatureSource {
|
||||||
|
|
||||||
private readonly state: {
|
private readonly state: {
|
||||||
readonly zoom: Store<number>
|
readonly zoom: Store<number>
|
||||||
readonly layoutToUse: LayoutConfig
|
readonly layers: LayerConfig[]
|
||||||
|
readonly widenFactor: number
|
||||||
readonly overpassUrl: Store<string[]>
|
readonly overpassUrl: Store<string[]>
|
||||||
readonly overpassTimeout: Store<number>
|
readonly overpassTimeout: Store<number>
|
||||||
readonly bounds: Store<BBox>
|
readonly bounds: Store<BBox>
|
||||||
|
@ -37,7 +37,8 @@ export default class OverpassFeatureSource implements FeatureSource {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
state: {
|
state: {
|
||||||
readonly layoutToUse: LayoutConfig
|
readonly layers: LayerConfig[]
|
||||||
|
readonly widenFactor: number
|
||||||
readonly zoom: Store<number>
|
readonly zoom: Store<number>
|
||||||
readonly overpassUrl: Store<string[]>
|
readonly overpassUrl: Store<string[]>
|
||||||
readonly overpassTimeout: Store<number>
|
readonly overpassTimeout: Store<number>
|
||||||
|
@ -117,7 +118,7 @@ export default class OverpassFeatureSource implements FeatureSource {
|
||||||
let lastUsed = 0
|
let lastUsed = 0
|
||||||
|
|
||||||
const layersToDownload = []
|
const layersToDownload = []
|
||||||
for (const layer of this.state.layoutToUse.layers) {
|
for (const layer of this.state.layers) {
|
||||||
if (typeof layer === "string") {
|
if (typeof layer === "string") {
|
||||||
throw "A layer was not expanded!"
|
throw "A layer was not expanded!"
|
||||||
}
|
}
|
||||||
|
@ -130,6 +131,14 @@ export default class OverpassFeatureSource implements FeatureSource {
|
||||||
if (layer.doNotDownload) {
|
if (layer.doNotDownload) {
|
||||||
continue
|
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) {
|
if (layer.source.geojsonSource !== undefined) {
|
||||||
// Not our responsibility to download this layer!
|
// Not our responsibility to download this layer!
|
||||||
continue
|
continue
|
||||||
|
@ -151,7 +160,7 @@ export default class OverpassFeatureSource implements FeatureSource {
|
||||||
do {
|
do {
|
||||||
try {
|
try {
|
||||||
bounds = this.state.bounds.data
|
bounds = this.state.bounds.data
|
||||||
?.pad(this.state.layoutToUse.widenFactor)
|
?.pad(this.state.widenFactor)
|
||||||
?.expandToTileBounds(this.padToZoomLevel?.data)
|
?.expandToTileBounds(this.padToZoomLevel?.data)
|
||||||
|
|
||||||
if (bounds === undefined) {
|
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
|
// 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))
|
// 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)
|
self.features.setData(data.features)
|
||||||
return [bounds, date, layersToDownload]
|
return [bounds, date, layersToDownload]
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -263,7 +263,11 @@ export abstract class Store<T> implements Readable<T> {
|
||||||
public subscribe(run: Subscriber<T> & ((value: T) => void), invalidate?): Unsubscriber {
|
public subscribe(run: Subscriber<T> & ((value: T) => void), invalidate?): Unsubscriber {
|
||||||
// We don't need to do anything with 'invalidate', see
|
// We don't need to do anything with 'invalidate', see
|
||||||
// https://github.com/sveltejs/svelte/issues/3859
|
// 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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||||
import LayerConfig from "../LayerConfig"
|
import LayerConfig from "../LayerConfig"
|
||||||
import { Utils } from "../../../Utils"
|
import { Utils } from "../../../Utils"
|
||||||
import Constants from "../../Constants"
|
import Constants from "../../Constants"
|
||||||
import { Translation, TypedTranslation } from "../../../UI/i18n/Translation"
|
import { Translation } from "../../../UI/i18n/Translation"
|
||||||
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
||||||
import LayoutConfig from "../LayoutConfig"
|
import LayoutConfig from "../LayoutConfig"
|
||||||
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||||
|
@ -16,7 +16,6 @@ import FilterConfigJson from "../Json/FilterConfigJson"
|
||||||
import DeleteConfig from "../DeleteConfig"
|
import DeleteConfig from "../DeleteConfig"
|
||||||
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
||||||
import Validators from "../../../UI/InputElement/Validators"
|
import Validators from "../../../UI/InputElement/Validators"
|
||||||
import xml2js from "xml2js"
|
|
||||||
|
|
||||||
class ValidateLanguageCompleteness extends DesugaringStep<any> {
|
class ValidateLanguageCompleteness extends DesugaringStep<any> {
|
||||||
private readonly _languages: string[]
|
private readonly _languages: string[]
|
||||||
|
@ -631,14 +630,14 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
||||||
}
|
}
|
||||||
const freeformType = json["freeform"]?.["type"]
|
const freeformType = json["freeform"]?.["type"]
|
||||||
if (freeformType) {
|
if (freeformType) {
|
||||||
if (Validators.AvailableTypes().indexOf(freeformType) < 0) {
|
if (Validators.availableTypes.indexOf(freeformType) < 0) {
|
||||||
throw (
|
throw (
|
||||||
"At " +
|
"At " +
|
||||||
context +
|
context +
|
||||||
".freeform.type is an unknown type: " +
|
".freeform.type is an unknown type: " +
|
||||||
freeformType +
|
freeformType +
|
||||||
"; try one of " +
|
"; try one of " +
|
||||||
Validators.AvailableTypes().join(", ")
|
Validators.availableTypes.join(", ")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -943,9 +942,9 @@ export class ValidateFilter extends DesugaringStep<FilterConfigJson> {
|
||||||
for (let i = 0; i < option.fields.length; i++) {
|
for (let i = 0; i < option.fields.length; i++) {
|
||||||
const field = option.fields[i]
|
const field = option.fields[i]
|
||||||
const type = field.type ?? "string"
|
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(
|
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(",")}`
|
).join(",")}`
|
||||||
errors.push(err)
|
errors.push(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,10 +119,20 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
const indexedElements = this.indexedFeatures
|
const indexedElements = this.indexedFeatures
|
||||||
this.featureProperties = new FeaturePropertiesStore(indexedElements)
|
this.featureProperties = new FeaturePropertiesStore(indexedElements)
|
||||||
const perLayer = new PerLayerFeatureSourceSplitter(
|
const perLayer = new PerLayerFeatureSourceSplitter(
|
||||||
Array.from(this.layerState.filteredLayers.values()),
|
Array.from(this.layerState.filteredLayers.values()).filter(
|
||||||
|
(l) => l.layerDef.source !== null
|
||||||
|
),
|
||||||
indexedElements,
|
indexedElements,
|
||||||
{
|
{
|
||||||
constructStore: (features, layer) => new GeoIndexedStoreForLayer(features, layer),
|
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
|
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?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
|
||||||
[fs.layer.isDisplayed]
|
[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, {
|
new ShowDataLayer(this.map, {
|
||||||
layer: fs.layer.layerDef,
|
layer: fs.layer.layerDef,
|
||||||
features: filtered,
|
features: filtered,
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import FromHtml from "./FromHtml.svelte";
|
import FromHtml from "./FromHtml.svelte";
|
||||||
|
|
||||||
export let t: Translation;
|
export let t: Translation;
|
||||||
export let tags: Record<string, string> | undefined;
|
export let tags: Record<string, string> | undefined = undefined;
|
||||||
// Text for the current language
|
// Text for the current language
|
||||||
let txt: string | undefined;
|
let txt: string | undefined;
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else }
|
{:else }
|
||||||
<input
|
<input
|
||||||
|
type="search"
|
||||||
bind:this={inputElement}
|
bind:this={inputElement}
|
||||||
on:keypress={keypr => keypr.key === "Enter" ? performSearch() : undefined}
|
on:keypress={keypr => keypr.key === "Enter" ? performSearch() : undefined}
|
||||||
|
|
||||||
|
|
|
@ -83,9 +83,6 @@ export default class LeftControls extends Combine {
|
||||||
"filters",
|
"filters",
|
||||||
guiState.filterViewIsOpened
|
guiState.filterViewIsOpened
|
||||||
)
|
)
|
||||||
const toggledFilter = new MapControlButton(Svg.layers_svg()).onClick(() =>
|
|
||||||
guiState.filterViewIsOpened.setData(true)
|
|
||||||
)
|
|
||||||
state.featureSwitchFilter.addCallbackAndRun((f) => {
|
state.featureSwitchFilter.addCallbackAndRun((f) => {
|
||||||
Hotkeys.RegisterHotkey(
|
Hotkeys.RegisterHotkey(
|
||||||
{ nomod: "B" },
|
{ nomod: "B" },
|
||||||
|
@ -96,8 +93,6 @@ export default class LeftControls extends Combine {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const filterButton = new Toggle(toggledFilter, undefined, state.featureSwitchFilter)
|
|
||||||
|
|
||||||
const mapSwitch = new Toggle(
|
const mapSwitch = new Toggle(
|
||||||
new BackgroundMapSwitch(state, state.backgroundLayer, { enableHotkeys: true }),
|
new BackgroundMapSwitch(state, state.backgroundLayer, { enableHotkeys: true }),
|
||||||
undefined,
|
undefined,
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||||
import TagRenderingAnswer from "../Popup/TagRenderingAnswer.svelte";
|
import TagRenderingAnswer from "../Popup/TagRenderingAnswer.svelte";
|
||||||
|
import TagRenderingQuestion from "../Popup/TagRenderingQuestion.svelte";
|
||||||
|
|
||||||
export let selectedElement: Feature;
|
export let selectedElement: Feature;
|
||||||
export let layer: LayerConfig;
|
export let layer: LayerConfig;
|
||||||
|
@ -41,7 +42,7 @@
|
||||||
<div class="flex flex-col sm:flex-row flex-grow justify-between">
|
<div class="flex flex-col sm:flex-row flex-grow justify-between">
|
||||||
<!-- Title element-->
|
<!-- Title element-->
|
||||||
<h3>
|
<h3>
|
||||||
<TagRenderingAnswer config={layer.title} {tags} {selectedElement}></TagRenderingAnswer>
|
<TagRenderingAnswer config={layer.title} {selectedElement} {tags}></TagRenderingAnswer>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2">
|
<div class="flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2">
|
||||||
|
@ -57,7 +58,11 @@
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
{#each layer.tagRenderings as config (config.id)}
|
{#each layer.tagRenderings as config (config.id)}
|
||||||
<TagRenderingAnswer {tags} {config} {state}></TagRenderingAnswer>
|
{#if config.IsKnown($tags)}
|
||||||
|
<TagRenderingAnswer {tags} {config} {state}></TagRenderingAnswer>
|
||||||
|
{:else}
|
||||||
|
<TagRenderingQuestion {config} {tags} {state}></TagRenderingQuestion>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
43
UI/InputElement/ValidatedInput.svelte
Normal file
43
UI/InputElement/ValidatedInput.svelte
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||||
|
import type { ValidatorType } from "./Validators";
|
||||||
|
import Validators from "./Validators";
|
||||||
|
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||||
|
import { Translation } from "../i18n/Translation";
|
||||||
|
|
||||||
|
export let value: UIEventSource<string>;
|
||||||
|
// Internal state, only copied to 'value' so that no invalid values leak outside
|
||||||
|
let _value = new UIEventSource(value.data ?? "")
|
||||||
|
export let type: ValidatorType;
|
||||||
|
let validator = Validators.get(type);
|
||||||
|
export let feedback: UIEventSource<Translation> | undefined = undefined
|
||||||
|
_value.addCallbackAndRun(v => {
|
||||||
|
if(validator.isValid(v)){
|
||||||
|
feedback?.setData(undefined)
|
||||||
|
value.setData(v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value.setData(undefined)
|
||||||
|
feedback?.setData(validator.getFeedback(v));
|
||||||
|
})
|
||||||
|
|
||||||
|
if (validator === undefined) {
|
||||||
|
throw "Not a valid type for a validator:" + type;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = _value.map(v => validator.isValid(v));
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if validator.textArea}
|
||||||
|
<textarea bind:value={$_value} inputmode={validator.inputmode ?? "text"}></textarea>
|
||||||
|
{:else }
|
||||||
|
<div class="flex">
|
||||||
|
<input bind:value={$_value} inputmode={validator.inputmode ?? "text"}>
|
||||||
|
{#if !$isValid}
|
||||||
|
<ExclamationIcon class="h-6 w-6 -ml-6"></ExclamationIcon>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -17,10 +17,17 @@ export abstract class Validator {
|
||||||
* What HTML-inputmode to use
|
* What HTML-inputmode to use
|
||||||
*/
|
*/
|
||||||
public readonly inputmode?: string
|
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.name = name
|
||||||
this.inputmode = inputmode
|
this.inputmode = inputmode
|
||||||
|
this.textArea = textArea ?? false
|
||||||
if (this.name.endsWith("textfield")) {
|
if (this.name.endsWith("textfield")) {
|
||||||
this.name = this.name.substr(0, this.name.length - "TextField".length)
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,29 @@ import BaseUIElement from "../BaseUIElement"
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../Base/Combine"
|
||||||
import Title from "../Base/Title"
|
import Title from "../Base/Title"
|
||||||
|
|
||||||
|
export type ValidatorType = typeof Validators.availableTypes[number]
|
||||||
|
|
||||||
export default class Validators {
|
export default class Validators {
|
||||||
private static readonly AllValidators: ReadonlyArray<Validator> = [
|
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<Validator> = [
|
||||||
new StringValidator(),
|
new StringValidator(),
|
||||||
new TextValidator(),
|
new TextValidator(),
|
||||||
new DateValidator(),
|
new DateValidator(),
|
||||||
|
@ -38,8 +59,16 @@ export default class Validators {
|
||||||
new OpeningHoursValidator(),
|
new OpeningHoursValidator(),
|
||||||
new ColorValidator(),
|
new ColorValidator(),
|
||||||
]
|
]
|
||||||
public static allTypes: Map<string, Validator> = Validators.allTypesDict()
|
|
||||||
|
|
||||||
|
private static _byType = Validators._byTypeConstructor()
|
||||||
|
|
||||||
|
private static _byTypeConstructor(): Map<ValidatorType, Validator> {
|
||||||
|
const map = new Map<ValidatorType, Validator>()
|
||||||
|
for (const validator of Validators.AllValidators) {
|
||||||
|
map.set(<ValidatorType>validator.name, validator)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
public static HelpText(): BaseUIElement {
|
public static HelpText(): BaseUIElement {
|
||||||
const explanations: BaseUIElement[] = Validators.AllValidators.map((type) =>
|
const explanations: BaseUIElement[] = Validators.AllValidators.map((type) =>
|
||||||
new Combine([new Title(type.name, 3), type.explanation]).SetClass("flex flex-col")
|
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")
|
]).SetClass("flex flex-col")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AvailableTypes(): string[] {
|
static get(type: ValidatorType): Validator {
|
||||||
return Validators.AllValidators.map((tp) => tp.name)
|
return Validators._byType.get(type)
|
||||||
}
|
|
||||||
|
|
||||||
private static allTypesDict(): Map<string, Validator> {
|
|
||||||
const types = new Map<string, Validator>()
|
|
||||||
for (const tp of Validators.AllValidators) {
|
|
||||||
types.set(tp.name, tp)
|
|
||||||
}
|
|
||||||
return types
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
reformat(str): string {
|
||||||
|
if (str.endsWith("°")) {
|
||||||
|
str = str.substring(0, str.length - 1)
|
||||||
|
}
|
||||||
const n = Number(str) % 360
|
const n = Number(str) % 360
|
||||||
return "" + n
|
return "" + n
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,6 @@ import { Validator } from "../Validator"
|
||||||
|
|
||||||
export default class TextValidator extends Validator {
|
export default class TextValidator extends Validator {
|
||||||
constructor() {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,8 +245,17 @@ class LineRenderingLayer {
|
||||||
})
|
})
|
||||||
|
|
||||||
this._visibility?.addCallbackAndRunD((visible) => {
|
this._visibility?.addCallbackAndRunD((visible) => {
|
||||||
map.setLayoutProperty(linelayer, "visibility", visible ? "visible" : "none")
|
try {
|
||||||
map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none")
|
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 {
|
} else {
|
||||||
src.setData({
|
src.setData({
|
||||||
|
@ -323,6 +332,7 @@ export default class ShowDataLayer {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public static showRange(
|
public static showRange(
|
||||||
map: Store<MlMap>,
|
map: Store<MlMap>,
|
||||||
features: FeatureSource,
|
features: FeatureSource,
|
||||||
|
|
14
UI/Popup/TagExplanation.svelte
Normal file
14
UI/Popup/TagExplanation.svelte
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<script lang="ts">/**
|
||||||
|
* Shows an explanation about the given TagsFilter.
|
||||||
|
*/
|
||||||
|
import { TagsFilter } from "../../Logic/Tags/TagsFilter";
|
||||||
|
import FromHtml from "../Base/FromHtml.svelte";
|
||||||
|
|
||||||
|
export let tagsFilter: TagsFilter;
|
||||||
|
export let properties: Record<string, string> | undefined = {};
|
||||||
|
export let linkToWiki: boolean = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if tagsFilter !== undefined}
|
||||||
|
<FromHtml src={tagsFilter.asHumanString(linkToWiki, false, properties)} />
|
||||||
|
{/if}
|
53
UI/Popup/TagRenderingQuestion.svelte
Normal file
53
UI/Popup/TagRenderingQuestion.svelte
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||||
|
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||||
|
import Tr from "../Base/Tr.svelte";
|
||||||
|
import If from "../Base/If.svelte";
|
||||||
|
import ValidatedInput from "../InputElement/ValidatedInput.svelte";
|
||||||
|
import TagRenderingMapping from "./TagRenderingMapping.svelte";
|
||||||
|
import type { Feature } from "geojson";
|
||||||
|
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
||||||
|
|
||||||
|
export let config: TagRenderingConfig;
|
||||||
|
export let tags: UIEventSource<Record<string, string>>;
|
||||||
|
export let selectedElement: Feature;
|
||||||
|
|
||||||
|
export let state: SpecialVisualizationState;
|
||||||
|
state.featureSwitchIsTesting;
|
||||||
|
|
||||||
|
let freeformInput = new UIEventSource<string>(undefined);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if config.question !== undefined}
|
||||||
|
<div class="border border-black subtle-background">
|
||||||
|
<If condition={state.featureSwitchIsTesting}>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<Tr t={config.question}></Tr>
|
||||||
|
{config.id}
|
||||||
|
</div>
|
||||||
|
<Tr slot="else" t={config.question}></Tr>
|
||||||
|
</If>
|
||||||
|
|
||||||
|
{#if config.questionhint}
|
||||||
|
<div class="subtle">
|
||||||
|
<Tr t={config.question}></Tr>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if config.freeform?.key && !(config.mappings?.length > 0)}
|
||||||
|
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
|
||||||
|
<ValidatedInput type={config.freeform.type} value={freeformInput}></ValidatedInput>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if config.mappings !== undefined}
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{#each config.mappings as mapping}
|
||||||
|
{#if mapping.hideInAnswer === true || !(mapping.hideInAnswer) || (console.log(tags) || true) || !(mapping.hideInAnswer?.matchesProperties($tags)) }
|
||||||
|
<TagRenderingMapping {mapping} {tags} {state} {selectedElement}></TagRenderingMapping>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
|
|
||||||
<div class="h-screen w-screen absolute top-0 left-0 flex">
|
<div class="h-screen w-screen absolute top-0 left-0 flex">
|
||||||
<MaplibreMap class="w-full h-full border border-black" map={maplibremap}></MaplibreMap>
|
<MaplibreMap map={maplibremap}></MaplibreMap>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute top-0 left-0 mt-2 ml-2">
|
<div class="absolute top-0 left-0 mt-2 ml-2">
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
|
|
||||||
<div class="absolute bottom-0 right-0 mb-4 mr-4">
|
<div class="absolute bottom-0 right-0 mb-4 mr-4">
|
||||||
<MapControlButton on:click={() => mapproperties.zoom.update(z => z+1)}>
|
<MapControlButton on:click={() => mapproperties.zoom.update(z => z+1)}>
|
||||||
<ToSvelte class="w-7 h-7 block" construct={Svg.plus_ui}></ToSvelte>
|
<ToSvelte construct={Svg.plus_ui}></ToSvelte>
|
||||||
</MapControlButton>
|
</MapControlButton>
|
||||||
<MapControlButton on:click={() => mapproperties.zoom.update(z => z-1)}>
|
<MapControlButton on:click={() => mapproperties.zoom.update(z => z-1)}>
|
||||||
<ToSvelte class="w-7 h-7 block" construct={Svg.min_ui}></ToSvelte>
|
<ToSvelte class="w-7 h-7 block" construct={Svg.min_ui}></ToSvelte>
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
<If condition={state.featureSwitches.featureSwitchSearch}>
|
<If condition={state.featureSwitches.featureSwitchSearch}>
|
||||||
<Geosearch bounds={state.mapProperties.bounds} layout={state.layout} location={state.mapProperties.location}
|
<Geosearch bounds={state.mapProperties.bounds} layout={state.layout} location={state.mapProperties.location}
|
||||||
{selectedElement} {selectedLayer}
|
{selectedElement} {selectedLayer}
|
||||||
zoom={state.mapProperties.zoom}></Geosearch>
|
></Geosearch>
|
||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -168,11 +168,14 @@
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
{#if $selectedElement !== undefined && $selectedLayer !== undefined}
|
{#if $selectedElement !== undefined && $selectedLayer !== undefined}
|
||||||
<div class="absolute top-0 right-0 normal-background">
|
<div class="absolute top-0 right-0 w-screen h-screen" style="background-color: #00000088">
|
||||||
|
|
||||||
|
<div class="w-full m-8 normal-background rounded overflow-auto">
|
||||||
|
|
||||||
<SelectedElementView layer={$selectedLayer} selectedElement={$selectedElement}
|
<SelectedElementView layer={$selectedLayer} selectedElement={$selectedElement}
|
||||||
tags={$selectedElementTags} state={state}></SelectedElementView>
|
tags={$selectedElementTags} state={state}></SelectedElementView>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -947,6 +947,10 @@ video {
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.-ml-6 {
|
||||||
|
margin-left: -1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mr-3 {
|
.mr-3 {
|
||||||
margin-right: 0.75rem;
|
margin-right: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
20
scripts/BuildMeta.ts
Normal file
20
scripts/BuildMeta.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import Script from "./Script"
|
||||||
|
import Validators from "../UI/InputElement/Validators"
|
||||||
|
|
||||||
|
export default class BuildMeta extends Script {
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
"Prints meta information about the mapcomplete codebase. Used to automate some things"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
async main(args: string[]): Promise<void> {
|
||||||
|
const types = Validators.AllValidators.map((v) => v.name)
|
||||||
|
.map((s) => `"${s}"`)
|
||||||
|
.join(", ")
|
||||||
|
console.log("public static readonly availableTypes = [ " + types + " ] as const")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new BuildMeta().run()
|
17
test.ts
17
test.ts
|
@ -5,11 +5,11 @@ import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"
|
||||||
import * as benches from "./assets/generated/themes/cyclofix.json"
|
import * as benches from "./assets/generated/themes/cyclofix.json"
|
||||||
import { UIEventSource } from "./Logic/UIEventSource"
|
import { UIEventSource } from "./Logic/UIEventSource"
|
||||||
import ThemeViewState from "./Models/ThemeViewState"
|
import ThemeViewState from "./Models/ThemeViewState"
|
||||||
import { SpecialVisualization, SpecialVisualizationState } from "./UI/SpecialVisualization"
|
|
||||||
import { Feature } from "geojson"
|
|
||||||
import Combine from "./UI/Base/Combine"
|
import Combine from "./UI/Base/Combine"
|
||||||
import SpecialVisualizations from "./UI/SpecialVisualizations"
|
import SpecialVisualizations from "./UI/SpecialVisualizations"
|
||||||
import BaseUIElement from "./UI/BaseUIElement"
|
import ValidatedInput from "./UI/InputElement/ValidatedInput.svelte"
|
||||||
|
import { VariableUiElement } from "./UI/Base/VariableUIElement"
|
||||||
|
import { Translation } from "./UI/i18n/Translation"
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
new FixedUiElement("").AttachTo("extradiv")
|
new FixedUiElement("").AttachTo("extradiv")
|
||||||
|
@ -18,7 +18,7 @@ async function main() {
|
||||||
main.AttachTo("maindiv")
|
main.AttachTo("maindiv")
|
||||||
}
|
}
|
||||||
|
|
||||||
async function test() {
|
async function testspecial() {
|
||||||
const layout = new LayoutConfig(<any>benches, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
const layout = new LayoutConfig(<any>benches, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||||
const state = new ThemeViewState(layout)
|
const state = new ThemeViewState(layout)
|
||||||
|
|
||||||
|
@ -27,5 +27,12 @@ async function test() {
|
||||||
)
|
)
|
||||||
new Combine(all).AttachTo("maindiv")
|
new Combine(all).AttachTo("maindiv")
|
||||||
}
|
}
|
||||||
// test().then((_) => {}) /*/
|
async function test() {
|
||||||
|
const value = new UIEventSource("Hello world!")
|
||||||
|
const feedback = new UIEventSource<Translation>(undefined)
|
||||||
|
new SvelteUIElement(ValidatedInput, { type: "direction", value, feedback }).AttachTo("maindiv")
|
||||||
|
new VariableUiElement(feedback).AttachTo("extradiv")
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
test().then((_) => {}) /*/
|
||||||
main().then((_) => {}) //*/
|
main().then((_) => {}) //*/
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { TagRenderingConfigJson } from "../../../Models/ThemeConfig/Json/TagRenderingConfigJson"
|
|
||||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
|
|
||||||
import TagRenderingQuestion from "../../../UI/Popup/TagRenderingQuestion"
|
|
||||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
|
||||||
import { describe, expect, it } from "vitest"
|
|
||||||
|
|
||||||
describe("TagRenderingQuestion", () => {
|
|
||||||
it("should have a freeform text field with the user defined placeholder", () => {
|
|
||||||
const configJson = <TagRenderingConfigJson>{
|
|
||||||
id: "test-tag-rendering",
|
|
||||||
question: "Question?",
|
|
||||||
render: "Rendering {capacity}",
|
|
||||||
freeform: {
|
|
||||||
key: "capacity",
|
|
||||||
type: "pnat",
|
|
||||||
placeholder: "Some user defined placeholder",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const config = new TagRenderingConfig(configJson, "test")
|
|
||||||
const ui = new TagRenderingQuestion(new UIEventSource<any>({}), config)
|
|
||||||
|
|
||||||
const html = ui.ConstructElement()
|
|
||||||
expect(html.getElementsByTagName("input")[0]["placeholder"]).toBe(
|
|
||||||
"Some user defined placeholder"
|
|
||||||
)
|
|
||||||
}) //*/
|
|
||||||
/*
|
|
||||||
it("should have a freeform text field with a type explanation", () => {
|
|
||||||
Locale.language.setData("en")
|
|
||||||
const configJson = <TagRenderingConfigJson>{
|
|
||||||
id: "test-tag-rendering",
|
|
||||||
question: "Question?",
|
|
||||||
render: "Rendering {capacity}",
|
|
||||||
freeform: {
|
|
||||||
key: "capacity",
|
|
||||||
type: "pnat",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const config = new TagRenderingConfig(configJson, "test")
|
|
||||||
const ui = new TagRenderingQuestion(new UIEventSource<any>({}), config)
|
|
||||||
const html = ui.ConstructElement()
|
|
||||||
expect(html.getElementsByTagName("input")[0]["placeholder"]).toBe(
|
|
||||||
"capacity (a positive, whole number)"
|
|
||||||
)
|
|
||||||
})//*/
|
|
||||||
})
|
|
Loading…
Reference in a new issue