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"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="375px" height="375px" viewBox="0 0 375 375" version="1.1"> <svg
<g id="surface1"> width="375px"
<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)"/> height="375px"
<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)"/> viewBox="0 0 375 375"
<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)"/> version="1.1"
<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)"/> 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> </g>
</svg> </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-width: 8rem;
} }
.min-w-48 {
min-width: 12rem;
}
.min-w-6 { .min-w-6 {
min-width: 1.5rem; min-width: 1.5rem;
} }

View file

@ -22,6 +22,7 @@ import { QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRender
import MarkdownUtils from "../../Utils/MarkdownUtils" import MarkdownUtils from "../../Utils/MarkdownUtils"
import { And } from "../../Logic/Tags/And" import { And } from "../../Logic/Tags/And"
import OsmWiki from "../../Logic/Osm/OsmWiki" import OsmWiki from "../../Logic/Osm/OsmWiki"
import { UnitUtils } from "../UnitUtils"
export default class LayerConfig extends WithContextLoader { export default class LayerConfig extends WithContextLoader {
public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const 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) => 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 let filter = json.filter

View file

@ -1,14 +1,9 @@
import BaseUIElement from "../UI/BaseUIElement" import BaseUIElement from "../UI/BaseUIElement"
import { Denomination } from "./Denomination" 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 { Validator } from "../UI/InputElement/Validator"
import FloatValidator from "../UI/InputElement/Validators/FloatValidator" import FloatValidator from "../UI/InputElement/Validators/FloatValidator"
export class Unit { export class Unit {
private static allUnits = this.initUnits()
public readonly appliesToKeys: Set<string> public readonly appliesToKeys: Set<string>
public readonly denominations: Denomination[] public readonly denominations: Denomination[]
public readonly denominationsSorted: 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 { isApplicableToKey(key: string | undefined): boolean {
if (key === undefined) { 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 MaplibreMap from "../../Map/MaplibreMap.svelte"
import Direction_stroke from "../../../assets/svg/Direction_stroke.svelte" import Direction_stroke from "../../../assets/svg/Direction_stroke.svelte"
import type { SpecialVisualizationState } from "../../SpecialVisualization" 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. * A visualisation to pick a direction on a map background.
*/ */
export let value: UIEventSource<undefined | string> export let value: UIEventSource<undefined | string>
export let state: SpecialVisualizationState = undefined export let state: SpecialVisualizationState = undefined
export let args: any[] = []
export let mapProperties: Partial<MapProperties> & { export let feature: Feature
readonly location: UIEventSource<{ lon: number; lat: number }> 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 map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let mla = new MapLibreAdaptor(map, mapProperties) let mla = new MapLibreAdaptor(map, mapProperties)
mla.allowMoving.setData(false) mla.allowMoving.setData(false)
mla.allowZooming.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 let directionElem: HTMLElement | undefined
$: value.addCallbackAndRunD((degrees) => { $: rotation.addCallbackAndRunD((degrees) => {
if (directionElem === undefined) { if (!directionElem?.style) {
return return
} }
directionElem.style.rotate = degrees + "deg" directionElem.style.rotate = degrees + "deg"
@ -32,13 +42,14 @@
let mainElem: HTMLElement let mainElem: HTMLElement
function onPosChange(x: number, y: number) { function onPosChange(x: number, y: number) {
const rect = mainElem.getBoundingClientRect() const rect = mainElem.getBoundingClientRect()
const dx = -(rect.left + rect.right) / 2 + x const dx = -(rect.left + rect.right) / 2 + x
const dy = (rect.top + rect.bottom) / 2 - y const dy = (rect.top + rect.bottom) / 2 - y
const angle = (180 * Math.atan2(dy, dx)) / Math.PI const angle = (180 * Math.atan2(dy, dx)) / Math.PI
const angleGeo = Math.floor((450 - angle) % 360) const angleGeo = Math.floor((450 - angle) % 360)
value.setData("" + angleGeo) rotation.setData(angleGeo)
} }
let isDown = false let isDown = false
@ -46,7 +57,7 @@
<div <div
bind:this={mainElem} 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:click={(e) => onPosChange(e.x, e.y)}
on:mousedown={(e) => { on:mousedown={(e) => {
isDown = true isDown = true
@ -71,7 +82,7 @@
<MaplibreMap mapProperties={mla} {map} /> <MaplibreMap mapProperties={mla} {map} />
</div> </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 /> <Direction_stroke />
</div> </div>
</div> </div>

View file

@ -1,67 +1,25 @@
<script lang="ts"> <script lang="ts">
/**
* Constructs an input helper element for the given type.
* Note that all values are stringified
*/
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import type { ValidatorType } from "./Validators" import type { ValidatorType } from "./Validators"
import InputHelpers from "./InputHelpers" import Validators from "./Validators"
import type { Feature } from "geojson" 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 type { SpecialVisualizationState } from "../SpecialVisualization"
import WikidataInputHelper from "./Helpers/WikidataInputHelper.svelte" import type { Validator } from "./Validator"
import DistanceInput from "./Helpers/DistanceInput.svelte" 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 type: ValidatorType
export let value: UIEventSource<string | object> export let value: UIEventSource<string | object>
export let feature: Feature = undefined export let feature: Feature = undefined
export let args: (string | number | boolean)[] | any = undefined export let args: (string | number | boolean)[] | any = undefined
export let state: SpecialVisualizationState = undefined export let state: SpecialVisualizationState = undefined
let validator = Validators.get(type)
let validatorHelper: Validator = validator.inputHelper
</script> </script>
{#if type === "translation"} {#if type === "distance"}
<TranslationInput {value} on:submit {args} /> <DistanceInput {value} {feature} {state} {args} />
{:else if type === "direction"} {:else if validatorHelper !== undefined}
<DirectionInput <svelte:component this={validatorHelper} {value} {feature} {state} {args} on:submit />
{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} {/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 { Translation } from "../i18n/Translation"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import { HTMLInputTypeAttribute } from "svelte/elements" 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. * 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 textArea: boolean
public readonly isMeta?: boolean public readonly isMeta?: boolean
public readonly inputHelper : ComponentType = undefined
public readonly hideInputField: boolean = false
constructor( constructor(
name: string, name: string,
@ -80,4 +83,5 @@ export abstract class Validator {
public validateArguments(args: string): undefined | string { public validateArguments(args: string): undefined | string {
return undefined return undefined
} }
} }

View file

@ -1,105 +1,114 @@
import { Validator } from "./Validator" import { Validator } from "./Validator"
import FloatValidator from "./Validators/FloatValidator"
import StringValidator from "./Validators/StringValidator" import StringValidator from "./Validators/StringValidator"
import PFloatValidator from "./Validators/PFloatValidator"
import TextValidator from "./Validators/TextValidator" import TextValidator from "./Validators/TextValidator"
import DateValidator from "./Validators/DateValidator" import DateValidator from "./Validators/DateValidator"
import { TimeValidator } from "./Validators/TimeValidator"
import NatValidator from "./Validators/NatValidator" import NatValidator from "./Validators/NatValidator"
import IntValidator from "./Validators/IntValidator" import IntValidator from "./Validators/IntValidator"
import DistanceValidator from "./Validators/DistanceValidator" import PNatValidator from "./Validators/PNatValidator"
import DirectionValidator from "./Validators/DirectionValidator" import DirectionValidator from "./Validators/DirectionValidator"
import WikidataValidator from "./Validators/WikidataValidator" 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 EmailValidator from "./Validators/EmailValidator"
import UrlValidator from "./Validators/UrlValidator" import UrlValidator from "./Validators/UrlValidator"
import PhoneValidator from "./Validators/PhoneValidator"
import OpeningHoursValidator from "./Validators/OpeningHoursValidator" import OpeningHoursValidator from "./Validators/OpeningHoursValidator"
import PhoneValidator from "./Validators/PhoneValidator"
import ColorValidator from "./Validators/ColorValidator" import ColorValidator from "./Validators/ColorValidator"
import SimpleTagValidator from "./Validators/SimpleTagValidator"
import ImageUrlValidator from "./Validators/ImageUrlValidator" import ImageUrlValidator from "./Validators/ImageUrlValidator"
import TagKeyValidator from "./Validators/TagKeyValidator" import TagKeyValidator from "./Validators/TagKeyValidator"
import TranslationValidator from "./Validators/TranslationValidator"
import FediverseValidator from "./Validators/FediverseValidator"
import IconValidator from "./Validators/IconValidator" import IconValidator from "./Validators/IconValidator"
import TagValidator from "./Validators/TagValidator"
import IdValidator from "./Validators/IdValidator"
import SlopeValidator from "./Validators/SlopeValidator" 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 VeloparkValidator from "./Validators/VeloparkValidator"
import NameSuggestionIndexValidator from "./Validators/NameSuggestionIndexValidator" import NameSuggestionIndexValidator from "./Validators/NameSuggestionIndexValidator"
import CurrencyValidator from "./Validators/CurrencyValidator"
import RegexValidator from "./Validators/RegexValidator" import RegexValidator from "./Validators/RegexValidator"
import { TimeValidator } from "./Validators/TimeValidator" import CurrencyValidator from "./Validators/CurrencyValidator"
import CollectionTimesValidator from "./Validators/CollectionTimesValidator" import TagValidator from "./Validators/TagValidator"
import TranslationValidator from "./Validators/TranslationValidator"
import DistanceValidator from "./Validators/DistanceValidator"
export type ValidatorType = (typeof Validators.availableTypes)[number] export const availableValidators = [
"color",
"currency",
"date",
"time",
"direction",
"distance",
"email",
"fediverse",
"float",
"icon",
"id",
"image",
"int",
"key",
"nat",
"nsi",
"opening_hours",
"pfloat",
"phone",
"pnat",
"points_in_time",
"regex",
"simple_tag",
"slope",
"string",
"tag",
"text",
"translation",
"url",
"velopark",
"wikidata",
] as const
export type ValidatorType = (typeof availableValidators)[number]
export default class Validators { export default class Validators {
public static readonly availableTypes = [ public static readonly availableTypes = availableValidators
"color",
"currency",
"date",
"time",
"direction",
"distance",
"email",
"fediverse",
"float",
"icon",
"id",
"image",
"int",
"key",
"nat",
"nsi",
"opening_hours",
"pfloat",
"phone",
"pnat",
"points_in_time",
"regex",
"simple_tag",
"slope",
"string",
"tag",
"text",
"translation",
"url",
"velopark",
"wikidata",
] as const
public static readonly AllValidators: ReadonlyArray<Validator> = [ public static readonly AllValidators: ReadonlyArray<Validator> = [
new StringValidator(), new StringValidator(),
new TextValidator(),
new DateValidator(),
new TimeValidator(),
new NatValidator(),
new IntValidator(),
new DistanceValidator(),
new DirectionValidator(),
new WikidataValidator(),
new PNatValidator(),
new FloatValidator(), new FloatValidator(),
new PFloatValidator(), new PFloatValidator(),
new EmailValidator(), new TextValidator(),
new UrlValidator(), new NatValidator(),
new PhoneValidator(), new PNatValidator(),
new OpeningHoursValidator(), new IntValidator(),
new DateValidator(),
new TimeValidator(),
new ColorValidator(), new ColorValidator(),
new ImageUrlValidator(),
new SimpleTagValidator(), new DirectionValidator(),
new TagValidator(),
new TagKeyValidator(),
new TranslationValidator(),
new IconValidator(),
new FediverseValidator(),
new IdValidator(),
new SlopeValidator(), new SlopeValidator(),
new VeloparkValidator(),
new NameSuggestionIndexValidator(),
new UrlValidator(),
new EmailValidator(),
new PhoneValidator(),
new FediverseValidator(),
new ImageUrlValidator(),
new OpeningHoursValidator(),
new CollectionTimesValidator(),
new CurrencyValidator(), new CurrencyValidator(),
new WikidataValidator(),
new TagKeyValidator(),
new IconValidator(),
new VeloparkValidator(),
new IdValidator(),
new RegexValidator(), new RegexValidator(),
new CollectionTimesValidator() new SimpleTagValidator(),
new TranslationValidator(),
new TagValidator(),
new NameSuggestionIndexValidator(),
new DistanceValidator(),
] ]
private static _byType = Validators._byTypeConstructor() private static _byType = Validators._byTypeConstructor()

View file

@ -1,7 +1,12 @@
import StringValidator from "./StringValidator" 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{ export default class CollectionTimesValidator extends StringValidator{
public readonly inputHelper: ComponentType = CollectionTimes
constructor() { 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") 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 { Validator } from "../Validator"
import ColorInput from "../Helpers/ColorInput.svelte"
export default class ColorValidator extends Validator { export default class ColorValidator extends Validator {
inputHelper = ColorInput
constructor() { constructor() {
super("color", "Shows a color picker") super("color", "Shows a color picker")
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,12 @@
import { Validator } from "../Validator" import { Validator } from "../Validator"
import MarkdownUtils from "../../../Utils/MarkdownUtils" 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 { export default class OpeningHoursValidator extends Validator {
public readonly inputHelper= OpeningHoursInput
constructor() { constructor() {
super( super(
"opening_hours", "opening_hours",
@ -39,4 +44,6 @@ export default class OpeningHoursValidator extends Validator {
].join("\n") ].join("\n")
) )
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
<script> <script>
export let color = "#000000" export let color = "#000000"
</script> </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>