Refactoring: allow to reuse units, move all units into central file

This commit is contained in:
Pieter Vander Vennet 2023-12-12 03:46:51 +01:00
parent 067fb549c1
commit 94e07d5b13
30 changed files with 1495 additions and 1307 deletions

View file

@ -137,11 +137,12 @@ export default class DetermineLayout {
if (json.layers === undefined && json.tagRenderings !== undefined) {
// We got fed a layer instead of a theme
const layerConfig = <LayerConfigJson>json
const iconTr: string | TagRenderingConfigJson = <any>(
layerConfig.pointRendering
.map((mr) => mr?.marker?.find((icon) => icon.icon !== undefined)?.icon)
.find((i) => i !== undefined)
) ?? "bug"
const iconTr: string | TagRenderingConfigJson =
<any>(
layerConfig.pointRendering
.map((mr) => mr?.marker?.find((icon) => icon.icon !== undefined)?.icon)
.find((i) => i !== undefined)
) ?? "bug"
const icon = new TagRenderingConfig(iconTr).render.txt
json = {
id: json.id,
@ -156,8 +157,8 @@ export default class DetermineLayout {
}
const knownLayersDict = new Map<string, LayerConfigJson>()
for (const key in known_layers.layers) {
const layer = known_layers.layers[key]
for (const key in known_layers["layers"]) {
const layer = known_layers["layers"][key]
knownLayersDict.set(layer.id, <LayerConfigJson>layer)
}
const convertState: DesugaringContext = {

View file

@ -1,4 +1,4 @@
import { Translation } from "../UI/i18n/Translation"
import { Translation, TypedTranslation } from "../UI/i18n/Translation"
import { DenominationConfigJson } from "./ThemeConfig/Json/UnitConfigJson"
import Translations from "../UI/i18n/Translations"
@ -9,20 +9,39 @@ import Translations from "../UI/i18n/Translations"
export class Denomination {
public readonly canonical: string
public readonly _canonicalSingular: string
public readonly useAsDefaultInput: boolean | string[]
public readonly useIfNoUnitGiven: boolean | string[]
public readonly prefix: boolean
public readonly addSpace: boolean
public readonly alternativeDenominations: string[]
private readonly _human: Translation
private readonly _humanSingular?: Translation
public readonly human: TypedTranslation<{ quantity: string }>
public readonly humanSingular?: Translation
constructor(json: DenominationConfigJson, useAsDefaultInput: boolean, context: string) {
private constructor(
canonical: string,
_canonicalSingular: string,
useIfNoUnitGiven: boolean | string[],
prefix: boolean,
addSpace: boolean,
alternativeDenominations: string[],
_human: TypedTranslation<{ quantity: string }>,
_humanSingular?: Translation
) {
this.canonical = canonical
this._canonicalSingular = _canonicalSingular
this.useIfNoUnitGiven = useIfNoUnitGiven
this.prefix = prefix
this.addSpace = addSpace
this.alternativeDenominations = alternativeDenominations
this.human = _human
this.humanSingular = _humanSingular
}
public static fromJson(json: DenominationConfigJson, context: string) {
context = `${context}.unit(${json.canonicalDenomination})`
this.canonical = json.canonicalDenomination.trim()
if (this.canonical === undefined) {
const canonical = json.canonicalDenomination.trim()
if (canonical === undefined) {
throw `${context}: this unit has no decent canonical value defined`
}
this._canonicalSingular = json.canonicalDenominationSingular?.trim()
json.alternativeDenomination?.forEach((v, i) => {
if ((v?.trim() ?? "") === "") {
@ -30,40 +49,67 @@ export class Denomination {
}
})
this.alternativeDenominations = json.alternativeDenomination?.map((v) => v.trim()) ?? []
if (json["default" /* @code-quality: ignore*/] !== undefined) {
throw `${context} uses the old 'default'-key. Use "useIfNoUnitGiven" or "useAsDefaultInput" instead`
}
this.useIfNoUnitGiven = json.useIfNoUnitGiven
this.useAsDefaultInput = useAsDefaultInput ?? json.useIfNoUnitGiven
this._human = Translations.T(json.human, context + "human")
this._humanSingular = Translations.T(json.humanSingular, context + "humanSingular")
this.prefix = json.prefix ?? false
const humanTexts = Translations.T(json.human, context + "human")
humanTexts.OnEveryLanguage((text, language) => {
if (text.indexOf("{quantity}") < 0) {
throw `In denomination: a human text should contain {quantity} (at ${context}.human.${language})`
}
return text
})
return new Denomination(
canonical,
json.canonicalDenominationSingular?.trim(),
json.useIfNoUnitGiven,
json.prefix ?? false,
json.addSpace ?? false,
json.alternativeDenomination?.map((v) => v.trim()) ?? [],
humanTexts,
Translations.T(json.humanSingular, context + "humanSingular")
)
}
get human(): Translation {
return this._human.Clone()
public clone() {
return new Denomination(
this.canonical,
this._canonicalSingular,
this.useIfNoUnitGiven,
this.prefix,
this.addSpace,
this.alternativeDenominations,
this.human,
this.humanSingular
)
}
get humanSingular(): Translation {
return (this._humanSingular ?? this._human).Clone()
public withBlankCanonical() {
return new Denomination(
"",
this._canonicalSingular,
this.useIfNoUnitGiven,
this.prefix,
this.addSpace,
[this.canonical, ...this.alternativeDenominations],
this.human,
this.humanSingular
)
}
/**
* Create a representation of the given value
* Create the canonical, human representation of the given value
* @param value the value from OSM
* @param actAsDefault if set and the value can be parsed as number, will be parsed and trimmed
*
* const unit = new Denomination({
* const unit = Denomination.fromJson({
* canonicalDenomination: "m",
* alternativeDenomination: ["meter"],
* human: {
* en: "meter"
* en: "{quantity} meter"
* }
* }, false, "test")
* }, "test")
* unit.canonicalValue("42m", true) // =>"42 m"
* unit.canonicalValue("42", true) // =>"42 m"
* unit.canonicalValue("42 m", true) // =>"42 m"
@ -72,13 +118,13 @@ export class Denomination {
* unit.canonicalValue("42", true) // =>"42 m"
*
* // Should be trimmed if canonical is empty
* const unit = new Denomination({
* const unit = Denomination.fromJson({
* canonicalDenomination: "",
* alternativeDenomination: ["meter","m"],
* human: {
* en: "meter"
* en: "{quantity} meter"
* }
* }, false, "test")
* }, "test")
* unit.canonicalValue("42m", true) // =>"42"
* unit.canonicalValue("42", true) // =>"42"
* unit.canonicalValue("42 m", true) // =>"42"
@ -160,14 +206,4 @@ export class Denomination {
return null
}
isDefaultDenomination(country: () => string) {
if (this.useIfNoUnitGiven === true) {
return true
}
if (this.useIfNoUnitGiven === false) {
return false
}
return this.useIfNoUnitGiven.indexOf(country()) >= 0
}
}

View file

@ -517,7 +517,10 @@ export interface LayerConfigJson {
*
* group: editing
*/
units?: UnitConfigJson[]
units?: (
| UnitConfigJson
| Record<string, string | { quantity: string; denominations: string[]; canonical?: string }>
)[]
/**
* If set, synchronizes whether or not this layer is enabled.

View file

@ -57,12 +57,16 @@
*
*/
export default interface UnitConfigJson {
/**
* What is quantified? E.g. 'speed', 'length' (including width, diameter, ...), 'electric tension', 'electric current', 'duration'
*/
quantity?: string
/**
* Every key from this list will be normalized.
*
* To render the value properly (with a human readable denomination), use `{canonical(<key>)}`
*/
appliesToKey: string[]
appliesToKey?: string[]
/**
* If set, invalid values will be erased in the MC application (but not in OSM of course!)
* Be careful with setting this
@ -143,4 +147,11 @@ export interface DenominationConfigJson {
* Note that if all values use 'prefix', the dropdown might move to before the text field
*/
prefix?: boolean
/**
* If set, add a space between the quantity and the denomination.
*
* E.g.: `50 mph` instad of `50mph`
*/
addSpace?: boolean
}

View file

@ -105,8 +105,10 @@ export default class LayerConfig extends WithContextLoader {
".units: the 'units'-section should be a list; you probably have an object there"
)
}
this.units = (json.units ?? []).map((unitJson, i) =>
Unit.fromJson(unitJson, `${context}.unit[${i}]`)
this.units = [].concat(
...(json.units ?? []).map((unitJson, i) =>
Unit.fromJson(unitJson, `${context}.unit[${i}]`)
)
)
if (json.description !== undefined) {

View file

@ -3,18 +3,23 @@ import { FixedUiElement } from "../UI/Base/FixedUiElement"
import Combine from "../UI/Base/Combine"
import { Denomination } from "./Denomination"
import UnitConfigJson from "./ThemeConfig/Json/UnitConfigJson"
import unit from "../../assets/layers/unit/unit.json"
export class Unit {
private static allUnits = this.initUnits()
public readonly appliesToKeys: Set<string>
public readonly denominations: Denomination[]
public readonly denominationsSorted: Denomination[]
public readonly eraseInvalid: boolean
public readonly quantity: string
constructor(
quantity: string,
appliesToKeys: string[],
applicableDenominations: Denomination[],
eraseInvalid: boolean
) {
this.quantity = quantity
this.appliesToKeys = new Set(appliesToKeys)
this.denominations = applicableDenominations
this.eraseInvalid = eraseInvalid
@ -60,12 +65,24 @@ export class Unit {
}
}
static fromJson(
json:
| UnitConfigJson
| Record<string, string | { quantity: string; denominations: string[] }>,
ctx: string
): Unit[] {
if (!json.appliesToKey && !json.quantity) {
return this.loadFromLibrary(<any>json, ctx)
}
return [this.parse(<UnitConfigJson>json, ctx)]
}
/**
*
* // Should detect invalid defaultInput
* let threwError = false
* try{
* Unit.fromJson({
* Unit.parse({
* appliesToKey: ["length"],
* defaultInput: "xcm",
* applicableUnits: [
@ -82,7 +99,7 @@ export class Unit {
* threwError // => true
*
* // Should work
* Unit.fromJson({
* Unit.parse({
* appliesToKey: ["length"],
* defaultInput: "xcm",
* applicableUnits: [
@ -98,9 +115,9 @@ export class Unit {
* ]
* }, "test")
*/
static fromJson(json: UnitConfigJson, ctx: string) {
private static parse(json: UnitConfigJson, ctx: string): Unit {
const appliesTo = json.appliesToKey
for (let i = 0; i < appliesTo.length; i++) {
for (let i = 0; i < (appliesTo ?? []).length; i++) {
let key = appliesTo[i]
if (key.trim() !== key) {
throw `${ctx}.appliesToKey[${i}] is invalid: it starts or ends with whitespace`
@ -112,15 +129,8 @@ export class Unit {
}
// Some keys do have unit handling
const applicable = json.applicableUnits.map(
(u, i) =>
new Denomination(
u,
u.canonicalDenomination === undefined
? undefined
: u.canonicalDenomination.trim() === json.defaultInput,
`${ctx}.units[${i}]`
)
const applicable = json.applicableUnits.map((u, i) =>
Denomination.fromJson(u, `${ctx}.units[${i}]`)
)
if (
@ -133,7 +143,85 @@ export class Unit {
.map((denom) => denom.canonical)
.join(", ")}`
}
return new Unit(appliesTo, applicable, json.eraseInvalidValues ?? false)
return new Unit(
json.quantity ?? "",
appliesTo,
applicable,
json.eraseInvalidValues ?? false
)
}
private static initUnits(): Map<string, Unit> {
const m = new Map<string, Unit>()
const units = (<UnitConfigJson[]>unit.units).map((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 }
>,
ctx: string
): Unit[] {
const units: Unit[] = []
for (const key in spec) {
const toLoad = spec[key]
if (typeof toLoad === "string") {
const loaded = this.getFromLibrary(toLoad, ctx)
units.push(
new Unit(loaded.quantity, [key], loaded.denominations, loaded.eraseInvalid)
)
continue
}
const loaded = this.getFromLibrary(toLoad.quantity, ctx)
const quantity = toLoad.quantity
function 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
}
const denoms = toLoad.denominations
.map((d) => d.toLowerCase())
.map((d) => fetchDenom(d))
if (toLoad.canonical) {
const canonical = fetchDenom(toLoad.canonical)
denoms.unshift(canonical.withBlankCanonical())
}
units.push(new Unit(loaded.quantity, [key], denoms, loaded.eraseInvalid))
}
return units
}
isApplicableToKey(key: string | undefined): boolean {
@ -161,47 +249,34 @@ export class Unit {
return [undefined, undefined]
}
asHumanLongValue(value: string, country: () => string): BaseUIElement {
asHumanLongValue(value: string, country: () => string): BaseUIElement | string {
if (value === undefined) {
return undefined
}
const [stripped, denom] = this.findDenomination(value, country)
const human = stripped === "1" ? denom?.humanSingular : denom?.human
if (stripped === "1") {
return denom?.humanSingular ?? stripped
}
const human = denom?.human
if (human === undefined) {
return new FixedUiElement(stripped ?? value)
return stripped ?? value
}
const elems = denom.prefix ? [human, stripped] : [stripped, human]
return new Combine(elems)
return human.Subs({ quantity: value })
}
public getDefaultInput(country: () => string | string[]) {
console.log("Searching the default denomination for input", country)
for (const denomination of this.denominations) {
if (denomination.useAsDefaultInput === true) {
return denomination
}
if (
denomination.useAsDefaultInput === undefined ||
denomination.useAsDefaultInput === false
) {
continue
}
let countries: string | string[] = country()
if (typeof countries === "string") {
countries = countries.split(",")
}
const denominationCountries: string[] = denomination.useAsDefaultInput
if (countries.some((country) => denominationCountries.indexOf(country) >= 0)) {
return denomination
}
public toOsm(value: string, denomination: string) {
const denom = this.denominations.find((d) => d.canonical === denomination)
const space = denom.addSpace ? " " : ""
if (denom.prefix) {
return denom.canonical + space + value
}
return this.denominations[0]
return value + space + denom.canonical
}
public getDefaultDenomination(country: () => string) {
for (const denomination of this.denominations) {
if (denomination.useIfNoUnitGiven === true || denomination.canonical === "") {
if (denomination.useIfNoUnitGiven === true) {
return denomination
}
if (
@ -219,6 +294,11 @@ export class Unit {
return denomination
}
}
for (const denomination of this.denominations) {
if (denomination.canonical === "") {
return denomination
}
}
return this.denominations[0]
}
}

View file

@ -1,95 +1,102 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import type { ValidatorType } from "./Validators"
import Validators from "./Validators"
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Translation } from "../i18n/Translation"
import { createEventDispatcher, onDestroy } from "svelte"
import { Validator } from "./Validator"
import { Unit } from "../../Models/Unit"
import UnitInput from "../Popup/UnitInput.svelte"
import { Utils } from "../../Utils"
import { twMerge } from "tailwind-merge"
import { UIEventSource } from "../../Logic/UIEventSource";
import type { ValidatorType } from "./Validators";
import Validators from "./Validators";
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid";
import { Translation } from "../i18n/Translation";
import { createEventDispatcher, onDestroy } from "svelte";
import { Validator } from "./Validator";
import { Unit } from "../../Models/Unit";
import UnitInput from "../Popup/UnitInput.svelte";
import { Utils } from "../../Utils";
import { twMerge } from "tailwind-merge";
export let type: ValidatorType
export let feedback: UIEventSource<Translation> | undefined = undefined
export let cls: string = undefined
export let getCountry: () => string | undefined
export let placeholder: string | Translation | undefined
export let unit: Unit = undefined
export let value: UIEventSource<string>
export let type: ValidatorType;
export let feedback: UIEventSource<Translation> | undefined = undefined;
export let cls: string = undefined;
export let getCountry: () => string | undefined;
export let placeholder: string | Translation | undefined;
export let unit: Unit = undefined;
/**
* Valid state, exported to the calling component
*/
export let value: UIEventSource<string | undefined>;
/**
* Internal state bound to the input element.
*
* This is only copied to 'value' when appropriate so that no invalid values leak outside;
* Additionally, the unit is added when copying
*/
let _value = new UIEventSource(value.data ?? "")
let _value = new UIEventSource(value.data ?? "");
let validator: Validator = Validators.get(type ?? "string")
let validator: Validator = Validators.get(type ?? "string");
if (validator === undefined) {
console.warn("Didn't find a validator for type", type)
console.warn("Didn't find a validator for type", type);
}
let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined)
let _placeholder = placeholder ?? validator?.getPlaceholder() ?? type
let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined);
let _placeholder = placeholder ?? validator?.getPlaceholder() ?? type;
function initValueAndDenom() {
if (unit && value.data) {
const [v, denom] = unit?.findDenomination(value.data, getCountry)
const [v, denom] = unit?.findDenomination(value.data, getCountry);
if (denom) {
_value.setData(v)
selectedUnit.setData(denom.canonical)
_value.setData(v);
selectedUnit.setData(denom.canonical);
} else {
_value.setData(value.data ?? "")
_value.setData(value.data ?? "");
}
} else {
_value.setData(value.data ?? "")
_value.setData(value.data ?? "");
}
}
initValueAndDenom()
initValueAndDenom();
$: {
// The type changed -> reset some values
validator = Validators.get(type ?? "string")
validator = Validators.get(type ?? "string");
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
feedback?.setData(validator?.getFeedback(_value.data, getCountry))
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type;
feedback?.setData(validator?.getFeedback(_value.data, getCountry));
initValueAndDenom()
initValueAndDenom();
}
function setValues() {
// Update the value stores
const v = _value.data
const v = _value.data;
if (!validator?.isValid(v, getCountry) || v === "") {
feedback?.setData(validator?.getFeedback(v, getCountry))
value.setData("")
return
feedback?.setData(validator?.getFeedback(v, getCountry));
value.setData("");
return;
}
if (unit !== undefined && isNaN(Number(v))) {
value.setData(undefined)
return
value.setData(undefined);
return;
}
feedback?.setData(undefined)
feedback?.setData(undefined);
if (selectedUnit.data) {
value.setData(v + selectedUnit.data)
value.setData(unit.toOsm(v, selectedUnit.data))
} else {
value.setData(v)
value.setData(v);
}
}
onDestroy(_value.addCallbackAndRun((_) => setValues()))
onDestroy(
value.addCallbackAndRunD((fromUpstream) => {
if (_value.data !== fromUpstream && fromUpstream !== "") {
_value.setData(fromUpstream)
}
})
)
onDestroy(selectedUnit.addCallback((_) => setValues()))
onDestroy(_value.addCallbackAndRun((_) => setValues()));
if (unit === undefined) {
onDestroy(
value.addCallbackAndRunD((fromUpstream) => {
if (_value.data !== fromUpstream && fromUpstream !== "") {
_value.setData(fromUpstream);
}
})
);
}else{
// Handled by the UnitInput
}
onDestroy(selectedUnit.addCallback((_) => setValues()));
if (validator === undefined) {
throw (
"Not a valid type (no validator found) for type '" +
@ -102,17 +109,17 @@
)
.slice(0, 5)
.join(", ")
)
);
}
const isValid = _value.map((v) => validator?.isValid(v, getCountry) ?? true)
const isValid = _value.map((v) => validator?.isValid(v, getCountry) ?? true);
let htmlElem: HTMLInputElement
let htmlElem: HTMLInputElement;
let dispatch = createEventDispatcher<{ selected; submit }>()
let dispatch = createEventDispatcher<{ selected; submit }>();
$: {
if (htmlElem !== undefined) {
htmlElem.onfocus = () => dispatch("selected")
htmlElem.onfocus = () => dispatch("selected");
}
}
@ -121,9 +128,9 @@
*/
function sendSubmit() {
if (feedback?.data) {
console.log("Not sending a submit as there is feedback")
console.log("Not sending a submit as there is feedback");
}
dispatch("submit")
dispatch("submit");
}
</script>
@ -150,7 +157,7 @@
{/if}
{#if unit !== undefined}
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value} />
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value} {getCountry} />
{/if}
</form>
{/if}

View file

@ -1,56 +1,67 @@
<script lang="ts">
import { Unit } from "../../Models/Unit"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Tr from "../Base/Tr.svelte"
import { onDestroy } from "svelte"
import { Unit } from "../../Models/Unit";
import { Store, UIEventSource } from "../../Logic/UIEventSource";
import Tr from "../Base/Tr.svelte";
import { onDestroy, onMount } from "svelte";
import { Denomination } from "../../Models/Denomination";
export let unit: Unit
export let unit: Unit;
/**
* The current value of the input field
* Not necessarily a correct number
* Not necessarily a correct number, should not contain the denomination
*/
export let textValue: UIEventSource<string>
export let textValue: UIEventSource<string>;
/**
* The actual _valid_ value that is upstreamed
* The actual _valid_ value that is upstreamed, including the denomination
*/
export let upstreamValue: Store<string>
export let upstreamValue: Store<string>;
let isSingle: Store<boolean> = textValue.map((v) => Number(v) === 1)
let isSingle: Store<boolean> = textValue.map((v) => Number(v) === 1);
export let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined)
export let getCountry = () => "be"
console.log("Unit", unit)
export let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined);
export let getCountry = () => "?";
onMount(() => {
console.log("Setting selected unit based on country", getCountry(), upstreamValue.data)
if(upstreamValue.data === undefined || upstreamValue.data === ""){
// Init the selected unit
let denomination: Denomination = unit.getDefaultDenomination(getCountry);
console.log("Found denom", denomination.canonical)
selectedUnit.setData(denomination.canonical)
}
})
onDestroy(
upstreamValue.addCallbackAndRun((v) => {
if (v === undefined) {
if (!selectedUnit.data) {
selectedUnit.setData(unit.getDefaultDenomination(getCountry).canonical)
}
if(v === undefined || v === ""){
return
}
const selected = unit.findDenomination(v, getCountry)
if (selected === undefined) {
selectedUnit.setData(unit.getDefaultDenomination(getCountry).canonical)
return
let denomination: Denomination = unit.getDefaultDenomination(getCountry);
const selected = unit.findDenomination(v, getCountry);
if(selected){
denomination = selected[1];
}
const [value, denomination] = selected
selectedUnit.setData(denomination.canonical)
return
selectedUnit.setData(denomination.canonical);
})
)
);
onDestroy(
textValue.addCallbackAndRunD((v) => {
// Fallback in case that the user manually types a denomination
const [value, denomination] = unit.findDenomination(v, getCountry)
const [value, denomination] = unit.findDenomination(v, getCountry);
if (value === undefined || denomination === undefined) {
return
return;
}
textValue.setData(value)
selectedUnit.setData(denomination.canonical)
if(value === v){
// The input value actually didn't have a denomination typed out - so lets ignore this one
// If a denomination is given, it is the default value anyway
return;
}
textValue.setData(value);
selectedUnit.setData(denomination.canonical);
})
)
);
</script>
<select bind:value={$selectedUnit}>
@ -59,7 +70,7 @@
{#if $isSingle}
<Tr t={denom.humanSingular} />
{:else}
<Tr t={denom.human} />
<Tr t={denom.human.Subs({quantity: ""})} />
{/if}
</option>
{/each}

View file

@ -83,6 +83,7 @@ import NearbyImages from "./Image/NearbyImages.svelte"
import NearbyImagesCollapsed from "./Image/NearbyImagesCollapsed.svelte"
import { svelte } from "@sveltejs/vite-plugin-svelte"
import MoveWizard from "./Popup/MoveWizard.svelte"
import { Unit } from "../Models/Unit"
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -890,7 +891,7 @@ export default class SpecialVisualizations {
if (value === undefined) {
return undefined
}
const allUnits = [].concat(
const allUnits: Unit[] = [].concat(
...(state?.layout?.layers?.map((lyr) => lyr.units) ?? [])
)
const unit = allUnits.filter((unit) =>
@ -899,7 +900,9 @@ export default class SpecialVisualizations {
if (unit === undefined) {
return value
}
return unit.asHumanLongValue(value)
const getCountry = () => tagSource.data._country
const [v, denom] = unit.findDenomination(value, getCountry)
return unit.asHumanLongValue(v, getCountry)
})
)
},

View file

@ -131,7 +131,7 @@ export default class Translations {
}
static isProbablyATranslation(transl: any) {
if (typeof transl !== "object") {
if (!transl || typeof transl !== "object") {
return false
}
if (Object.keys(transl).length == 0) {

View file

@ -1081,7 +1081,19 @@
"items": {
"anyOf": [
{
"$ref": "#/definitions/TagRenderingConfigJson"
"allOf": [
{
"$ref": "#/definitions/TagRenderingConfigJson"
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}
]
},
{
"type": "string"
@ -1103,7 +1115,16 @@
"maxItems": 1
}
],
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)"
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)\nUse `auto:<tagrenderingId>` to automatically create an icon based on a tagRendering which has icons"
},
{
"path": [
"titleIcons"
],
"required": false,
"hints": {},
"type": "object",
"description": "A TagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet.\nFor an _editable_ tagRendering, use 'QuestionableTagRenderingConfigJson' instead, which extends this one"
},
{
"path": [
@ -10380,6 +10401,10 @@
"if": "value=tree_node",
"then": "tree_node - A layer showing trees"
},
{
"if": "value=unit",
"then": "unit - Library layer with all common units"
},
{
"if": "value=usersettings",
"then": "usersettings - A special layer which is not meant to be shown on a map, but which is used to set user settings"
@ -10447,7 +10472,7 @@
"hints": {
"typehint": "tagrendering[]",
"group": "tagrenderings",
"question": "Edit this attribute showing piece/question"
"question": "Edit this way this attributed is displayed or queried"
},
"type": [
{
@ -15969,21 +15994,20 @@
},
{
"path": [
"units"
"units",
"quantity"
],
"required": false,
"hints": {
"default": "ult: true,"
},
"type": "object",
"description": "In some cases, a value is represented in a certain unit (such as meters for heigt/distance/..., km/h for speed, ...)\nSometimes, multiple denominations are possible (e.g. km/h vs mile/h; megawatt vs kilowatt vs gigawatt for power generators, ...)\nThis brings in some troubles, as there are multiple ways to write it (no denomitation, 'm' vs 'meter' 'metre', ...)\nNot only do we want to write consistent data to OSM, we also want to present this consistently to the user.\nThis is handled by defining units.\n# Rendering\nTo render a value with long (human) denomination, use {canonical(key)}\n# Usage\nFirst of all, you define which keys have units applied, for example:\n```\nunits: [\n appliesTo: [\"maxspeed\", \"maxspeed:hgv\", \"maxspeed:bus\"]\n applicableUnits: [\n ...\n ]\n]\n```\nApplicableUnits defines which is the canonical extension, how it is presented to the user, ...:\n```\napplicableUnits: [\n{\n canonicalDenomination: \"km/h\",\n alternativeDenomination: [\"km/u\", \"kmh\", \"kph\"]\n human: {\n en: \"kilometer/hour\",\n nl: \"kilometer/uur\"\n },\n humanShort: {\n en: \"km/h\",\n nl: \"km/u\"\n }\n},\n{\n canoncialDenomination: \"mph\",\n ... similar for miles an hour ...\n}\n]\n```\nIf this is defined, then every key which the denominations apply to (`maxspeed`, `maxspeed:hgv` and `maxspeed:bus`) will be rewritten at the metatagging stage:\nevery value will be parsed and the canonical extension will be added add presented to the other parts of the code.\nAlso, if a freeform text field is used, an extra dropdown with applicable denominations will be given"
"hints": {},
"type": "string",
"description": "What is quantified? E.g. 'speed', 'length' (including width, diameter, ...), 'electric tension', 'electric current', 'duration'"
},
{
"path": [
"units",
"appliesToKey"
],
"required": true,
"required": false,
"hints": {},
"type": "array",
"description": "Every key from this list will be normalized.\nTo render the value properly (with a human readable denomination), use `{canonical(<key>)}`"
@ -16109,6 +16133,17 @@
"type": "boolean",
"description": "If set, then the canonical value will be prefixed instead, e.g. for '€'\nNote that if all values use 'prefix', the dropdown might move to before the text field"
},
{
"path": [
"units",
"applicableUnits",
"addSpace"
],
"required": false,
"hints": {},
"type": "boolean",
"description": "If set, add a space between the quantity and the denomination.\nE.g.: `50 mph` instad of `50mph`"
},
{
"path": [
"units",

View file

@ -832,6 +832,10 @@
"if": "value=tree_node",
"then": "tree_node - A layer showing trees"
},
{
"if": "value=unit",
"then": "unit - Library layer with all common units"
},
{
"if": "value=usersettings",
"then": "usersettings - A special layer which is not meant to be shown on a map, but which is used to set user settings"
@ -1183,14 +1187,26 @@
"type": "boolean"
},
"titleIcons": {
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)\n\nType: icon[]\ngroup: infobox",
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)\n\nUse `auto:<tagrenderingId>` to automatically create an icon based on a tagRendering which has icons\n\nType: icon[]\ngroup: infobox",
"anyOf": [
{
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/TagRenderingConfigJson"
"allOf": [
{
"$ref": "#/definitions/TagRenderingConfigJson"
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}
]
},
{
"type": "string"
@ -1300,7 +1316,7 @@
}
},
"tagRenderings": {
"description": "question: Edit this attribute showing piece/question\n\nA tag rendering is a block that either shows the known value or asks a question.\n\nRefer to the class `TagRenderingConfigJson` to see the possibilities.\n\nNote that we can also use a string here - where the string refers to a tag rendering defined in `assets/questions/questions.json`,\nwhere a few very general questions are defined e.g. website, phone number, ...\nFurthermore, _all_ the questions of another layer can be reused with `otherlayer.*`\nIf you need only a single of the tagRenderings, use `otherlayer.tagrenderingId`\nIf one or more questions have a 'group' or 'label' set, select all the entries with the corresponding group or label with `otherlayer.*group`\nRemark: if a tagRendering is 'lent' from another layer, the 'source'-tags are copied and added as condition.\nIf they are not wanted, remove them with an override\n\nA special value is 'questions', which indicates the location of the questions box. If not specified, it'll be appended to the bottom of the featureInfobox.\n\nAt last, one can define a group of renderings where parts of all strings will be replaced by multiple other strings.\nThis is mainly create questions for a 'left' and a 'right' side of the road.\nThese will be grouped and questions will be asked together\n\ntype: tagrendering[]\ngroup: tagrenderings",
"description": "question: Edit this way this attributed is displayed or queried\n\nA tag rendering is a block that either shows the known value or asks a question.\n\nRefer to the class `TagRenderingConfigJson` to see the possibilities.\n\nNote that we can also use a string here - where the string refers to a tag rendering defined in `assets/questions/questions.json`,\nwhere a few very general questions are defined e.g. website, phone number, ...\nFurthermore, _all_ the questions of another layer can be reused with `otherlayer.*`\nIf you need only a single of the tagRenderings, use `otherlayer.tagrenderingId`\nIf one or more questions have a 'group' or 'label' set, select all the entries with the corresponding group or label with `otherlayer.*group`\nRemark: if a tagRendering is 'lent' from another layer, the 'source'-tags are copied and added as condition.\nIf they are not wanted, remove them with an override\n\nA special value is 'questions', which indicates the location of the questions box. If not specified, it'll be appended to the bottom of the featureInfobox.\n\nAt last, one can define a group of renderings where parts of all strings will be replaced by multiple other strings.\nThis is mainly create questions for a 'left' and a 'right' side of the road.\nThese will be grouped and questions will be asked together\n\ntype: tagrendering[]\ngroup: tagrenderings",
"type": "array",
"items": {
"anyOf": [
@ -1706,7 +1722,14 @@
"units": {
"type": "array",
"items": {
"$ref": "#/definitions/default_2"
"anyOf": [
{
"$ref": "#/definitions/default_2"
},
{
"$ref": "#/definitions/Record<string,string|{quantity:string;denominations:string[];}>"
}
]
}
},
"syncSelection": {
@ -2889,7 +2912,19 @@
"items": {
"anyOf": [
{
"$ref": "#/definitions/TagRenderingConfigJson"
"allOf": [
{
"$ref": "#/definitions/TagRenderingConfigJson"
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}
]
},
{
"type": "string"
@ -2911,7 +2946,17 @@
"maxItems": 1
}
],
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)"
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)\nUse `auto:<tagrenderingId>` to automatically create an icon based on a tagRendering which has icons"
},
{
"path": [
"layers",
"titleIcons"
],
"required": false,
"hints": {},
"type": "object",
"description": "A TagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet.\nFor an _editable_ tagRendering, use 'QuestionableTagRenderingConfigJson' instead, which extends this one"
},
{
"path": [
@ -12476,6 +12521,10 @@
"if": "value=tree_node",
"then": "tree_node - A layer showing trees"
},
{
"if": "value=unit",
"then": "unit - Library layer with all common units"
},
{
"if": "value=usersettings",
"then": "usersettings - A special layer which is not meant to be shown on a map, but which is used to set user settings"
@ -12545,7 +12594,7 @@
"hints": {
"typehint": "tagrendering[]",
"group": "tagrenderings",
"question": "Edit this attribute showing piece/question"
"question": "Edit this way this attributed is displayed or queried"
},
"type": [
{
@ -18273,14 +18322,13 @@
{
"path": [
"layers",
"units"
"units",
"quantity"
],
"required": false,
"hints": {
"default": "ult: true,"
},
"type": "object",
"description": "In some cases, a value is represented in a certain unit (such as meters for heigt/distance/..., km/h for speed, ...)\nSometimes, multiple denominations are possible (e.g. km/h vs mile/h; megawatt vs kilowatt vs gigawatt for power generators, ...)\nThis brings in some troubles, as there are multiple ways to write it (no denomitation, 'm' vs 'meter' 'metre', ...)\nNot only do we want to write consistent data to OSM, we also want to present this consistently to the user.\nThis is handled by defining units.\n# Rendering\nTo render a value with long (human) denomination, use {canonical(key)}\n# Usage\nFirst of all, you define which keys have units applied, for example:\n```\nunits: [\n appliesTo: [\"maxspeed\", \"maxspeed:hgv\", \"maxspeed:bus\"]\n applicableUnits: [\n ...\n ]\n]\n```\nApplicableUnits defines which is the canonical extension, how it is presented to the user, ...:\n```\napplicableUnits: [\n{\n canonicalDenomination: \"km/h\",\n alternativeDenomination: [\"km/u\", \"kmh\", \"kph\"]\n human: {\n en: \"kilometer/hour\",\n nl: \"kilometer/uur\"\n },\n humanShort: {\n en: \"km/h\",\n nl: \"km/u\"\n }\n},\n{\n canoncialDenomination: \"mph\",\n ... similar for miles an hour ...\n}\n]\n```\nIf this is defined, then every key which the denominations apply to (`maxspeed`, `maxspeed:hgv` and `maxspeed:bus`) will be rewritten at the metatagging stage:\nevery value will be parsed and the canonical extension will be added add presented to the other parts of the code.\nAlso, if a freeform text field is used, an extra dropdown with applicable denominations will be given"
"hints": {},
"type": "string",
"description": "What is quantified? E.g. 'speed', 'length' (including width, diameter, ...), 'electric tension', 'electric current', 'duration'"
},
{
"path": [
@ -18288,7 +18336,7 @@
"units",
"appliesToKey"
],
"required": true,
"required": false,
"hints": {},
"type": "array",
"description": "Every key from this list will be normalized.\nTo render the value properly (with a human readable denomination), use `{canonical(<key>)}`"
@ -18423,6 +18471,18 @@
"type": "boolean",
"description": "If set, then the canonical value will be prefixed instead, e.g. for '€'\nNote that if all values use 'prefix', the dropdown might move to before the text field"
},
{
"path": [
"layers",
"units",
"applicableUnits",
"addSpace"
],
"required": false,
"hints": {},
"type": "boolean",
"description": "If set, add a space between the quantity and the denomination.\nE.g.: `50 mph` instad of `50mph`"
},
{
"path": [
"layers",
@ -19646,7 +19706,19 @@
"items": {
"anyOf": [
{
"$ref": "#/definitions/TagRenderingConfigJson"
"allOf": [
{
"$ref": "#/definitions/TagRenderingConfigJson"
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}
]
},
{
"type": "string"
@ -19668,7 +19740,18 @@
"maxItems": 1
}
],
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)"
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)\nUse `auto:<tagrenderingId>` to automatically create an icon based on a tagRendering which has icons"
},
{
"path": [
"layers",
"override",
"titleIcons"
],
"required": false,
"hints": {},
"type": "object",
"description": "A TagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet.\nFor an _editable_ tagRendering, use 'QuestionableTagRenderingConfigJson' instead, which extends this one"
},
{
"path": [
@ -29521,6 +29604,10 @@
"if": "value=tree_node",
"then": "tree_node - A layer showing trees"
},
{
"if": "value=unit",
"then": "unit - Library layer with all common units"
},
{
"if": "value=usersettings",
"then": "usersettings - A special layer which is not meant to be shown on a map, but which is used to set user settings"
@ -29592,7 +29679,7 @@
"hints": {
"typehint": "tagrendering[]",
"group": "tagrenderings",
"question": "Edit this attribute showing piece/question"
"question": "Edit this way this attributed is displayed or queried"
},
"type": [
{
@ -35526,14 +35613,13 @@
"path": [
"layers",
"override",
"units"
"units",
"quantity"
],
"required": false,
"hints": {
"default": "ult: true,"
},
"type": "object",
"description": "In some cases, a value is represented in a certain unit (such as meters for heigt/distance/..., km/h for speed, ...)\nSometimes, multiple denominations are possible (e.g. km/h vs mile/h; megawatt vs kilowatt vs gigawatt for power generators, ...)\nThis brings in some troubles, as there are multiple ways to write it (no denomitation, 'm' vs 'meter' 'metre', ...)\nNot only do we want to write consistent data to OSM, we also want to present this consistently to the user.\nThis is handled by defining units.\n# Rendering\nTo render a value with long (human) denomination, use {canonical(key)}\n# Usage\nFirst of all, you define which keys have units applied, for example:\n```\nunits: [\n appliesTo: [\"maxspeed\", \"maxspeed:hgv\", \"maxspeed:bus\"]\n applicableUnits: [\n ...\n ]\n]\n```\nApplicableUnits defines which is the canonical extension, how it is presented to the user, ...:\n```\napplicableUnits: [\n{\n canonicalDenomination: \"km/h\",\n alternativeDenomination: [\"km/u\", \"kmh\", \"kph\"]\n human: {\n en: \"kilometer/hour\",\n nl: \"kilometer/uur\"\n },\n humanShort: {\n en: \"km/h\",\n nl: \"km/u\"\n }\n},\n{\n canoncialDenomination: \"mph\",\n ... similar for miles an hour ...\n}\n]\n```\nIf this is defined, then every key which the denominations apply to (`maxspeed`, `maxspeed:hgv` and `maxspeed:bus`) will be rewritten at the metatagging stage:\nevery value will be parsed and the canonical extension will be added add presented to the other parts of the code.\nAlso, if a freeform text field is used, an extra dropdown with applicable denominations will be given"
"hints": {},
"type": "string",
"description": "What is quantified? E.g. 'speed', 'length' (including width, diameter, ...), 'electric tension', 'electric current', 'duration'"
},
{
"path": [
@ -35542,7 +35628,7 @@
"units",
"appliesToKey"
],
"required": true,
"required": false,
"hints": {},
"type": "array",
"description": "Every key from this list will be normalized.\nTo render the value properly (with a human readable denomination), use `{canonical(<key>)}`"
@ -35686,6 +35772,19 @@
"type": "boolean",
"description": "If set, then the canonical value will be prefixed instead, e.g. for '€'\nNote that if all values use 'prefix', the dropdown might move to before the text field"
},
{
"path": [
"layers",
"override",
"units",
"applicableUnits",
"addSpace"
],
"required": false,
"hints": {},
"type": "boolean",
"description": "If set, add a space between the quantity and the denomination.\nE.g.: `50 mph` instad of `50mph`"
},
{
"path": [
"layers",