forked from MapComplete/MapComplete
Merge master
This commit is contained in:
commit
80168f5d0d
919 changed files with 95585 additions and 8504 deletions
65
src/UI/Studio/EditLayer.svelte
Normal file
65
src/UI/Studio/EditLayer.svelte
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<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";
|
||||
|
||||
import drinking_water from "../../assets/layers/drinking_water/drinking_water.json"
|
||||
|
||||
const layerSchema: ConfigMeta[] = layerSchemaRaw
|
||||
let state = new EditLayerState(layerSchema)
|
||||
state.configuration.setData(drinking_water)
|
||||
/**
|
||||
* Blacklist for the general area tab
|
||||
*/
|
||||
const regionBlacklist = ["hidden",undefined,"infobox", "tagrenderings","maprendering", "editing"]
|
||||
const allNames = Utils.Dedup(layerSchema.map(meta => meta.hints.group))
|
||||
|
||||
const perRegion: Record<string, ConfigMeta[]> = {}
|
||||
for (const region of allNames) {
|
||||
perRegion[region] = layerSchema.filter(meta => meta.hints.group === region)
|
||||
}
|
||||
|
||||
const baselayerRegions: string[] = ["Basic", "presets","filters","advanced","expert"]
|
||||
for (const baselayerRegion of baselayerRegions) {
|
||||
if(perRegion[baselayerRegion] === undefined){
|
||||
console.error("BaseLayerRegions in editLayer: no items have group '"+baselayerRegion+'"')
|
||||
}
|
||||
}
|
||||
const leftoverRegions : string[] = allNames.filter(r => regionBlacklist.indexOf(r) <0 && baselayerRegions.indexOf(r) <0 )
|
||||
</script>
|
||||
|
||||
<h3>Edit layer</h3>
|
||||
|
||||
<div class="m4">
|
||||
{allNames}
|
||||
<TabbedGroup tab={new UIEventSource(1)}>
|
||||
<div slot="title0">General properties</div>
|
||||
<div class="flex flex-col" slot="content0">
|
||||
{#each baselayerRegions as region}
|
||||
<Region {state} configs={perRegion[region]} title={region}/>
|
||||
{/each}
|
||||
|
||||
{#each leftoverRegions as region}
|
||||
<Region {state} configs={perRegion[region]} title={region}/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div slot="title1">Information panel (questions and answers)</div>
|
||||
<div slot="content1">
|
||||
<Region {state} configs={perRegion["title"]} title="Title"/>
|
||||
<Region {state} configs={perRegion["tagrenderings"]} title="Infobox"/>
|
||||
<Region {state} configs={perRegion["editing"]} title="Other editing elements"/>
|
||||
</div>
|
||||
|
||||
<div slot="title2">Rendering on the map</div>
|
||||
<div slot="content2">
|
||||
TODO: rendering on the map
|
||||
</div>
|
||||
</TabbedGroup>
|
||||
|
||||
</div>
|
||||
100
src/UI/Studio/EditLayerState.ts
Normal file
100
src/UI/Studio/EditLayerState.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { ConfigMeta } from "./configMeta"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import { DeleteConfigJson } from "../../Models/ThemeConfig/Json/DeleteConfigJson"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export default class EditLayerState {
|
||||
public readonly osmConnection: OsmConnection
|
||||
public readonly schema: ConfigMeta[]
|
||||
|
||||
public readonly featureSwitches: { featureSwitchIsDebugging: UIEventSource<boolean> }
|
||||
|
||||
public readonly configuration: UIEventSource<Partial<LayerConfigJson>> = new UIEventSource<
|
||||
Partial<LayerConfigJson>
|
||||
>({})
|
||||
|
||||
constructor(schema: ConfigMeta[]) {
|
||||
this.schema = schema
|
||||
this.osmConnection = new OsmConnection({})
|
||||
this.featureSwitches = {
|
||||
featureSwitchIsDebugging: new UIEventSource<boolean>(true),
|
||||
}
|
||||
this.configuration.addCallback((config) => {
|
||||
console.log("Current config is", Utils.Clone(config))
|
||||
})
|
||||
}
|
||||
|
||||
public getCurrentValueFor(path: ReadonlyArray<string | number>): any | undefined {
|
||||
// Walk the path down to see if we find something
|
||||
let entry = this.configuration.data
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
if (entry === undefined) {
|
||||
// We reached a dead end - no old vlaue
|
||||
return undefined
|
||||
}
|
||||
const breadcrumb = path[i]
|
||||
entry = entry[breadcrumb]
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
public register(
|
||||
path: ReadonlyArray<string | number>,
|
||||
value: Store<any>,
|
||||
noInitialSync: boolean = false
|
||||
): () => void {
|
||||
const unsync = value.addCallback((v) => this.setValueAt(path, v))
|
||||
if (!noInitialSync) {
|
||||
this.setValueAt(path, value.data)
|
||||
}
|
||||
return unsync
|
||||
}
|
||||
|
||||
public getSchemaStartingWith(path: string[]) {
|
||||
return this.schema.filter(
|
||||
(sch) =>
|
||||
!path.some((part, i) => !(sch.path.length > path.length && sch.path[i] === part))
|
||||
)
|
||||
}
|
||||
|
||||
public getTranslationAt(path: string[]): ConfigMeta {
|
||||
const origConfig = this.getSchema(path)[0]
|
||||
return {
|
||||
path,
|
||||
type: "translation",
|
||||
hints: {
|
||||
typehint: "translation",
|
||||
},
|
||||
required: origConfig.required ?? false,
|
||||
description: origConfig.description ?? "A translatable object",
|
||||
}
|
||||
}
|
||||
public getSchema(path: string[]): ConfigMeta[] {
|
||||
return this.schema.filter(
|
||||
(sch) =>
|
||||
sch !== undefined &&
|
||||
!path.some((part, i) => !(sch.path.length == path.length && sch.path[i] === part))
|
||||
)
|
||||
}
|
||||
|
||||
public setValueAt(path: ReadonlyArray<string | number>, v: any) {
|
||||
{
|
||||
let entry = this.configuration.data
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const breadcrumb = path[i]
|
||||
if (entry[breadcrumb] === undefined) {
|
||||
entry[breadcrumb] = typeof path[i + 1] === "number" ? [] : {}
|
||||
}
|
||||
entry = entry[breadcrumb]
|
||||
}
|
||||
if (v) {
|
||||
entry[path.at(-1)] = v
|
||||
} else if (entry) {
|
||||
delete entry[path.at(-1)]
|
||||
}
|
||||
this.configuration.ping()
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/UI/Studio/Region.svelte
Normal file
29
src/UI/Studio/Region.svelte
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<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 type {ConfigMeta} from "./configMeta";
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import SchemaBasedInput from "./SchemaBasedInput.svelte";
|
||||
|
||||
export let state: EditLayerState
|
||||
export let configs: ConfigMeta[]
|
||||
export let title: string
|
||||
|
||||
</script>
|
||||
{#if title}
|
||||
<h3>{title}</h3>
|
||||
<div class="pl-2 border border-black flex flex-col gap-y-1">
|
||||
|
||||
{#each configs as config}
|
||||
<SchemaBasedInput {state} path={config.path} schema={config}/>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="pl-2 flex flex-col gap-y-1">
|
||||
{#each configs as config}
|
||||
<SchemaBasedInput {state} path={config.path} schema={config}/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
65
src/UI/Studio/RegisteredTagInput.svelte
Normal file
65
src/UI/Studio/RegisteredTagInput.svelte
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<script lang="ts">
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import type {TagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson";
|
||||
import TagInput from "./TagInput/TagInput.svelte";
|
||||
import type {ConfigMeta} from "./configMeta";
|
||||
import {PencilAltIcon} from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
/**
|
||||
* Thin wrapper around 'TagInput' which registers the output with the state
|
||||
*/
|
||||
export let path: (string | number)[]
|
||||
export let state: EditLayerState
|
||||
|
||||
export let schema: ConfigMeta
|
||||
|
||||
const initialValue = state.getCurrentValueFor(path)
|
||||
let tag: UIEventSource<TagConfigJson> = new UIEventSource<TagConfigJson>(initialValue)
|
||||
|
||||
onDestroy(state.register(path, tag))
|
||||
|
||||
let mode: "editing" | "set" = tag.data === undefined ? "editing" : "set"
|
||||
|
||||
function simplify(tag: TagConfigJson): string {
|
||||
if (typeof tag === "string") {
|
||||
return tag
|
||||
}
|
||||
if (tag["and"]) {
|
||||
return "{ and: " + simplify(tag["and"].map(simplify).join(" ; ") + " }")
|
||||
}
|
||||
if (tag["or"]) {
|
||||
return "{ or: " + simplify(tag["or"].map(simplify).join(" ; ") + " }")
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#if mode === "editing"}
|
||||
<div class="interactive border-interactive">
|
||||
<h3>{schema.hints.question ?? "What tags should be applied?"}</h3>
|
||||
{schema.description}
|
||||
<TagInput {tag}/>
|
||||
<div class="flex justify-end">
|
||||
|
||||
<button class="primary w-fit" on:click={() => {mode = "set"}}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="low-interaction flex justify-between">
|
||||
<div>
|
||||
|
||||
{schema.path.at(-1)}
|
||||
{simplify($tag)}
|
||||
</div>
|
||||
<button
|
||||
on:click={() => {mode = "editing"}}
|
||||
class="secondary h-8 w-8 shrink-0 self-start rounded-full p-1"
|
||||
>
|
||||
<PencilAltIcon/>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
168
src/UI/Studio/SchemaBaseMultiType.svelte
Normal file
168
src/UI/Studio/SchemaBaseMultiType.svelte
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
<script lang="ts">
|
||||
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import type { ConfigMeta } from "./configMeta";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import type {
|
||||
QuestionableTagRenderingConfigJson
|
||||
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import { onDestroy } from "svelte";
|
||||
import SchemaBasedInput from "./SchemaBasedInput.svelte";
|
||||
import type { JsonSchemaType } from "./jsonSchema";
|
||||
// @ts-ignore
|
||||
import nmd from "nano-markdown";
|
||||
|
||||
/**
|
||||
* If 'types' is defined: allow the user to pick one of the types to input.
|
||||
*/
|
||||
|
||||
export let state: EditLayerState;
|
||||
export let path: (string | number)[] = [];
|
||||
export let schema: ConfigMeta;
|
||||
const defaultOption = schema.hints.typesdefault ? Number(schema.hints.typesdefault) : undefined;
|
||||
|
||||
const hasBooleanOption = (<JsonSchemaType[]>schema.type)?.findIndex(t => t["type"] === "boolean");
|
||||
const types = schema.hints.types.split(";");
|
||||
if (hasBooleanOption >= 0) {
|
||||
console.log(path.join("."), ": types are", types, ", boolean index is", hasBooleanOption);
|
||||
types.splice(hasBooleanOption);
|
||||
}
|
||||
|
||||
const configJson: QuestionableTagRenderingConfigJson = {
|
||||
id: "TYPE_OF:" + path.join("_"),
|
||||
question: "Which subcategory is needed?",
|
||||
questionHint: nmd(schema.description),
|
||||
mappings: types.map(opt => opt.trim()).filter(opt => opt.length > 0).map((opt, i) => ({
|
||||
if: "value=" + i,
|
||||
addExtraTags: ["direct="],
|
||||
then: opt + (i === defaultOption ? " (Default)" : "")
|
||||
}))
|
||||
};
|
||||
let tags = new UIEventSource<Record<string, string>>({});
|
||||
|
||||
if (hasBooleanOption >= 0) {
|
||||
configJson.mappings.unshift(
|
||||
{
|
||||
if: "direct=true",
|
||||
then: "Yes " + (schema.hints.iftrue ?? ""),
|
||||
addExtraTags: ["value="]
|
||||
},
|
||||
{
|
||||
if: "direct=false",
|
||||
then: "No " + (schema.hints.iffalse ?? ""),
|
||||
addExtraTags: ["value="]
|
||||
}
|
||||
);
|
||||
}
|
||||
const config = new TagRenderingConfig(configJson, "config based on " + schema.path.join("."));
|
||||
|
||||
|
||||
const existingValue = state.getCurrentValueFor(path);
|
||||
console.log("Setting direct: ", hasBooleanOption, path.join("."), existingValue);
|
||||
if (hasBooleanOption >= 0 && (existingValue === true || existingValue === false)) {
|
||||
tags.setData({ direct: "" + existingValue });
|
||||
} else if (existingValue) {
|
||||
// We found an existing value. Let's figure out what type it matches and select that one
|
||||
// We run over all possibilities and check what is required
|
||||
const possibleTypes: { index: number, matchingPropertiesCount: number, optionalMatches: number }[] = [];
|
||||
outer: for (let i = 0; i < (<[]>schema.type).length; i++) {
|
||||
const type = schema.type[i];
|
||||
let optionalMatches = 0
|
||||
for (const key of Object.keys(type.properties ?? {})) {
|
||||
if(!!existingValue[key]){
|
||||
optionalMatches++
|
||||
}
|
||||
}
|
||||
if (type.required) {
|
||||
let numberOfMatches = 0;
|
||||
|
||||
for (const requiredAttribute of type.required) {
|
||||
if (existingValue[requiredAttribute] === undefined) {
|
||||
console.log(path.join("."), " does not have required field", requiredAttribute, " so it cannot be type ", type);
|
||||
// The 'existingValue' does _not_ have this required attribute, so it cannot be of this type
|
||||
continue outer;
|
||||
}
|
||||
numberOfMatches++;
|
||||
}
|
||||
possibleTypes.push({ index: i, matchingPropertiesCount: numberOfMatches , optionalMatches});
|
||||
} else {
|
||||
possibleTypes.push({ index: i, matchingPropertiesCount: 0, optionalMatches });
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
possibleTypes.sort((a, b) => b.optionalMatches - a.optionalMatches);
|
||||
possibleTypes.sort((a, b) => b.matchingPropertiesCount - a.matchingPropertiesCount);
|
||||
console.log(">>> possible types", possibleTypes)
|
||||
if (possibleTypes.length > 0) {
|
||||
tags.setData({ value: "" + possibleTypes[0].index });
|
||||
}
|
||||
} else if (defaultOption !== undefined) {
|
||||
tags.setData({ value: "" + defaultOption });
|
||||
}
|
||||
|
||||
if (hasBooleanOption >= 0) {
|
||||
|
||||
const directValue = tags.mapD(tags => {
|
||||
if (tags["value"]) {
|
||||
return undefined;
|
||||
}
|
||||
return tags["direct"] === "true";
|
||||
});
|
||||
onDestroy(state.register(path, directValue, true));
|
||||
}
|
||||
|
||||
let chosenOption: number = defaultOption;
|
||||
let subSchemas: ConfigMeta[] = [];
|
||||
|
||||
let subpath = path;
|
||||
|
||||
onDestroy(tags.addCallbackAndRun(tags => {
|
||||
const oldOption = chosenOption;
|
||||
chosenOption = tags["value"] ? Number(tags["value"]) : defaultOption;
|
||||
if (chosenOption !== oldOption) {
|
||||
// Reset the values beneath
|
||||
subSchemas = [];
|
||||
state.setValueAt(path, undefined);
|
||||
}
|
||||
const type = schema.type[chosenOption];
|
||||
console.log("Subtype is", type);
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
subpath = path;
|
||||
const cleanPath = <string[]>path.filter(p => typeof p === "string");
|
||||
if (type["$ref"] === "#/definitions/Record<string,string>") {
|
||||
// The subtype is a translation object
|
||||
const schema = state.getTranslationAt(cleanPath);
|
||||
console.log("Got a translation,", schema);
|
||||
subSchemas.push(schema);
|
||||
subpath = path.slice(0, path.length - 2);
|
||||
return;
|
||||
}
|
||||
if (!type.properties) {
|
||||
return;
|
||||
}
|
||||
for (const crumble of Object.keys(type.properties)) {
|
||||
subSchemas.push(...(state.getSchema([...cleanPath, crumble])));
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div class="p-2 border-2 border-dashed border-gray-300 flex flex-col gap-y-2">
|
||||
<div>
|
||||
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags} />
|
||||
</div>
|
||||
|
||||
{#if chosenOption !== undefined}
|
||||
{#each subSchemas as subschema}
|
||||
<SchemaBasedInput {state} schema={subschema}
|
||||
path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
94
src/UI/Studio/SchemaBasedArray.svelte
Normal file
94
src/UI/Studio/SchemaBasedArray.svelte
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<script lang="ts">
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import type {ConfigMeta} from "./configMeta";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import SchemaBasedInput from "./SchemaBasedInput.svelte";
|
||||
import SchemaBasedField from "./SchemaBasedField.svelte";
|
||||
import {TrashIcon} from "@babeard/svelte-heroicons/mini";
|
||||
|
||||
export let state: EditLayerState
|
||||
export let schema: ConfigMeta
|
||||
|
||||
let title = schema.path.at(-1)
|
||||
let singular = title
|
||||
if (title.endsWith("s")) {
|
||||
singular = title.slice(0, title.length - 1)
|
||||
}
|
||||
let article = "a"
|
||||
if (singular.match(/^[aeoui]/)) {
|
||||
article = "an"
|
||||
}
|
||||
export let path: (string | number)[] = []
|
||||
|
||||
const subparts = state.getSchemaStartingWith(schema.path)
|
||||
|
||||
let createdItems = 0
|
||||
/**
|
||||
* Keeps track of the items.
|
||||
* We keep a single string (stringified 'createdItems') to make sure the order is correct
|
||||
*/
|
||||
export let values: UIEventSource<number[]> = new UIEventSource<number[]>([])
|
||||
|
||||
const currentValue = <[]>state.getCurrentValueFor(path)
|
||||
if (currentValue) {
|
||||
if (!Array.isArray(currentValue)) {
|
||||
console.error("SchemaBaseArray for path", path, "expected an array as initial value, but got a", typeof currentValue, currentValue)
|
||||
} else {
|
||||
values.setData(currentValue.map((_, i) => i))
|
||||
}
|
||||
}
|
||||
|
||||
function createItem() {
|
||||
values.data.push(createdItems)
|
||||
createdItems++
|
||||
values.ping()
|
||||
}
|
||||
|
||||
function fusePath(i: number, subpartPath: string[]): (string | number)[] {
|
||||
const newPath = [...path, i]
|
||||
const toAdd = [...subpartPath]
|
||||
for (const part of path) {
|
||||
if (toAdd[0] === part) {
|
||||
toAdd.splice(0, 1)
|
||||
}
|
||||
}
|
||||
newPath.push(...toAdd)
|
||||
return newPath
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="pl-2">
|
||||
<h3>{schema.path.at(-1)}</h3>
|
||||
|
||||
{#if subparts.length > 0}
|
||||
<span class="subtle">
|
||||
{schema.description}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if $values.length === 0}
|
||||
No values are defined
|
||||
{:else if subparts.length === 0}
|
||||
<!-- We need an array of values, so we use the typehint of the _parent_ element as field -->
|
||||
{#each $values as value (value)}
|
||||
<SchemaBasedField {state} {schema} path={[...path, value]}/>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each $values as value (value)}
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="m-0">{singular} {value}</h3>
|
||||
<button class="border-black border rounded-full p-1 w-fit h-fit" on:click={() => {values.data.splice(values.data.indexOf(value)); values.ping()}}>
|
||||
<TrashIcon class="w-4 h-4"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="border border-black">
|
||||
{#each subparts as subpart}
|
||||
<SchemaBasedInput {state} path={fusePath(value, subpart.path)} schema={subpart}/>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<button on:click={createItem}>Add {article} {singular}</button>
|
||||
</div>
|
||||
110
src/UI/Studio/SchemaBasedField.svelte
Normal file
110
src/UI/Studio/SchemaBasedField.svelte
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
<script lang="ts">
|
||||
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import type {ConfigMeta} from "./configMeta";
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import nmd from "nano-markdown"
|
||||
import type {
|
||||
QuestionableTagRenderingConfigJson
|
||||
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
|
||||
export let state: EditLayerState
|
||||
export let path: (string | number)[] = []
|
||||
export let schema: ConfigMeta
|
||||
let value = new UIEventSource<string>(undefined)
|
||||
|
||||
let type = schema.hints.typehint ?? "string"
|
||||
if(type === "rendered"){
|
||||
type = "translation"
|
||||
}
|
||||
const isTranslation =schema.hints.typehint === "translation" || schema.hints.typehint === "rendered"
|
||||
|
||||
const configJson: QuestionableTagRenderingConfigJson = {
|
||||
id: path.join("_"),
|
||||
render: schema.type === "boolean" ? undefined : ((schema.hints.inline ?? schema.path.at(-1) )+ ": <b>{value}</b>"),
|
||||
question: schema.hints.question,
|
||||
questionHint: nmd(schema.description),
|
||||
freeform: schema.type === "boolean" ? undefined : {
|
||||
key: "value",
|
||||
type,
|
||||
inline: schema.hints.inline !== undefined
|
||||
},
|
||||
}
|
||||
|
||||
if (schema.hints.default) {
|
||||
configJson.mappings = [{
|
||||
if: "value=", // We leave this blank
|
||||
then: schema.path.at(-1) + " is not set. The default value <b>" + schema.hints.default + "</b> will be used. " + (schema.hints.ifunset ?? ""),
|
||||
}]
|
||||
} else if (!schema.required) {
|
||||
configJson.mappings = [{
|
||||
if: "value=",
|
||||
then: schema.path.at(-1) + " is not set. " + (schema.hints.ifunset ?? ""),
|
||||
}]
|
||||
}
|
||||
|
||||
if (schema.type === "boolean" || (Array.isArray(schema.type) && schema.type.some(t => t["type"] === "boolean"))) {
|
||||
configJson.mappings = configJson.mappings ?? []
|
||||
configJson.mappings.push(
|
||||
{
|
||||
if: "value=true",
|
||||
then: "Yes "+(schema.hints?.iftrue??"")
|
||||
},
|
||||
{
|
||||
if: "value=false",
|
||||
then: "No "+(schema.hints?.iffalse??"")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (schema.hints.suggestions) {
|
||||
if (!configJson.mappings) {
|
||||
configJson.mappings = []
|
||||
}
|
||||
configJson.mappings.push(...schema.hints.suggestions)
|
||||
}
|
||||
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 = path.join(".") + " " + e
|
||||
}
|
||||
let startValue = state.getCurrentValueFor(path)
|
||||
console.log("StartValue for", path.join("."), " is", startValue)
|
||||
if (typeof startValue !== "string") {
|
||||
startValue = JSON.stringify(startValue)
|
||||
}
|
||||
const tags = new UIEventSource<Record<string, string>>({value: startValue ?? ""})
|
||||
tags.addCallbackAndRunD(tgs => console.log(">>> tgs for",path.join("."),"are",tgs ))
|
||||
onDestroy(state.register(path, tags.map(tgs => {
|
||||
const v = tgs["value"];
|
||||
if (schema.type === "boolan") {
|
||||
return v === "true" || v === "yes" || v === "1"
|
||||
}
|
||||
if (schema.type === "number") {
|
||||
return Number(v)
|
||||
}
|
||||
console.log(schema, v)
|
||||
if(isTranslation) {
|
||||
if(v === ""){
|
||||
return {}
|
||||
}
|
||||
return JSON.parse(v)
|
||||
}
|
||||
return v
|
||||
}), isTranslation))
|
||||
</script>
|
||||
|
||||
{#if err !== undefined}
|
||||
<span class="alert">{err}</span>
|
||||
{:else}
|
||||
<div>
|
||||
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags}/>
|
||||
</div>
|
||||
{/if}
|
||||
27
src/UI/Studio/SchemaBasedInput.svelte
Normal file
27
src/UI/Studio/SchemaBasedInput.svelte
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import type { ConfigMeta } from "./configMeta";
|
||||
import SchemaBasedField from "./SchemaBasedField.svelte";
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import SchemaBasedArray from "./SchemaBasedArray.svelte";
|
||||
import SchemaBaseMultiType from "./SchemaBaseMultiType.svelte";
|
||||
import RegisteredTagInput from "./RegisteredTagInput.svelte";
|
||||
import SchemaBasedTranslationInput from "./SchemaBasedTranslationInput.svelte";
|
||||
|
||||
export let schema: ConfigMeta
|
||||
export let state: EditLayerState
|
||||
export let path: (string | number)[] = []
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{#if schema.type === "array"}
|
||||
<SchemaBasedArray {path} {state} {schema}/>
|
||||
{:else if schema.hints.typehint === "tag"}
|
||||
<RegisteredTagInput {state} {path} {schema}/>
|
||||
{:else if schema.type === "translation"}
|
||||
<SchemaBasedTranslationInput {path} {state} {schema}/>
|
||||
{:else if schema.hints.types}
|
||||
<SchemaBaseMultiType {path} {state} {schema}/>
|
||||
{:else}
|
||||
<SchemaBasedField {path} {state} {schema}/>
|
||||
{/if}
|
||||
16
src/UI/Studio/SchemaBasedTranslationInput.svelte
Normal file
16
src/UI/Studio/SchemaBasedTranslationInput.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import type { ConfigMeta } from "./configMeta";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import TranslationInput from "../InputElement/Helpers/TranslationInput.svelte";
|
||||
|
||||
export let state: EditLayerState;
|
||||
export let path: (string | number)[] = [];
|
||||
export let schema: ConfigMeta;
|
||||
|
||||
let value = new UIEventSource<string>("{}");
|
||||
console.log("Registering translation to path", path)
|
||||
state.register(path, value.mapD(v => JSON.parse(value.data )));
|
||||
</script>
|
||||
|
||||
<TranslationInput {value} />
|
||||
159
src/UI/Studio/TagExpression.svelte
Normal file
159
src/UI/Studio/TagExpression.svelte
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
<script lang="ts">/**
|
||||
* Allows to create `and` and `or` expressions graphically
|
||||
*/
|
||||
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import type {TagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson";
|
||||
import BasicTagInput from "./TagInput/BasicTagInput.svelte";
|
||||
import TagInput from "./TagInput/TagInput.svelte";
|
||||
import {TrashIcon} from "@babeard/svelte-heroicons/mini";
|
||||
|
||||
export let tag: UIEventSource<TagConfigJson>
|
||||
let mode: "and" | "or" = "and"
|
||||
|
||||
let basicTags: UIEventSource<UIEventSource<string>[]> = new UIEventSource([])
|
||||
|
||||
/**
|
||||
* Sub-expressions
|
||||
*/
|
||||
let expressions: UIEventSource<UIEventSource<TagConfigJson>[]> = new UIEventSource([])
|
||||
export let uploadableOnly: boolean
|
||||
export let overpassSupportNeeded: boolean
|
||||
|
||||
function update(_) {
|
||||
let config: TagConfigJson = <any>{}
|
||||
if (!mode) {
|
||||
return
|
||||
}
|
||||
const tags = []
|
||||
|
||||
const subpartSources = (<UIEventSource<string | TagConfigJson>[]>basicTags.data).concat(expressions.data)
|
||||
for (const src of subpartSources) {
|
||||
const t = src.data
|
||||
if (!t) {
|
||||
// We indicate upstream that this value is invalid
|
||||
tag.setData(undefined)
|
||||
return
|
||||
}
|
||||
tags.push(t)
|
||||
}
|
||||
if (tags.length === 1) {
|
||||
tag.setData(tags[0])
|
||||
} else {
|
||||
config[mode] = tags
|
||||
tag.setData(config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function addBasicTag(value?: string) {
|
||||
const src = new UIEventSource(value)
|
||||
basicTags.data.push(src);
|
||||
basicTags.ping()
|
||||
src.addCallbackAndRunD(_ => update(_))
|
||||
}
|
||||
|
||||
function removeTag(basicTag: UIEventSource<any>) {
|
||||
const index = basicTags.data.indexOf(basicTag)
|
||||
console.log("Removing", index, basicTag)
|
||||
if (index >= 0) {
|
||||
basicTag.setData(undefined)
|
||||
basicTags.data.splice(index, 1)
|
||||
basicTags.ping()
|
||||
}
|
||||
}
|
||||
|
||||
function removeExpression(expr: UIEventSource<any>) {
|
||||
const index = expressions.data.indexOf(expr)
|
||||
if (index >= 0) {
|
||||
expr.setData(undefined)
|
||||
expressions.data.splice(index, 1)
|
||||
expressions.ping()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function addExpression(expr?: TagConfigJson) {
|
||||
const src = new UIEventSource(expr)
|
||||
expressions.data.push(src);
|
||||
expressions.ping()
|
||||
src.addCallbackAndRunD(_ => update(_))
|
||||
}
|
||||
|
||||
|
||||
$: update(mode)
|
||||
expressions.addCallback(_ => update(_))
|
||||
basicTags.addCallback(_ => update(_))
|
||||
|
||||
let initialTag: TagConfigJson = tag.data
|
||||
|
||||
function initWith(initialTag: TagConfigJson) {
|
||||
if (typeof initialTag === "string") {
|
||||
addBasicTag(initialTag)
|
||||
return
|
||||
}
|
||||
mode = <"or" | "and">Object.keys(initialTag)[0]
|
||||
const subExprs = (<TagConfigJson[]>initialTag[mode])
|
||||
if (subExprs.length == 0) {
|
||||
return
|
||||
}
|
||||
if (subExprs.length == 1) {
|
||||
initWith(subExprs[0])
|
||||
return;
|
||||
}
|
||||
for (const subExpr of subExprs) {
|
||||
if (typeof subExpr === "string") {
|
||||
addBasicTag(subExpr)
|
||||
} else {
|
||||
addExpression(subExpr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!initialTag) {
|
||||
addBasicTag()
|
||||
} else {
|
||||
initWith(initialTag)
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="flex items-center">
|
||||
|
||||
<select bind:value={mode}>
|
||||
<option value="and">and</option>
|
||||
{#if !uploadableOnly}
|
||||
<option value="or">or</option>
|
||||
{/if}
|
||||
</select>
|
||||
<div class="border-l-4 border-black flex flex-col ml-1 pl-1">
|
||||
{#each $basicTags as basicTag (basicTag)}
|
||||
<div class="flex">
|
||||
<BasicTagInput {overpassSupportNeeded} {uploadableOnly} tag={basicTag}/>
|
||||
<button class="border border-black rounded-full w-fit h-fit p-0" on:click={() => removeTag(basicTag)}>
|
||||
<TrashIcon class="w-4 h-4 p-1"/>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{#each $expressions as expression}
|
||||
<TagInput {overpassSupportNeeded} {uploadableOnly} tag={expression}>
|
||||
<button slot="delete" on:click={() => removeExpression(expression)}>
|
||||
<TrashIcon class="w-4 h-4 p-1"/>
|
||||
Delete subexpression
|
||||
</button>
|
||||
</TagInput>
|
||||
{/each}
|
||||
<div class="flex">
|
||||
<button class="w-fit" on:click={() => addBasicTag()}>
|
||||
Add a tag
|
||||
</button>
|
||||
<button class="w-fit" on:click={() => addExpression()}>
|
||||
Add an expression
|
||||
</button>
|
||||
<slot name="delete"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
60
src/UI/Studio/TagInfoStats.svelte
Normal file
60
src/UI/Studio/TagInfoStats.svelte
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<script lang="ts">/**
|
||||
* A small component showing statistics from tagInfo.
|
||||
* Will show this in an 'alert' if very little (<250) tags are known
|
||||
*/
|
||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource";
|
||||
import type {TagInfoStats} from "../../Logic/Web/TagInfo";
|
||||
import TagInfo from "../../Logic/Web/TagInfo";
|
||||
import {twMerge} from "tailwind-merge";
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
|
||||
export let tag: UIEventSource<string>
|
||||
const tagStabilized = tag.stabilized(500)
|
||||
const tagInfoStats: Store<TagInfoStats> = tagStabilized.bind(tag => {
|
||||
if (!tag) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
|
||||
const t = TagUtils.Tag(tag)
|
||||
const k = t["key"]
|
||||
let v = t["value"]
|
||||
if (typeof v !== "string") {
|
||||
v = undefined
|
||||
}
|
||||
if (!k) {
|
||||
return undefined
|
||||
}
|
||||
return UIEventSource.FromPromise(TagInfo.global.getStats(k, v))
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
const tagInfoUrl: Store<string> = tagStabilized.mapD(tag => {
|
||||
try {
|
||||
|
||||
const t = TagUtils.Tag(tag)
|
||||
const k = t["key"]
|
||||
let v = t["value"]
|
||||
if (typeof v !== "string") {
|
||||
v = undefined
|
||||
}
|
||||
if (!k) {
|
||||
return undefined
|
||||
}
|
||||
return TagInfo.global.webUrl(k, v)
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
const total = tagInfoStats.mapD(data => data.data.find(i => i.type === "all").count)
|
||||
</script>
|
||||
|
||||
{#if $tagStabilized !== $tag}
|
||||
<Loading/>
|
||||
{:else if $tagInfoStats }
|
||||
<a href={$tagInfoUrl} target="_blank" class={twMerge(($total < 250) ? "alert" : "thanks", "w-fit link-underline")}>
|
||||
{$total} features on OSM have this tag
|
||||
</a>
|
||||
{/if}
|
||||
120
src/UI/Studio/TagInput/BasicTagInput.svelte
Normal file
120
src/UI/Studio/TagInput/BasicTagInput.svelte
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<script lang="ts">
|
||||
|
||||
import ValidatedInput from "../../InputElement/ValidatedInput.svelte";
|
||||
import {UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import {onDestroy} from "svelte";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import {TagUtils} from "../../../Logic/Tags/TagUtils";
|
||||
import TagInfoStats from "../TagInfoStats.svelte";
|
||||
|
||||
export let tag: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
export let uploadableOnly: boolean
|
||||
export let overpassSupportNeeded: boolean
|
||||
|
||||
let feedbackGlobal = tag.map(tag => {
|
||||
if (!tag) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
TagUtils.Tag(tag)
|
||||
return undefined
|
||||
} catch (e) {
|
||||
return e
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
let feedbackKey = new UIEventSource<string>(undefined)
|
||||
let keyValue = new UIEventSource<string>(undefined)
|
||||
|
||||
|
||||
let feedbackValue = new UIEventSource<string>(undefined)
|
||||
/**
|
||||
* The value of the tag. The name is a bit confusing
|
||||
*/
|
||||
let valueValue = new UIEventSource<string>(undefined)
|
||||
|
||||
|
||||
let mode: string = "="
|
||||
let modes: string[] = []
|
||||
|
||||
for (const k in TagUtils.modeDocumentation) {
|
||||
const docs = TagUtils.modeDocumentation[k]
|
||||
if (overpassSupportNeeded && !docs.overpassSupport) {
|
||||
continue
|
||||
}
|
||||
if (uploadableOnly && !docs.uploadable) {
|
||||
continue
|
||||
}
|
||||
modes.push(k)
|
||||
}
|
||||
if (!uploadableOnly && !overpassSupportNeeded) {
|
||||
modes.push(...TagUtils.comparators.map(c => c[0]))
|
||||
}
|
||||
|
||||
|
||||
if (tag.data) {
|
||||
const sortedModes = [...modes]
|
||||
sortedModes.sort((a, b) => b.length - a.length)
|
||||
const t = tag.data
|
||||
console.log(t)
|
||||
for (const m of sortedModes) {
|
||||
if (t.indexOf(m) >= 0) {
|
||||
const [k, v] = t.split(m)
|
||||
keyValue.setData(k)
|
||||
valueValue.setData(v)
|
||||
mode = m
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onDestroy(valueValue.addCallbackAndRun(setTag))
|
||||
onDestroy(keyValue.addCallbackAndRun(setTag))
|
||||
|
||||
$: {
|
||||
setTag(mode)
|
||||
}
|
||||
|
||||
function setTag(_) {
|
||||
const k = keyValue.data
|
||||
const v = valueValue.data
|
||||
const t = k + mode + v
|
||||
try {
|
||||
TagUtils.Tag(t)
|
||||
tag.setData(t)
|
||||
} catch (e) {
|
||||
tag.setData(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="flex items-center">
|
||||
|
||||
<div class="flex h-fit ">
|
||||
|
||||
<ValidatedInput feedback={feedbackKey} placeholder="The key of the tag" type="key"
|
||||
value={keyValue}></ValidatedInput>
|
||||
<select bind:value={mode}>
|
||||
{#each modes as option}
|
||||
<option value={option}>
|
||||
{option}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<ValidatedInput feedback={feedbackValue} placeholder="The value of the tag" type="string"
|
||||
value={valueValue}></ValidatedInput>
|
||||
</div>
|
||||
|
||||
{#if $feedbackKey}
|
||||
<Tr cls="alert" t={$feedbackKey}/>
|
||||
{:else if $feedbackValue}
|
||||
<Tr cls="alert" t={$feedbackValue}/>
|
||||
{:else if $feedbackGlobal}
|
||||
<Tr cls="alert" t={$feedbackGlobal}/>
|
||||
{/if}
|
||||
<TagInfoStats {tag}/>
|
||||
</div>
|
||||
18
src/UI/Studio/TagInput/TagInput.svelte
Normal file
18
src/UI/Studio/TagInput/TagInput.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">/**
|
||||
* An element input a tag; has `and`, `or`, `regex`, ...
|
||||
*/
|
||||
import type {TagConfigJson} from "../../../Models/ThemeConfig/Json/TagConfigJson";
|
||||
import {UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import TagExpression from "../TagExpression.svelte";
|
||||
|
||||
|
||||
export let tag: UIEventSource<TagConfigJson>
|
||||
export let uploadableOnly: boolean
|
||||
export let overpassSupportNeeded: boolean
|
||||
</script>
|
||||
|
||||
<div class="m-4">
|
||||
<TagExpression {overpassSupportNeeded} {tag} {uploadableOnly}>
|
||||
<slot name="delete" slot="delete"/>
|
||||
</TagExpression>
|
||||
</div>
|
||||
24
src/UI/Studio/configMeta.ts
Normal file
24
src/UI/Studio/configMeta.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { JsonSchema, JsonSchemaType } from "./jsonSchema"
|
||||
|
||||
export interface ConfigMeta {
|
||||
path: string[]
|
||||
type: JsonSchemaType | JsonSchema[]
|
||||
hints: {
|
||||
group?: string
|
||||
typehint?: string
|
||||
/**
|
||||
* If multiple subcategories can be chosen
|
||||
*/
|
||||
types?: string
|
||||
question?: string
|
||||
iftrue?: string
|
||||
iffalse?: string
|
||||
ifunset?: string
|
||||
inline?: string
|
||||
default?: string
|
||||
typesdefault?: string
|
||||
suggestions?: []
|
||||
}
|
||||
required: boolean
|
||||
description: string
|
||||
}
|
||||
20
src/UI/Studio/jsonSchema.ts
Normal file
20
src/UI/Studio/jsonSchema.ts
Normal 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[]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue