Studio: first draft of layer editing

This commit is contained in:
Pieter Vander Vennet 2023-06-16 02:36:11 +02:00
parent 9661ade80c
commit 069767b9c7
43 changed files with 45374 additions and 5403 deletions

View file

@ -1,116 +1,118 @@
<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 {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"
export let type: ValidatorType
export let feedback: UIEventSource<Translation> | undefined = undefined
export let getCountry: () => string | undefined
export let placeholder: string | Translation | undefined
export let unit: Unit = undefined
export let type: ValidatorType
export let feedback: UIEventSource<Translation> | undefined = undefined
export let getCountry: () => string | undefined
export let placeholder: string | Translation | undefined
export let unit: Unit = undefined
export let value: UIEventSource<string>
/**
* 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 ?? "")
export let value: UIEventSource<string>
/**
* 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 validator: Validator = Validators.get(type ?? "string")
let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined)
let _placeholder = placeholder ?? validator?.getPlaceholder() ?? type
let validator: Validator = Validators.get(type ?? "string")
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)
if (denom) {
_value.setData(v)
selectedUnit.setData(denom.canonical)
} else {
_value.setData(value.data ?? "")
}
} else {
_value.setData(value.data ?? "")
function initValueAndDenom() {
if (unit && value.data) {
const [v, denom] = unit?.findDenomination(value.data, getCountry)
if (denom) {
_value.setData(v)
selectedUnit.setData(denom.canonical)
} else {
_value.setData(value.data ?? "")
}
} else {
_value.setData(value.data ?? "")
}
}
}
initValueAndDenom()
$: {
// The type changed -> reset some values
validator = Validators.get(type ?? "string")
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
feedback = feedback?.setData(validator?.getFeedback(_value.data, getCountry))
initValueAndDenom()
}
function setValues() {
// Update the value stores
const v = _value.data
if (!validator.isValid(v, getCountry) || v === "") {
value.setData(undefined)
feedback?.setData(validator.getFeedback(v, getCountry))
return
$: {
// The type changed -> reset some values
validator = Validators.get(type ?? "string")
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
feedback = feedback?.setData(validator?.getFeedback(_value.data, getCountry))
initValueAndDenom()
}
if (unit && isNaN(Number(v))) {
value.setData(undefined)
return
function setValues() {
// Update the value stores
const v = _value.data
if (!validator.isValid(v, getCountry) || v === "") {
value.setData(undefined)
feedback?.setData(validator.getFeedback(v, getCountry))
return
}
if (unit && isNaN(Number(v))) {
value.setData(undefined)
return
}
feedback?.setData(undefined)
value.setData(v + (selectedUnit.data ?? ""))
}
feedback?.setData(undefined)
value.setData(v + (selectedUnit.data ?? ""))
}
onDestroy(_value.addCallbackAndRun((_) => setValues()))
onDestroy(selectedUnit.addCallback((_) => setValues()))
if (validator === undefined) {
throw "Not a valid type for a validator:" + type
}
const isValid = _value.map((v) => validator.isValid(v, getCountry))
let htmlElem: HTMLInputElement
let dispatch = createEventDispatcher<{ selected }>()
$: {
if (htmlElem !== undefined) {
htmlElem.onfocus = () => dispatch("selected")
onDestroy(_value.addCallbackAndRun((_) => setValues()))
onDestroy(selectedUnit.addCallback((_) => setValues()))
if (validator === undefined) {
throw "Not a valid type for a validator:" + type
}
const isValid = _value.map((v) => validator.isValid(v, getCountry))
let htmlElem: HTMLInputElement
let dispatch = createEventDispatcher<{ selected, submit }>()
$: {
if (htmlElem !== undefined) {
htmlElem.onfocus = () => dispatch("selected")
}
}
}
</script>
{#if validator.textArea}
<form on:submit|preventDefault={() => dispatch("submit")}>
<textarea
class="w-full"
bind:value={$_value}
inputmode={validator.inputmode ?? "text"}
placeholder={_placeholder}
/>
class="w-full"
bind:value={$_value}
inputmode={validator.inputmode ?? "text"}
placeholder={_placeholder}></textarea>
</form>
{:else}
<span class="inline-flex">
<input
bind:this={htmlElem}
bind:value={$_value}
class="w-full"
inputmode={validator.inputmode ?? "text"}
placeholder={_placeholder}
/>
{#if !$isValid}
<ExclamationIcon class="-ml-6 h-6 w-6" />
{/if}
<form class="inline-flex" on:submit={() => dispatch("submit")}>
<input
bind:this={htmlElem}
bind:value={$_value}
class="w-full"
inputmode={validator.inputmode ?? "text"}
placeholder={_placeholder}
/>
{#if !$isValid}
<ExclamationIcon class="-ml-6 h-6 w-6"/>
{/if}
{#if unit !== undefined}
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value} />
{/if}
</span>
{#if unit !== undefined}
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value}/>
{/if}
</form>
{/if}

View file

@ -17,12 +17,12 @@
* If given, this function will be called to embed the given tags hint into this translation
*/
export let embedIn: ((string: string) => Translation) | undefined = undefined
const userDetails = state.osmConnection.userDetails
const userDetails = state?.osmConnection?.userDetails
let tagsExplanation = ""
$: tagsExplanation = tags?.asHumanString(true, false, currentProperties)
</script>
{#if $userDetails.loggedIn}
{#if !userDetails || $userDetails.loggedIn}
<div>
{#if tags === undefined}
<slot name="no-tags">No tags</slot>

View file

@ -26,7 +26,7 @@
export let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
let dispatch = createEventDispatcher<{ selected }>()
let dispatch = createEventDispatcher<{ selected, submit }>()
onDestroy(
value.addCallbackD(() => {
dispatch("selected")
@ -46,6 +46,8 @@
{getCountry}
{unit}
on:selected={() => dispatch("selected")}
on:submit={() => dispatch("submit")}
type={config.freeform.type}
{placeholder}
{value}
@ -57,6 +59,7 @@
{getCountry}
{unit}
on:selected={() => dispatch("selected")}
on:submit={() => dispatch("submit")}
type={config.freeform.type}
{placeholder}
{value}

View file

@ -23,7 +23,7 @@
export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>>
export let feature: Feature
export let layer: LayerConfig
export let layer: LayerConfig | undefined
let txt: string
$: onDestroy(

View file

@ -1,6 +1,6 @@
<script lang="ts">
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
import { UIEventSource } from "../../../Logic/UIEventSource"
import {Store, UIEventSource} from "../../../Logic/UIEventSource"
import type { Feature } from "geojson"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import TagRenderingAnswer from "./TagRenderingAnswer.svelte"
@ -18,7 +18,7 @@
export let state: SpecialVisualizationState
export let layer: LayerConfig
export let editingEnabled = state.featureSwitchUserbadge
export let editingEnabled : Store<boolean> | undefined = state?.featureSwitchUserbadge
export let highlightedRendering: UIEventSource<string> = undefined
export let showQuestionIfUnknown: boolean = false
@ -67,7 +67,7 @@
</script>
<div bind:this={htmlElem} class="">
{#if config.question && $editingEnabled}
{#if config.question && (!editingEnabled || $editingEnabled)}
{#if editMode}
<TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer}>
<button

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
import {ImmutableStore, Store, UIEventSource} from "../../../Logic/UIEventSource"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import Tr from "../../Base/Tr.svelte"
import type { Feature } from "geojson"
@ -27,11 +27,11 @@
export let tags: UIEventSource<Record<string, string>>
export let selectedElement: Feature
export let state: SpecialVisualizationState
export let layer: LayerConfig
export let layer: LayerConfig | undefined
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
let unit: Unit = layer.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
// Will be bound if a freeform is available
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key])
@ -45,7 +45,7 @@
return m.hideInAnswer.matchesProperties(tags.data)
})
// We received a new config -> reinit
unit = layer.units.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
if (
config.mappings?.length > 0 &&
@ -96,7 +96,7 @@
if (selectedTags === undefined) {
return
}
if (layer.source === null) {
if (layer === undefined || layer?.source === null) {
/**
* This is a special, priviliged layer.
* We simply apply the tags onto the records
@ -128,12 +128,12 @@
.catch(console.error)
}
let featureSwitchIsTesting = state.featureSwitchIsTesting
let featureSwitchIsDebugging = state.featureSwitches.featureSwitchIsDebugging
let showTags = state.userRelatedState.showTags
let featureSwitchIsTesting = state.featureSwitchIsTesting ?? new ImmutableStore(false)
let featureSwitchIsDebugging = state.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false)
let showTags = state.userRelatedState?.showTags ?? new ImmutableStore(undefined)
let numberOfCs = state.osmConnection.userDetails.data.csCount
onDestroy(
state.osmConnection.userDetails.addCallbackAndRun((ud) => {
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
numberOfCs = ud.csCount
})
)
@ -176,6 +176,7 @@
{unit}
feature={selectedElement}
value={freeformInput}
on:submit={onSave}
/>
{:else if mappings !== undefined && !config.multiAnswer}
<!-- Simple radiobuttons as mapping -->
@ -215,6 +216,7 @@
feature={selectedElement}
value={freeformInput}
on:selected={() => (selectedMapping = config.mappings?.length)}
on:submit={onSave}
/>
</label>
{/if}
@ -254,6 +256,7 @@
feature={selectedElement}
value={freeformInput}
on:selected={() => (checkedMappings[config.mappings.length] = true)}
on:submit={onSave}
/>
</label>
{/if}

View file

@ -0,0 +1,45 @@
<script lang="ts">
import EditLayerState from "./EditLayerState";
import layerSchemaRaw from "../../assets/layerconfigmeta.json"
import Region from "./Region.svelte";
import TabbedGroup from "../Base/TabbedGroup.svelte";
import {UIEventSource} from "../../Logic/UIEventSource";
import type {ConfigMeta} from "./configMeta";
import {Utils} from "../../Utils";
let state = new EditLayerState()
let layer = state.layer
const layerSchema: ConfigMeta[] = layerSchemaRaw
const regions = Utils.Dedup(layerSchema.map(meta => meta.hints.group))
.filter(region => region !== undefined)
const perRegion: Record<string, ConfigMeta[]> = {}
for (const region of regions) {
perRegion[region] = layerSchema.filter(meta => meta.hints.group === region)
}
console.log({perRegion})
</script>
<h3>Edit layer {$layer?.id}</h3>
<TabbedGroup tab={new UIEventSource(0)}>
<div slot="title0">General properties</div>
<div class="flex flex-col" slot="content0">
{#each regions as region}
<Region {state} configs={perRegion[region]} title={region}/>
{/each}
</div>
<div slot="title1">Information panel (questions and answers)</div>
<div slot="content1">
Information panel (todo)
</div>
<div slot="title2">Rendering on the map</div>
<div slot="content2">
TODO: rendering on the map
</div>
</TabbedGroup>

View file

@ -0,0 +1,8 @@
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
export default class EditLayerState {
public readonly osmConnection: OsmConnection
constructor() {
this.osmConnection = new OsmConnection({})
}
}

21
UI/Studio/Region.svelte Normal file
View file

@ -0,0 +1,21 @@
<script lang="ts">/***
* A 'region' is a collection of properties that can be edited which are somewhat related.
* They will typically be a subset of some properties
*/
import SchemaBasedField from "./SchemaBasedField.svelte";
import type {ConfigMeta} from "./configMeta";
import EditLayerState from "./EditLayerState";
export let state : EditLayerState
export let configs: ConfigMeta[]
export let title: string
</script>
<h3>{title}</h3>
<div class="pl-2 border border-black flex flex-col gap-y-1">
{#each configs as config}
<SchemaBasedField {state} schema={config} title={config.path.at(-1)}></SchemaBasedField>
{/each}
</div>

View file

@ -0,0 +1,6 @@
<script lang="ts">
export let title
export let description
</script>

View file

@ -0,0 +1,55 @@
<script lang="ts">
import {UIEventSource} from "../../Logic/UIEventSource";
import {Translation} from "../i18n/Translation";
import type {ConfigMeta} from "./configMeta";
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import type {
QuestionableTagRenderingConfigJson
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
import EditLayerState from "./EditLayerState";
export let state: EditLayerState
export let schema: ConfigMeta
export let title: string | undefined
let value = new UIEventSource<string>(undefined)
let feedback = new UIEventSource<Translation>(undefined)
const configJson: QuestionableTagRenderingConfigJson = {
id: schema.path.join("."),
render: schema.path.at(-1) + ": <b>{value}</b>",
question: schema.hints.question,
questionHint: schema.description,
freeform: {
key: "value",
type: schema.hints.typehint ?? "string"
}
}
if (!schema.required) {
configJson.mappings = [{
if: "value=",
then: schema.path.at(-1) + " is not set. " + (schema.hints.ifunset ?? ""),
}]
}
let config: TagRenderingConfig
let err: string = undefined
try {
config = new TagRenderingConfig(configJson, "config based on " + schema.path.join("."))
} catch (e) {
console.error(e, config)
err = e
}
let tags = new UIEventSource<Record<string, string>>({})
</script>
{#if err !== undefined}
<span class="alert">{err}</span>
{:else}
<div>
<TagRenderingEditable {config} showQuestionIfUnknown={true} {state} {tags}/>
</div>
{/if}

14
UI/Studio/configMeta.ts Normal file
View file

@ -0,0 +1,14 @@
import { JsonSchema, JsonSchemaType } from "./jsonSchema"
export interface ConfigMeta {
path: string[]
type: JsonSchemaType | JsonSchema[]
hints: {
group?: string
typehint?: string
question?: string
ifunset?: string
}
required: boolean
description: string
}

20
UI/Studio/jsonSchema.ts Normal file
View file

@ -0,0 +1,20 @@
/**
* Extracts the data from the scheme file and writes them in a flatter structure
*/
export type JsonSchemaType =
| string
| { $ref: string; description: string }
| { type: string }
| JsonSchemaType[]
export interface JsonSchema {
description?: string
type?: JsonSchemaType
properties?: any
items?: JsonSchema
allOf?: JsonSchema[]
anyOf: JsonSchema[]
enum: JsonSchema[]
$ref: string
required: string[]
}

7
UI/StudioGUI.svelte Normal file
View file

@ -0,0 +1,7 @@
<script lang="ts">
import EditLayer from "./Studio/EditLayer.svelte";
</script>
<EditLayer/>

10
UI/StudioGui.ts Normal file
View file

@ -0,0 +1,10 @@
import SvelteUIElement from "./Base/SvelteUIElement"
import StudioGUI from "./StudioGUI.svelte"
export default class StudioGui {
public setup() {
new SvelteUIElement(StudioGUI, {}).AttachTo("main")
}
}
new StudioGui().setup()