Refactoring: dismantle 'inputHelpers'

This commit is contained in:
Pieter Vander Vennet 2025-07-28 03:14:33 +02:00
parent 29dc7d1e03
commit 7c42758b42
26 changed files with 485 additions and 439 deletions

@ -1 +1 @@
Subproject commit b7b29d20e40bde9144c719a2b59484c04cc79b9f
Subproject commit a48aaffec4ca59a2129834207e72ee3df85d2cd6

View file

@ -1,9 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg" width="375px" height="375px" viewBox="0 0 375 375" version="1.1">
<g id="surface1">
<path style="fill:none;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(97.254902%,100%,96.078432%);stroke-opacity:1;stroke-miterlimit:4;" d="M -177.48351 -16.993714 C -177.484166 101.48875 -226.288922 214.739751 -312.411923 296.10684 C -398.528411 377.467771 -514.363074 419.770216 -632.651731 413.060164 " transform="matrix(-0.316636,-0.299423,0.299423,-0.316636,0,0)"/>
<path style="fill:none;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:4;" d="M 743.478328 134.561833 L 430.253662 430.253662 L 117.002105 134.508051 " transform="matrix(0.435789,0,0,0.435789,0,0)"/>
<path style="fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:4;" d="M -177.48351 -16.993714 C -177.484166 101.48875 -226.288922 214.739751 -312.411923 296.10684 C -398.528411 377.467771 -514.363074 419.770216 -632.651731 413.060164 " transform="matrix(-0.316636,-0.299423,0.299423,-0.316636,0,0)"/>
<path style="fill:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:4;" d="M 743.478328 134.561833 L 430.253662 430.253662 L 117.002105 134.508051 " transform="matrix(0.435789,0,0,0.435789,0,0)"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="375px"
height="375px"
viewBox="0 0 375 375"
version="1.1"
id="svg4"
sodipodi:docname="direction_stroke.svg"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs4" />
<sodipodi:namedview
id="namedview4"
pagecolor="#ffffff"
bordercolor="#999999"
borderopacity="1"
inkscape:showpageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.87158493"
inkscape:cx="-23.52037"
inkscape:cy="19.504697"
inkscape:window-width="1920"
inkscape:window-height="1005"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<g
id="surface1"
style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-opacity:1"
transform="translate(0,4)">
<path
style="color:#000000;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;-inkscape-stroke:none"
d="m -177.48437,-20.435547 a 3.4420288,3.4420288 0 0 0 -3.44141,3.441406 c -6.5e-4,117.537141 -48.41377,229.881731 -133.84961,310.599611 -85.42934,80.71172 -200.33664,122.6741 -317.68164,116.01758 a 3.4420288,3.4420288 0 0 0 -3.63086,3.24218 3.4420288,3.4420288 0 0 0 3.24219,3.63086 c 119.23207,6.76357 235.9934,-35.87674 322.79687,-117.88672 86.80999,-82.01614 136.00715,-196.17596 136.00781,-315.603511 a 3.4420288,3.4420288 0 0 0 -3.44335,-3.441406 z"
id="path1"
transform="matrix(-0.316636,-0.299423,0.299423,-0.316636,0,0)" />
<path
style="color:#000000;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-opacity:1;-inkscape-stroke:none"
d="m 119.36523,132.00586 -4.72656,5.00586 315.61524,297.97461 315.58789,-297.92188 -4.72657,-5.00586 -310.86132,293.46094 z"
id="path2"
transform="scale(0.435789)" />
<path
style="color:#000000;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;-inkscape-stroke:none"
d="m -177.48437,-20.435547 a 3.4420288,3.4420288 0 0 0 -3.44141,3.441406 c -6.5e-4,117.537141 -48.41377,229.881731 -133.84961,310.599611 -85.42934,80.71172 -200.33664,122.6741 -317.68164,116.01758 a 3.4420288,3.4420288 0 0 0 -3.63086,3.24218 3.4420288,3.4420288 0 0 0 3.24219,3.63086 c 119.23207,6.76357 235.9934,-35.87674 322.79687,-117.88672 86.80999,-82.01614 136.00715,-196.17596 136.00781,-315.603511 a 3.4420288,3.4420288 0 0 0 -3.44335,-3.441406 z"
id="path3"
transform="matrix(-0.316636,-0.299423,0.299423,-0.316636,0,0)" />
<path
style="color:#000000;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-opacity:1;-inkscape-stroke:none"
d="m 119.36523,132.00586 -4.72656,5.00586 315.61524,297.97461 315.58789,-297.92188 -4.72657,-5.00586 -310.86132,293.46094 z"
id="path4"
transform="scale(0.435789)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before After
Before After

View file

@ -2209,6 +2209,10 @@ input[type="range"].range-lg::-moz-range-thumb {
min-width: 8rem;
}
.min-w-48 {
min-width: 12rem;
}
.min-w-6 {
min-width: 1.5rem;
}

View file

@ -22,6 +22,7 @@ import { QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRender
import MarkdownUtils from "../../Utils/MarkdownUtils"
import { And } from "../../Logic/Tags/And"
import OsmWiki from "../../Logic/Osm/OsmWiki"
import { UnitUtils } from "../UnitUtils"
export default class LayerConfig extends WithContextLoader {
public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const
@ -312,7 +313,7 @@ export default class LayerConfig extends WithContextLoader {
)
}
this.units = (json.units ?? []).flatMap((unitJson, i) =>
Unit.fromJson(unitJson, this.tagRenderings, `${context}.unit[${i}]`)
UnitUtils.fromJson()(unitJson, this.tagRenderings, `${context}.unit[${i}]`)
)
{
let filter = json.filter

View file

@ -1,14 +1,9 @@
import BaseUIElement from "../UI/BaseUIElement"
import { Denomination } from "./Denomination"
import UnitConfigJson from "./ThemeConfig/Json/UnitConfigJson"
import unit from "../../assets/layers/unit/unit.json"
import TagRenderingConfig from "./ThemeConfig/TagRenderingConfig"
import Validators, { ValidatorType } from "../UI/InputElement/Validators"
import { Validator } from "../UI/InputElement/Validator"
import FloatValidator from "../UI/InputElement/Validators/FloatValidator"
export class Unit {
private static allUnits = this.initUnits()
public readonly appliesToKeys: Set<string>
public readonly denominations: Denomination[]
public readonly denominationsSorted: Denomination[]
@ -85,226 +80,7 @@ export class Unit {
}
}
static fromJson(
json:
| UnitConfigJson
| Record<
string,
string | { quantity: string; denominations: string[]; inverted?: boolean }
>,
tagRenderings: TagRenderingConfig[],
ctx: string
): Unit[] {
const types: Record<string, ValidatorType> = {}
for (const tagRendering of tagRenderings) {
if (tagRendering.freeform?.type) {
types[tagRendering.freeform.key] = tagRendering.freeform.type
}
}
if (!json.appliesToKey && !json.quantity) {
return this.loadFromLibrary(<any>json, types, ctx)
}
return this.parse(<UnitConfigJson>json, types, ctx)
}
private static parseDenomination(
json: UnitConfigJson,
validator: Validator,
appliesToKey: string,
ctx: string
): Unit {
const applicable = json.applicableUnits.map((u, i) =>
Denomination.fromJson(u, validator, `${ctx}.units[${i}]`)
)
if (
json.defaultInput &&
!applicable.some((denom) => denom.canonical.trim() === json.defaultInput)
) {
throw `${ctx}: no denomination has the specified default denomination. The default denomination is '${
json.defaultInput
}', but the available denominations are ${applicable
.map((denom) => denom.canonical)
.join(", ")}`
}
return new Unit(
json.quantity ?? "",
appliesToKey === undefined ? undefined : [appliesToKey],
applicable,
json.eraseInvalidValues ?? false,
validator
)
}
/**
*
* // Should detect invalid defaultInput
* let threwError = false
* try{
* Unit.parse({
* appliesToKey: ["length"],
* defaultInput: "xcm",
* applicableUnits: [
* {
* canonicalDenomination: "m",
* useIfNoUnitGiven: true,
* human: "meter"
* }
* ]
* },"test")
* }catch(e){
* threwError = true
* }
* threwError // => true
*
* // Should work
* Unit.parse({
* appliesToKey: ["length"],
* defaultInput: "xcm",
* applicableUnits: [
* {
* canonicalDenomination: "m",
* useIfNoUnitGiven: true,
* humen: "meter"
* },
* {
* canonicalDenomination: "cm",
* human: "centimeter"
* }
* ]
* }, "test")
*/
private static parse(
json: UnitConfigJson,
types: Record<string, ValidatorType>,
ctx: string
): Unit[] {
const appliesTo = json.appliesToKey
for (let i = 0; i < (appliesTo ?? []).length; i++) {
const key = appliesTo[i]
if (key.trim() !== key) {
throw `${ctx}.appliesToKey[${i}] is invalid: it starts or ends with whitespace`
}
}
if ((json.applicableUnits ?? []).length === 0) {
throw `${ctx}: define at least one applicable unit`
}
// Some keys do have unit handling
const units: Unit[] = []
if (appliesTo === undefined) {
units.push(this.parseDenomination(json, Validators.get("float"), undefined, ctx))
}
for (const key of appliesTo ?? []) {
const validator = Validators.get(types[key] ?? "float")
units.push(this.parseDenomination(json, validator, undefined, ctx))
}
return units
}
private static initUnits(): Map<string, Unit> {
const m = new Map<string, Unit>()
const units = (<UnitConfigJson[]>unit.units).flatMap((json, i) =>
this.parse(json, {}, "unit.json.units." + i)
)
for (const unit of units) {
m.set(unit.quantity, unit)
}
return m
}
private static getFromLibrary(name: string, ctx: string): Unit {
const loaded = this.allUnits.get(name)
if (loaded === undefined) {
throw (
"No unit with quantity name " +
name +
" found (at " +
ctx +
"). Try one of: " +
Array.from(this.allUnits.keys()).join(", ")
)
}
return loaded
}
private static loadFromLibrary(
spec: Record<
string,
| string
| { quantity: string; denominations: string[]; canonical?: string; inverted?: boolean }
>,
types: Record<string, ValidatorType>,
ctx: string
): Unit[] {
const units: Unit[] = []
for (const key in spec) {
const toLoad = spec[key]
const validator = Validators.get(types[key] ?? "float")
if (typeof toLoad === "string") {
const loaded = this.getFromLibrary(toLoad, ctx)
units.push(
new Unit(
loaded.quantity,
[key],
loaded.denominations,
loaded.eraseInvalid,
validator,
toLoad["inverted"]
)
)
continue
}
const loaded = this.getFromLibrary(toLoad.quantity, ctx)
const quantity = toLoad.quantity
const fetchDenom = (d: string): Denomination => {
const found = loaded.denominations.find(
(denom) => denom.canonical.toLowerCase() === d
)
if (!found) {
throw (
`Could not find a denomination \`${d}\`for quantity ${quantity} at ${ctx}. Perhaps you meant to use on of ` +
loaded.denominations.map((d) => d.canonical).join(", ")
)
}
return found
}
if (!Array.isArray(toLoad.denominations)) {
throw (
"toLoad is not an array. Did you forget the [ and ] around the denominations at " +
ctx +
"?"
)
}
const denoms = toLoad.denominations
.map((d) => d.toLowerCase())
.map((d) => fetchDenom(d))
.map((d) => d.withValidator(validator))
if (toLoad.canonical) {
const canonical = fetchDenom(toLoad.canonical).withValidator(validator)
denoms.unshift(canonical.withBlankCanonical())
}
units.push(
new Unit(
loaded.quantity,
[key],
denoms,
loaded.eraseInvalid,
validator,
toLoad["inverted"]
)
)
}
return units
}
isApplicableToKey(key: string | undefined): boolean {
if (key === undefined) {

232
src/Models/UnitUtils.ts Normal file
View file

@ -0,0 +1,232 @@
import UnitConfigJson from "./ThemeConfig/Json/UnitConfigJson"
import TagRenderingConfig from "./ThemeConfig/TagRenderingConfig"
import Validators, { ValidatorType } from "../UI/InputElement/Validators"
import { Validator } from "../UI/InputElement/Validator"
import { Denomination } from "./Denomination"
import unit from "../../assets/layers/unit/unit.json"
import { Unit } from "./Unit"
export class UnitUtils {
private static allUnits = this.initUnits()
static fromJson(
json:
| UnitConfigJson
| Record<
string,
string | { quantity: string; denominations: string[]; inverted?: boolean }
>,
tagRenderings: TagRenderingConfig[],
ctx: string,
): Unit[] {
const types: Record<string, ValidatorType> = {}
for (const tagRendering of tagRenderings) {
if (tagRendering.freeform?.type) {
types[tagRendering.freeform.key] = tagRendering.freeform.type
}
}
if (!json.appliesToKey && !json.quantity) {
return this.loadFromLibrary(<any>json, types, ctx)
}
return this.parse(<UnitConfigJson>json, types, ctx)
}
private static parseDenomination(
json: UnitConfigJson,
validator: Validator,
appliesToKey: string,
ctx: string,
): Unit {
const applicable = json.applicableUnits.map((u, i) =>
Denomination.fromJson(u, validator, `${ctx}.units[${i}]`),
)
if (
json.defaultInput &&
!applicable.some((denom) => denom.canonical.trim() === json.defaultInput)
) {
throw `${ctx}: no denomination has the specified default denomination. The default denomination is '${
json.defaultInput
}', but the available denominations are ${applicable
.map((denom) => denom.canonical)
.join(", ")}`
}
return new Unit(
json.quantity ?? "",
appliesToKey === undefined ? undefined : [appliesToKey],
applicable,
json.eraseInvalidValues ?? false,
validator,
)
}
/**
*
* // Should detect invalid defaultInput
* let threwError = false
* try{
* Unit.parse({
* appliesToKey: ["length"],
* defaultInput: "xcm",
* applicableUnits: [
* {
* canonicalDenomination: "m",
* useIfNoUnitGiven: true,
* human: "meter"
* }
* ]
* },"test")
* }catch(e){
* threwError = true
* }
* threwError // => true
*
* // Should work
* Unit.parse({
* appliesToKey: ["length"],
* defaultInput: "xcm",
* applicableUnits: [
* {
* canonicalDenomination: "m",
* useIfNoUnitGiven: true,
* humen: "meter"
* },
* {
* canonicalDenomination: "cm",
* human: "centimeter"
* }
* ]
* }, "test")
*/
private static parse(
json: UnitConfigJson,
types: Record<string, ValidatorType>,
ctx: string,
): Unit[] {
const appliesTo = json.appliesToKey
for (let i = 0; i < (appliesTo ?? []).length; i++) {
const key = appliesTo[i]
if (key.trim() !== key) {
throw `${ctx}.appliesToKey[${i}] is invalid: it starts or ends with whitespace`
}
}
if ((json.applicableUnits ?? []).length === 0) {
throw `${ctx}: define at least one applicable unit`
}
// Some keys do have unit handling
const units: Unit[] = []
if (appliesTo === undefined) {
units.push(this.parseDenomination(json, Validators.get("float"), undefined, ctx))
}
for (const key of appliesTo ?? []) {
const validator = Validators.get(types[key] ?? "float")
units.push(this.parseDenomination(json, validator, undefined, ctx))
}
return units
}
private static initUnits(): Map<string, Unit> {
const m = new Map<string, Unit>()
const units = (<UnitConfigJson[]>unit.units).flatMap((json, i) =>
this.parse(json, {}, "unit.json.units." + i),
)
for (const unit of units) {
m.set(unit.quantity, unit)
}
return m
}
private static getFromLibrary(name: string, ctx: string): Unit {
const loaded = this.allUnits.get(name)
if (loaded === undefined) {
throw (
"No unit with quantity name " +
name +
" found (at " +
ctx +
"). Try one of: " +
Array.from(this.allUnits.keys()).join(", ")
)
}
return loaded
}
private static loadFromLibrary(
spec: Record<
string,
| string
| { quantity: string; denominations: string[]; canonical?: string; inverted?: boolean }
>,
types: Record<string, ValidatorType>,
ctx: string,
): Unit[] {
const units: Unit[] = []
for (const key in spec) {
const toLoad = spec[key]
const validator = Validators.get(types[key] ?? "float")
if (typeof toLoad === "string") {
const loaded = this.getFromLibrary(toLoad, ctx)
units.push(
new Unit(
loaded.quantity,
[key],
loaded.denominations,
loaded.eraseInvalid,
validator,
toLoad["inverted"],
),
)
continue
}
const loaded = this.getFromLibrary(toLoad.quantity, ctx)
const quantity = toLoad.quantity
const fetchDenom = (d: string): Denomination => {
const found = loaded.denominations.find(
(denom) => denom.canonical.toLowerCase() === d,
)
if (!found) {
throw (
`Could not find a denomination \`${d}\`for quantity ${quantity} at ${ctx}. Perhaps you meant to use on of ` +
loaded.denominations.map((d) => d.canonical).join(", ")
)
}
return found
}
if (!Array.isArray(toLoad.denominations)) {
throw (
"toLoad is not an array. Did you forget the [ and ] around the denominations at " +
ctx +
"?"
)
}
const denoms = toLoad.denominations
.map((d) => d.toLowerCase())
.map((d) => fetchDenom(d))
.map((d) => d.withValidator(validator))
if (toLoad.canonical) {
const canonical = fetchDenom(toLoad.canonical).withValidator(validator)
denoms.unshift(canonical.withBlankCanonical())
}
units.push(
new Unit(
loaded.quantity,
[key],
denoms,
loaded.eraseInvalid,
validator,
toLoad["inverted"],
),
)
}
return units
}
}

View file

@ -6,25 +6,35 @@
import MaplibreMap from "../../Map/MaplibreMap.svelte"
import Direction_stroke from "../../../assets/svg/Direction_stroke.svelte"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import type Feature from "geojson"
import { GeoOperations } from "../../../Logic/GeoOperations"
/**
* A visualisation to pick a direction on a map background.
*/
export let value: UIEventSource<undefined | string>
export let state: SpecialVisualizationState = undefined
export let mapProperties: Partial<MapProperties> & {
readonly location: UIEventSource<{ lon: number; lat: number }>
export let args: any[] = []
export let feature: Feature
let [lon, lat] = GeoOperations.centerpointCoordinates(feature)
let mapProperties: MapProperties = {
location: new UIEventSource({ lon, lat }),
zoom: new UIEventSource(args[0] !== undefined ? Number(args[0]) : 17),
rasterLayer: state.mapProperties.rasterLayer,
rotation: state.mapProperties.rotation,
}
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let mla = new MapLibreAdaptor(map, mapProperties)
mla.allowMoving.setData(false)
mla.allowZooming.setData(false)
state?.mapProperties?.rasterLayer?.addCallbackAndRunD((l) => mla.rasterLayer.set(l))
let rotation = new UIEventSource(value.data)
rotation.addCallbackD(rotation => {
const r = (rotation + mapProperties.rotation.data + 360) % 360
console.log("Setting value to", r)
value.setData(""+Math.floor(r))
}, [mapProperties.rotation])
let directionElem: HTMLElement | undefined
$: value.addCallbackAndRunD((degrees) => {
if (directionElem === undefined) {
$: rotation.addCallbackAndRunD((degrees) => {
if (!directionElem?.style) {
return
}
directionElem.style.rotate = degrees + "deg"
@ -32,13 +42,14 @@
let mainElem: HTMLElement
function onPosChange(x: number, y: number) {
const rect = mainElem.getBoundingClientRect()
const dx = -(rect.left + rect.right) / 2 + x
const dy = (rect.top + rect.bottom) / 2 - y
const angle = (180 * Math.atan2(dy, dx)) / Math.PI
const angleGeo = Math.floor((450 - angle) % 360)
value.setData("" + angleGeo)
rotation.setData(angleGeo)
}
let isDown = false
@ -46,7 +57,7 @@
<div
bind:this={mainElem}
class="relative h-48 w-48 cursor-pointer overflow-hidden"
class="relative h-48 min-w-48 w-full cursor-pointer overflow-hidden rounded-xl"
on:click={(e) => onPosChange(e.x, e.y)}
on:mousedown={(e) => {
isDown = true
@ -71,7 +82,7 @@
<MaplibreMap mapProperties={mla} {map} />
</div>
<div bind:this={directionElem} class="absolute left-0 top-0 h-full w-full">
<div bind:this={directionElem} class="absolute left-0 top-0 h-full w-full p-1">
<Direction_stroke />
</div>
</div>

View file

@ -1,67 +1,25 @@
<script lang="ts">
/**
* Constructs an input helper element for the given type.
* Note that all values are stringified
*/
import { UIEventSource } from "../../Logic/UIEventSource"
import type { ValidatorType } from "./Validators"
import InputHelpers from "./InputHelpers"
import Validators from "./Validators"
import type { Feature } from "geojson"
import ImageHelper from "./Helpers/ImageHelper.svelte"
import TranslationInput from "./Helpers/TranslationInput.svelte"
import TagInput from "./Helpers/TagInput.svelte"
import SimpleTagInput from "./Helpers/SimpleTagInput.svelte"
import DirectionInput from "./Helpers/DirectionInput.svelte"
import DateInput from "./Helpers/DateInput.svelte"
import ColorInput from "./Helpers/ColorInput.svelte"
import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte"
import SlopeInput from "./Helpers/SlopeInput.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import WikidataInputHelper from "./Helpers/WikidataInputHelper.svelte"
import type { Validator } from "./Validator"
import DistanceInput from "./Helpers/DistanceInput.svelte"
import TimeInput from "./Helpers/TimeInput.svelte"
import CollectionTimes from "./Helpers/CollectionTimes/CollectionTimes.svelte"
export let type: ValidatorType
export let value: UIEventSource<string | object>
export let feature: Feature = undefined
export let args: (string | number | boolean)[] | any = undefined
export let state: SpecialVisualizationState = undefined
let validator = Validators.get(type)
let validatorHelper: Validator = validator.inputHelper
</script>
{#if type === "translation"}
<TranslationInput {value} on:submit {args} />
{:else if type === "direction"}
<DirectionInput
{value}
{state}
mapProperties={InputHelpers.constructMapProperties({ feature, args: args ?? [] })}
/>
{:else if type === "date"}
<DateInput {value} />
{:else if type === "time"}
<TimeInput {value} />
{:else if type === "points_in_time"}
<CollectionTimes {value} />
{:else if type === "color"}
<ColorInput {value} />
{:else if type === "image"}
<ImageHelper {value} />
{:else if type === "tag"}
<TagInput {value} on:submit />
{:else if type === "simple_tag"}
<SimpleTagInput {value} {args} on:submit />
{:else if type === "opening_hours"}
<OpeningHoursInput {value} {args} />
{:else if type === "slope"}
<SlopeInput {value} {feature} {state} />
{:else if type === "wikidata"}
<WikidataInputHelper {value} {feature} {state} {args} />
{:else if type === "distance"}
<DistanceInput {value} {state} {feature} {args} />
{:else}
<slot name="fallback" />
{#if type === "distance"}
<DistanceInput {value} {feature} {state} {args} />
{:else if validatorHelper !== undefined}
<svelte:component this={validatorHelper} {value} {feature} {state} {args} on:submit />
{/if}

View file

@ -1,67 +0,0 @@
import { UIEventSource } from "../../Logic/UIEventSource"
import { MapProperties } from "../../Models/MapProperties"
import { Feature } from "geojson"
import { GeoOperations } from "../../Logic/GeoOperations"
import { ValidatorType } from "./Validators"
export interface InputHelperProperties {
/**
* Extra arguments which might be used by the helper component
*/
args?: (string | number | boolean)[]
/**
* Used for map-based helpers, such as 'direction'
*/
mapProperties?: Partial<MapProperties> & {
readonly location: UIEventSource<{ lon: number; lat: number }>
}
/**
* The feature that this question is about
* Used by the wikidata-input to read properties, which in turn is used to read the name to pre-populate the text field.
* Additionally, used for direction input to set the default location if no mapProperties with location are given
*/
feature?: Feature
}
export default class InputHelpers {
public static hideInputField: ValidatorType[] = ["translation", "simple_tag", "tag","time"]
/**
* Constructs a mapProperties-object for the given properties.
* Assumes that the first helper-args contains the desired zoom-level
* Used for the 'direction' input helper
* @param properties
* @private
*/
public static constructMapProperties(
properties: InputHelperProperties
): Partial<MapProperties> {
let location = properties?.mapProperties?.location
if (!location) {
const [lon, lat] = GeoOperations.centerpointCoordinates(properties.feature)
location = new UIEventSource<{ lon: number; lat: number }>({ lon, lat })
}
let mapProperties: Partial<MapProperties> = properties?.mapProperties ?? { location }
if (!mapProperties.location) {
mapProperties = { ...mapProperties, location }
}
let zoom = 17
if (properties?.args?.[0] !== undefined) {
zoom = Number(properties.args[0])
if (isNaN(zoom)) {
throw "Invalid zoom level for argument at 'length'-input"
}
}
if (!mapProperties.zoom) {
mapProperties = { ...mapProperties, zoom: new UIEventSource<number>(zoom) }
}
if (!mapProperties.rasterLayer) {
/* mapProperties = {
...mapProperties, rasterLayer: properties?.mapProperties?.rasterLayer
}*/
}
return mapProperties
}
}

View file

@ -1,6 +1,7 @@
import { Translation } from "../i18n/Translation"
import Translations from "../i18n/Translations"
import { HTMLInputTypeAttribute } from "svelte/elements"
import { ComponentType } from "svelte/types/runtime/internal/dev"
/**
* A 'TextFieldValidator' contains various methods to check and cleanup an entered value or to give feedback.
@ -21,6 +22,8 @@ export abstract class Validator {
public readonly textArea: boolean
public readonly isMeta?: boolean
public readonly inputHelper : ComponentType = undefined
public readonly hideInputField: boolean = false
constructor(
name: string,
@ -80,4 +83,5 @@ export abstract class Validator {
public validateArguments(args: string): undefined | string {
return undefined
}
}

View file

@ -1,40 +1,37 @@
import { Validator } from "./Validator"
import FloatValidator from "./Validators/FloatValidator"
import StringValidator from "./Validators/StringValidator"
import PFloatValidator from "./Validators/PFloatValidator"
import TextValidator from "./Validators/TextValidator"
import DateValidator from "./Validators/DateValidator"
import { TimeValidator } from "./Validators/TimeValidator"
import NatValidator from "./Validators/NatValidator"
import IntValidator from "./Validators/IntValidator"
import DistanceValidator from "./Validators/DistanceValidator"
import PNatValidator from "./Validators/PNatValidator"
import DirectionValidator from "./Validators/DirectionValidator"
import WikidataValidator from "./Validators/WikidataValidator"
import PNatValidator from "./Validators/PNatValidator"
import FloatValidator from "./Validators/FloatValidator"
import PFloatValidator from "./Validators/PFloatValidator"
import EmailValidator from "./Validators/EmailValidator"
import UrlValidator from "./Validators/UrlValidator"
import PhoneValidator from "./Validators/PhoneValidator"
import OpeningHoursValidator from "./Validators/OpeningHoursValidator"
import PhoneValidator from "./Validators/PhoneValidator"
import ColorValidator from "./Validators/ColorValidator"
import SimpleTagValidator from "./Validators/SimpleTagValidator"
import ImageUrlValidator from "./Validators/ImageUrlValidator"
import TagKeyValidator from "./Validators/TagKeyValidator"
import TranslationValidator from "./Validators/TranslationValidator"
import FediverseValidator from "./Validators/FediverseValidator"
import IconValidator from "./Validators/IconValidator"
import TagValidator from "./Validators/TagValidator"
import IdValidator from "./Validators/IdValidator"
import SlopeValidator from "./Validators/SlopeValidator"
import CollectionTimesValidator from "./Validators/CollectionTimesValidator"
import IdValidator from "./Validators/IdValidator"
import FediverseValidator from "./Validators/FediverseValidator"
import SimpleTagValidator from "./Validators/SimpleTagValidator"
import VeloparkValidator from "./Validators/VeloparkValidator"
import NameSuggestionIndexValidator from "./Validators/NameSuggestionIndexValidator"
import CurrencyValidator from "./Validators/CurrencyValidator"
import RegexValidator from "./Validators/RegexValidator"
import { TimeValidator } from "./Validators/TimeValidator"
import CollectionTimesValidator from "./Validators/CollectionTimesValidator"
import CurrencyValidator from "./Validators/CurrencyValidator"
import TagValidator from "./Validators/TagValidator"
import TranslationValidator from "./Validators/TranslationValidator"
import DistanceValidator from "./Validators/DistanceValidator"
export type ValidatorType = (typeof Validators.availableTypes)[number]
export default class Validators {
public static readonly availableTypes = [
export const availableValidators = [
"color",
"currency",
"date",
@ -66,40 +63,52 @@ export default class Validators {
"url",
"velopark",
"wikidata",
] as const
] as const
export type ValidatorType = (typeof availableValidators)[number]
export default class Validators {
public static readonly availableTypes = availableValidators
public static readonly AllValidators: ReadonlyArray<Validator> = [
new StringValidator(),
new TextValidator(),
new DateValidator(),
new TimeValidator(),
new NatValidator(),
new IntValidator(),
new DistanceValidator(),
new DirectionValidator(),
new WikidataValidator(),
new PNatValidator(),
new FloatValidator(),
new PFloatValidator(),
new EmailValidator(),
new UrlValidator(),
new PhoneValidator(),
new OpeningHoursValidator(),
new TextValidator(),
new NatValidator(),
new PNatValidator(),
new IntValidator(),
new DateValidator(),
new TimeValidator(),
new ColorValidator(),
new ImageUrlValidator(),
new SimpleTagValidator(),
new TagValidator(),
new TagKeyValidator(),
new TranslationValidator(),
new IconValidator(),
new FediverseValidator(),
new IdValidator(),
new DirectionValidator(),
new SlopeValidator(),
new VeloparkValidator(),
new NameSuggestionIndexValidator(),
new UrlValidator(),
new EmailValidator(),
new PhoneValidator(),
new FediverseValidator(),
new ImageUrlValidator(),
new OpeningHoursValidator(),
new CollectionTimesValidator(),
new CurrencyValidator(),
new WikidataValidator(),
new TagKeyValidator(),
new IconValidator(),
new VeloparkValidator(),
new IdValidator(),
new RegexValidator(),
new CollectionTimesValidator()
new SimpleTagValidator(),
new TranslationValidator(),
new TagValidator(),
new NameSuggestionIndexValidator(),
new DistanceValidator(),
]
private static _byType = Validators._byTypeConstructor()

View file

@ -1,7 +1,12 @@
import StringValidator from "./StringValidator"
import { ComponentType } from "svelte/types/runtime/internal/dev"
import CollectionTimes from "../Helpers/CollectionTimes/CollectionTimes.svelte"
export default class CollectionTimesValidator extends StringValidator{
public readonly inputHelper: ComponentType = CollectionTimes
constructor() {
super("points_in_time", "'Points in time' are points according to a fixed schedule, e.g. 'every monday at 10:00'. They are typically used for postbox collection times or times of mass at a place of worship")
}
}

View file

@ -1,7 +1,11 @@
import { Validator } from "../Validator"
import ColorInput from "../Helpers/ColorInput.svelte"
export default class ColorValidator extends Validator {
inputHelper = ColorInput
constructor() {
super("color", "Shows a color picker")
}
}

View file

@ -1,6 +1,10 @@
import { Validator } from "../Validator"
import DateInput from "../Helpers/DateInput.svelte"
export default class DateValidator extends Validator {
public readonly inputHelper = DateInput
public readonly hideInputField = true
constructor() {
super("date", "A date with date picker")
}
@ -25,4 +29,5 @@ export default class DateValidator extends Validator {
return [year, month, day].join("-")
}
}

View file

@ -1,6 +1,9 @@
import IntValidator from "./IntValidator"
import DirectionInput from "../Helpers/DirectionInput.svelte"
export default class DirectionValidator extends IntValidator {
public readonly inputHelper = DirectionInput
constructor() {
super(
"direction",

View file

@ -3,6 +3,7 @@ import { Utils } from "../../../Utils"
import { eliCategory } from "../../../Models/RasterLayerProperties"
export default class DistanceValidator extends Validator {
private readonly docs: string = [
"#### Helper-arguments",
"Options are:",
@ -58,4 +59,5 @@ export default class DistanceValidator extends Validator {
}
return undefined
}
}

View file

@ -1,9 +1,11 @@
import UrlValidator from "./UrlValidator"
import { Translation } from "../../i18n/Translation"
import ImageHelper from "../Helpers/ImageHelper.svelte"
export default class ImageUrlValidator extends UrlValidator {
private static readonly allowedExtensions = ["jpg", "jpeg", "svg", "png"]
public readonly isMeta = true
public readonly inputHelper = ImageHelper
constructor() {
super(
@ -37,4 +39,6 @@ export default class ImageUrlValidator extends UrlValidator {
}
return ImageUrlValidator.hasValidExternsion(str)
}
}

View file

@ -1,7 +1,12 @@
import { Validator } from "../Validator"
import MarkdownUtils from "../../../Utils/MarkdownUtils"
import { ComponentType } from "svelte/types/runtime/internal/dev"
import OpeningHoursInput from "../Helpers/OpeningHoursInput.svelte"
export default class OpeningHoursValidator extends Validator {
public readonly inputHelper= OpeningHoursInput
constructor() {
super(
"opening_hours",
@ -39,4 +44,6 @@ export default class OpeningHoursValidator extends Validator {
].join("\n")
)
}
}

View file

@ -2,12 +2,15 @@ import { Validator } from "../Validator"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import TagKeyValidator from "./TagKeyValidator"
import SimpleTagInput from "../Helpers/SimpleTagInput.svelte"
/**
* Checks that the input conforms `key=value`, where `key` and `value` don't have too much weird characters
*/
export default class SimpleTagValidator extends Validator {
private static readonly KeyValidator = new TagKeyValidator()
public readonly inputHelper = SimpleTagInput
public readonly hideInputField = true
public readonly isMeta = true
constructor() {
@ -50,4 +53,6 @@ export default class SimpleTagValidator extends Validator {
isValid(tag: string, _): boolean {
return this.getFeedback(tag, _) === undefined
}
}

View file

@ -1,6 +1,9 @@
import FloatValidator from "./FloatValidator"
import SlopeInput from "../Helpers/SlopeInput.svelte"
export default class SlopeValidator extends FloatValidator {
public readonly inputHelper =SlopeInput
constructor() {
super(
"slope",
@ -40,4 +43,5 @@ export default class SlopeValidator extends FloatValidator {
}
return super.reformat(str) + lastChar
}
}

View file

@ -1,11 +1,14 @@
import { Validator } from "../Validator"
import { Translation } from "../../i18n/Translation"
import TagInput from "../Helpers/TagInput.svelte"
/**
* Checks that the input conforms a JSON-encoded tag expression or a simpleTag`key=value`,
*/
export default class TagValidator extends Validator {
public readonly isMeta = true
public readonly inputHelper = TagInput
public readonly hideInputField = true
constructor() {
super("tag", "A simple tag of the format `key=value` OR a tagExpression")
@ -18,4 +21,5 @@ export default class TagValidator extends Validator {
isValid(tag: string, _): boolean {
return this.getFeedback(tag, _) === undefined
}
}

View file

@ -1,11 +1,15 @@
import { Validator } from "../Validator"
import TimeInput from "../Helpers/TimeInput.svelte"
export class TimeValidator extends Validator {
inputmode = "time"
public readonly inputmode = "time"
public readonly inputHelper = TimeInput
public readonly hideInputField = true
constructor() {
super("time", "A time picker")
}
}

View file

@ -1,7 +1,10 @@
import { Validator } from "../Validator"
import TranslationInput from "../Helpers/TranslationInput.svelte"
export default class TranslationValidator extends Validator {
public readonly inputHelper = TranslationInput
public readonly isMeta = true
public readonly hideInputField = true
constructor() {
super("translation", "Makes sure the the string is of format `Record<string, string>` ")
}
@ -14,4 +17,5 @@ export default class TranslationValidator extends Validator {
return false
}
}
}

View file

@ -3,9 +3,11 @@ import { Validator } from "../Validator"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import MarkdownUtils from "../../../Utils/MarkdownUtils"
import WikidataInputHelper from "../Helpers/WikidataInputHelper.svelte"
export default class WikidataValidator extends Validator {
public static readonly _searchCache = new Map<string, Promise<WikidataResponse[]>>()
public readonly inputHelper = WikidataInputHelper
private static docs = [
"#### Helper arguments",
@ -182,4 +184,5 @@ Another example is to search for species and trees:
}
return clipped
}
}

View file

@ -8,8 +8,8 @@
import InputHelper from "../../InputElement/InputHelper.svelte"
import type { Feature } from "geojson"
import { Unit } from "../../../Models/Unit"
import InputHelpers from "../../InputElement/InputHelpers"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import Validators from "../../InputElement/Validators"
export let value: UIEventSource<string>
export let unvalidatedText: UIEventSource<string> = new UIEventSource<string>(value.data)
@ -29,6 +29,7 @@
}
const dispatch = createEventDispatcher<{ selected }>()
let hideInput = Validators.get(config.freeform.type).hideInputField
export let feedback: UIEventSource<Translation>
onDestroy(
value.addCallbackD(() => {
@ -44,6 +45,16 @@
<div class="inline-flex w-full flex-col">
{#if inline}
<Inline key={config.freeform.key} {tags} template={config.render}>
{#if hideInput}
<InputHelper
args={config.freeform.args}
{feature}
type={config.freeform.type}
{value}
{state}
on:submit
/>
{:else}
<ValidatedInput
{feedback}
{getCountry}
@ -55,8 +66,9 @@
{value}
range={config.freeform.range}
/>
{/if}
</Inline>
{:else if InputHelpers.hideInputField.indexOf(config.freeform.type) < 0}
{:else if !hideInput}
<ValidatedInput
{feedback}
{getCountry}
@ -71,6 +83,7 @@
/>
{/if}
{#if !(inline && hideInput)}
<InputHelper
args={config.freeform.args}
{feature}
@ -79,4 +92,5 @@
{state}
on:submit
/>
{/if}
</div>

View file

@ -1,4 +1,4 @@
<script>
export let color = "#000000"
</script>
<svg {...$$restProps} on:click on:mouseover on:mouseenter on:mouseleave on:keydown on:focus xmlns="http://www.w3.org/2000/svg" width="375px" height="375px" viewBox="0 0 375 375" version="1.1"> <g id="surface1"> <path style="fill: none !important;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(97.254902%,100%,96.078432%);stroke-opacity:1;stroke-miterlimit:4;" d="M -177.48351 -16.993714 C -177.484166 101.48875 -226.288922 214.739751 -312.411923 296.10684 C -398.528411 377.467771 -514.363074 419.770216 -632.651731 413.060164 " transform="matrix(-0.316636,-0.299423,0.299423,-0.316636,0,0)"/> <path style="fill: none !important;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:4;" d="M 743.478328 134.561833 L 430.253662 430.253662 L 117.002105 134.508051 " transform="matrix(0.435789,0,0,0.435789,0,0)"/> <path style="fill: none !important;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke:{color};stroke-opacity:1;stroke-miterlimit:4;" d="M -177.48351 -16.993714 C -177.484166 101.48875 -226.288922 214.739751 -312.411923 296.10684 C -398.528411 377.467771 -514.363074 419.770216 -632.651731 413.060164 " transform="matrix(-0.316636,-0.299423,0.299423,-0.316636,0,0)"/> <path style="fill: none !important;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke:{color};stroke-opacity:1;stroke-miterlimit:4;" d="M 743.478328 134.561833 L 430.253662 430.253662 L 117.002105 134.508051 " transform="matrix(0.435789,0,0,0.435789,0,0)"/> </g> </svg>
<svg {...$$restProps} on:click on:mouseover on:mouseenter on:mouseleave on:keydown on:focus width="375px" height="375px" viewBox="0 0 375 375" version="1.1" id="svg4" sodipodi:docname="direction_stroke.svg" inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <defs id="defs4" /> <sodipodi:namedview id="namedview4" pagecolor="#ffffff" bordercolor="#999999" borderopacity="1" inkscape:showpageshadow="2" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="0.87158493" inkscape:cx="-23.52037" inkscape:cy="19.504697" inkscape:window-width="1920" inkscape:window-height="1005" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg4" /> <g id="surface1" style="fill:{color};fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-opacity:1" transform="translate(0,4)"> <path style="color:{color};fill:{color};fill-opacity:1;stroke:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;-inkscape-stroke:none" d="m -177.48437,-20.435547 a 3.4420288,3.4420288 0 0 0 -3.44141,3.441406 c -6.5e-4,117.537141 -48.41377,229.881731 -133.84961,310.599611 -85.42934,80.71172 -200.33664,122.6741 -317.68164,116.01758 a 3.4420288,3.4420288 0 0 0 -3.63086,3.24218 3.4420288,3.4420288 0 0 0 3.24219,3.63086 c 119.23207,6.76357 235.9934,-35.87674 322.79687,-117.88672 86.80999,-82.01614 136.00715,-196.17596 136.00781,-315.603511 a 3.4420288,3.4420288 0 0 0 -3.44335,-3.441406 z" id="path1" transform="matrix(-0.316636,-0.299423,0.299423,-0.316636,0,0)" /> <path style="color:{color};fill:{color};fill-opacity:1;stroke:#ffffff;stroke-opacity:1;-inkscape-stroke:none" d="m 119.36523,132.00586 -4.72656,5.00586 315.61524,297.97461 315.58789,-297.92188 -4.72657,-5.00586 -310.86132,293.46094 z" id="path2" transform="scale(0.435789)" /> <path style="color:{color};fill:{color};fill-opacity:1;stroke:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;-inkscape-stroke:none" d="m -177.48437,-20.435547 a 3.4420288,3.4420288 0 0 0 -3.44141,3.441406 c -6.5e-4,117.537141 -48.41377,229.881731 -133.84961,310.599611 -85.42934,80.71172 -200.33664,122.6741 -317.68164,116.01758 a 3.4420288,3.4420288 0 0 0 -3.63086,3.24218 3.4420288,3.4420288 0 0 0 3.24219,3.63086 c 119.23207,6.76357 235.9934,-35.87674 322.79687,-117.88672 86.80999,-82.01614 136.00715,-196.17596 136.00781,-315.603511 a 3.4420288,3.4420288 0 0 0 -3.44335,-3.441406 z" id="path3" transform="matrix(-0.316636,-0.299423,0.299423,-0.316636,0,0)" /> <path style="color:{color};fill:{color};fill-opacity:1;stroke:#ffffff;stroke-opacity:1;-inkscape-stroke:none" d="m 119.36523,132.00586 -4.72656,5.00586 315.61524,297.97461 315.58789,-297.92188 -4.72657,-5.00586 -310.86132,293.46094 z" id="path4" transform="scale(0.435789)" /> </g> </svg>