Merge master

This commit is contained in:
Pieter Vander Vennet 2023-07-28 00:29:21 +02:00
commit 80168f5d0d
919 changed files with 95585 additions and 8504 deletions

View 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>

View 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()
}
}
}

View 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}

View 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}

View 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>

View 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>

View 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}

View 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}

View 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} />

View 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>

View 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}

View 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>

View 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>

View 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
}

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[]
}