Studio: WIP

This commit is contained in:
Pieter Vander Vennet 2023-08-23 11:11:53 +02:00
parent 04ecdad1bb
commit 903e168a89
62 changed files with 19152 additions and 123399 deletions

View file

@ -0,0 +1,20 @@
<script lang="ts">/**
* Input helper to create a tag. The tag is JSON-encoded
*/
import { UIEventSource } from "../../../Logic/UIEventSource";
import BasicTagInput from "../../Studio/TagInput/BasicTagInput.svelte";
export let value: UIEventSource<undefined | string>;
export let uploadableOnly: boolean;
export let overpassSupportNeeded: boolean;
/**
* Only show the taginfo-statistics if they are suspicious (thus: less then 250 entries)
*/
export let silent: boolean = false;
</script>
<BasicTagInput {overpassSupportNeeded} {silent} tag={value} {uploadableOnly} />

View file

@ -0,0 +1,33 @@
<script lang="ts">/**
* Input helper to create a tag. The tag is JSON-encoded
*/
import { UIEventSource } from "../../../Logic/UIEventSource";
import type { TagConfigJson } from "../../../Models/ThemeConfig/Json/TagConfigJson";
import FullTagInput from "../../Studio/TagInput/FullTagInput.svelte";
export let value: UIEventSource<undefined | string>;
export let uploadableOnly: boolean;
export let overpassSupportNeeded: boolean;
/**
* Only show the taginfo-statistics if they are suspicious (thus: less then 250 entries)
*/
export let silent: boolean = false;
let tag: UIEventSource<string | TagConfigJson> = value.sync(s => {
try {
return JSON.parse(s);
} catch (e) {
return s;
}
}, [], t => {
if(typeof t === "string"){
return t
}
return JSON.stringify(t);
});
</script>
<FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} />

View file

@ -15,6 +15,8 @@ import { Feature } from "geojson"
import { GeoOperations } from "../../Logic/GeoOperations"
import ImageHelper from "./Helpers/ImageHelper.svelte"
import TranslationInput from "./Helpers/TranslationInput.svelte"
import TagInput from "./Helpers/TagInput.svelte"
import SimpleTagInput from "./Helpers/SimpleTagInput.svelte"
export interface InputHelperProperties {
/**
@ -59,9 +61,11 @@ export default class InputHelpers {
wikidata: InputHelpers.constructWikidataHelper,
image: (value) => new SvelteUIElement(ImageHelper, { value }),
translation: (value) => new SvelteUIElement(TranslationInput, { value }),
tag: (value) => new SvelteUIElement(TagInput, { value }),
simple_tag: (value) => new SvelteUIElement(SimpleTagInput, { value }),
} as const
public static hideInputField : string[] = ["translation"]
public static hideInputField: string[] = ["translation", "simple_tag", "tag"]
/**
* Constructs a mapProperties-object for the given properties.

View file

@ -1,6 +1,6 @@
import BaseUIElement from "../BaseUIElement";
import { Translation } from "../i18n/Translation";
import Translations from "../i18n/Translations";
import BaseUIElement from "../BaseUIElement"
import { Translation } from "../i18n/Translation"
import Translations from "../i18n/Translations"
/**
* A 'TextFieldValidator' contains various methods to check and cleanup an entered value or to give feedback.
@ -16,13 +16,23 @@ export abstract class Validator {
/**
* What HTML-inputmode to use
*/
public readonly inputmode?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'
public readonly inputmode?:
| "none"
| "text"
| "tel"
| "url"
| "email"
| "numeric"
| "decimal"
| "search"
public readonly textArea: boolean
public readonly isMeta?: boolean
constructor(
name: string,
explanation: string | BaseUIElement,
inputmode?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search',
inputmode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search",
textArea?: false | boolean
) {
this.name = name

View file

@ -23,6 +23,8 @@ import ImageUrlValidator from "./Validators/ImageUrlValidator"
import TagKeyValidator from "./Validators/TagKeyValidator"
import TranslationValidator from "./Validators/TranslationValidator"
import FediverseValidator from "./Validators/FediverseValidator"
import IconValidator from "./Validators/IconValidator"
import TagValidator from "./Validators/TagValidator"
export type ValidatorType = (typeof Validators.availableTypes)[number]
@ -48,7 +50,9 @@ export default class Validators {
"simple_tag",
"key",
"translation",
"icon",
"fediverse",
"tag",
] as const
public static readonly AllValidators: ReadonlyArray<Validator> = [
@ -70,20 +74,15 @@ export default class Validators {
new ColorValidator(),
new ImageUrlValidator(),
new SimpleTagValidator(),
new TagValidator(),
new TagKeyValidator(),
new TranslationValidator(),
new IconValidator(),
new FediverseValidator(),
]
private static _byType = Validators._byTypeConstructor()
private static _byTypeConstructor(): Map<ValidatorType, Validator> {
const map = new Map<ValidatorType, Validator>()
for (const validator of Validators.AllValidators) {
map.set(<ValidatorType>validator.name, validator)
}
return map
}
public static HelpText(): BaseUIElement {
const explanations: BaseUIElement[] = Validators.AllValidators.map((type) =>
new Combine([new Title(type.name, 3), type.explanation]).SetClass("flex flex-col")
@ -95,6 +94,14 @@ export default class Validators {
]).SetClass("flex flex-col")
}
private static _byTypeConstructor(): Map<ValidatorType, Validator> {
const map = new Map<ValidatorType, Validator>()
for (const validator of Validators.AllValidators) {
map.set(<ValidatorType>validator.name, validator)
}
return map
}
static get(type: ValidatorType): Validator {
return Validators._byType.get(type)
}

View file

@ -0,0 +1,46 @@
import { Validator } from "../Validator"
import { Translation } from "../../i18n/Translation"
import licenses from "../../../assets/generated/license_info.json"
import { Utils } from "../../../Utils"
export default class IconValidator extends Validator {
private static allLicenses = new Set(licenses.map((l) => l.path))
private static allLicensesArr = Array.from(IconValidator.allLicenses)
public static readonly isMeta = true
constructor() {
super("icon", "Makes sure that a valid .svg-path is added")
}
getFeedback(s: string, getCountry, sloppy?: boolean): Translation | undefined {
if (!s.startsWith("http")) {
if (!IconValidator.allLicenses.has(s)) {
const close = sloppy
? []
: Utils.sortedByLevenshteinDistance(
s.substring(s.lastIndexOf("/")),
IconValidator.allLicensesArr,
(s) => s.substring(s.lastIndexOf("/"))
).slice(0, 5)
return new Translation(
[
`Unkown builtin icon ${s}, perhaps you meant one of: <ul>`,
...close.map(
(item) =>
`<li><span class="flex justify-start"> <img src='${item}' class="w-6 h-6"/>${item}</span></li>`
),
"</ul>",
].join("")
)
}
}
if (!s.endsWith(".svg")) {
return new Translation("An icon should end with `.svg`")
}
return undefined
}
isValid(key: string, getCountry?: () => string): boolean {
return this.getFeedback(key, getCountry, true) === undefined
}
}

View file

@ -3,6 +3,7 @@ import { Translation } from "../../i18n/Translation"
export default class ImageUrlValidator extends UrlValidator {
private static readonly allowedExtensions = ["jpg", "jpeg", "svg", "png"]
public readonly isMeta = true
constructor() {
super(

View file

@ -8,6 +8,8 @@ import TagKeyValidator from "./TagKeyValidator"
*/
export default class SimpleTagValidator extends Validator {
private static readonly KeyValidator = new TagKeyValidator()
public readonly isMeta = true
constructor() {
super(
"simple_tag",

View file

@ -3,6 +3,8 @@ import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
export default class TagKeyValidator extends Validator {
public readonly isMeta = true
constructor() {
super("key", "Validates a key, mostly that no weird characters are used")
}

View file

@ -0,0 +1,24 @@
import { Validator } from "../Validator"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import TagKeyValidator from "./TagKeyValidator"
import SimpleTagValidator from "./SimpleTagValidator"
/**
* Checks that the input conforms a JSON-encoded tag expression or a simpleTag`key=value`,
*/
export default class TagValidator extends Validator {
public readonly isMeta = true
constructor() {
super("tag", "A simple tag of the format `key=value` OR a tagExpression")
}
getFeedback(tag: string, _): Translation | undefined {
return undefined
}
isValid(tag: string, _): boolean {
return this.getFeedback(tag, _) === undefined
}
}

View file

@ -1,6 +1,8 @@
import { Validator } from "../Validator"
export default class TranslationValidator extends Validator {
public readonly isMeta = true
constructor() {
super("translation", "Makes sure the the string is of format `Record<string, string>` ")
}

View file

@ -29,7 +29,7 @@
})
)
let htmlElem: HTMLBaseElement
let htmlElem: HTMLDivElement
$: {
if (editMode && htmlElem !== undefined) {
// EditMode switched to true, so the person wants to make a change
@ -37,7 +37,7 @@
// Some delay is applied to give Svelte the time to render the _question_
window.setTimeout(() => {
Utils.scrollIntoView(htmlElem)
Utils.scrollIntoView(<any> htmlElem)
}, 50)
}
}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import EditLayerState from "./EditLayerState";
import layerSchemaRaw from "../../assets/layerconfigmeta.json"
import layerSchemaRaw from "../../assets/schemas/layerconfigmeta.json"
import Region from "./Region.svelte";
import TabbedGroup from "../Base/TabbedGroup.svelte";
import {UIEventSource} from "../../Logic/UIEventSource";
@ -10,7 +10,7 @@
import drinking_water from "../../../assets/layers/drinking_water/drinking_water.json"
const layerSchema: ConfigMeta[] = layerSchemaRaw
const layerSchema: ConfigMeta[] = <any> layerSchemaRaw
let state = new EditLayerState(layerSchema)
state.configuration.setData(drinking_water)
/**
@ -40,14 +40,13 @@
<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}-->
{/each}
</div>
<div slot="title1">Information panel (questions and answers)</div>
@ -57,9 +56,8 @@
The bulk of the popup content
</div>
</Region>
<!--
<Region {state} configs={perRegion["title"]} title="Popup title"/>
<Region {state} configs={perRegion["editing"]} title="Other editing elements"/>-->
<Region {state} configs={perRegion["editing"]} title="Other editing elements"/>
</div>
<div slot="title2">Rendering on the map</div>

View file

@ -2,8 +2,6 @@ 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
@ -40,6 +38,15 @@ export default class EditLayerState {
return entry
}
public getStoreFor(path: ReadonlyArray<string | number>): UIEventSource<any | undefined> {
const store = new UIEventSource<any>(this.getCurrentValueFor(path))
store.addCallback((v) => {
console.log("UPdating store", path, v)
this.setValueAt(path, v)
})
return store
}
public register(
path: ReadonlyArray<string | number>,
value: Store<any>,

View file

@ -0,0 +1,71 @@
<script lang="ts">
import type { MappingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
import EditLayerState from "./EditLayerState";
import { Translation } from "../i18n/Translation";
import { UIEventSource } from "../../Logic/UIEventSource";
import type { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson";
import { TagUtils } from "../../Logic/Tags/TagUtils";
import FromHtml from "../Base/FromHtml.svelte";
import { PencilIcon } from "@rgossiaux/svelte-heroicons/outline";
import Region from "./Region.svelte";
import type { ConfigMeta } from "./configMeta";
import configs from "../../assets/schemas/questionabletagrenderingconfigmeta.json";
import { Utils } from "../../Utils";
export let mapping: MappingConfigJson;
export let state: EditLayerState;
export let path: (string | number)[];
let tag: UIEventSource<TagConfigJson> = state.getStoreFor([...path, "if"]);
let parsedTag = tag.map(t => t ? TagUtils.Tag(t) : undefined);
let exampleTags = parsedTag.map(pt => {
if (!pt) {
return {};
}
const keys = pt.usedKeys();
const o = {};
for (const key of keys) {
o[key] = "value";
}
return o;
});
let uploadableOnly: boolean = true;
let thenStringified = state.getStoreFor([...path, "then"]).sync(t => t ? JSON.stringify(t) : undefined, [], s => s ? JSON.parse(s) : undefined);
let thenParsed = thenStringified.map(s => s ? JSON.parse(s) : s);
let editMode = Object.keys(thenParsed.data).length === 0;
const mappingConfigs: ConfigMeta[] = configs.filter(c => c.path[0] === "mappings")
.map(c => <ConfigMeta>Utils.Clone(c))
.map(c => {
c.path.splice(0, 1);
return c;
})
.filter(c => c.path.length == 1 && c.hints.group !== "hidden");
</script>
<button on:click={() => {editMode = !editMode}}>
<PencilIcon class="w-6 h-6" />
</button>
{#if editMode}
<div class="flex justify-between items-start w-full">
<div class="flex flex-col w-full">
<Region {state} configs={mappingConfigs} path={path} />
</div>
<slot name="delete" />
</div>
{:else}
<div>
{#if Object.keys($thenParsed).length > 0}
<b>
{new Translation($thenParsed).txt}
</b>
{:else}
<i>No then is set</i>
{/if}
<FromHtml src={ $parsedTag?.asHumanString(false, false, $exampleTags)} />
</div>
{/if}

View file

@ -2,28 +2,32 @@
* 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 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
export let state: EditLayerState;
export let configs: ConfigMeta[];
export let title: string | undefined = undefined;
export let path: (string | number)[] = [];
</script>
{#if title}
<div class="w-full flex flex-col">
<h3>{title}</h3>
<div class="pl-2 border border-black flex flex-col gap-y-1">
<slot name="description"/>
{#each configs as config}
<SchemaBasedInput {state} path={config.path} schema={config}/>
{/each}
<div class="pl-2 border border-black flex flex-col gap-y-1 w-full">
<slot name="description" />
{#each configs as config}
<SchemaBasedInput {state} path={config.path} schema={config} />
{/each}
</div>
</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>
<div class="pl-2 flex flex-col gap-y-1 w-full">
{#each configs as config}
<SchemaBasedInput {state} path={path.concat(config.path)} schema={config} />
{/each}
</div>
{/if}

View file

@ -2,7 +2,7 @@
import EditLayerState from "./EditLayerState";
import {UIEventSource} from "../../Logic/UIEventSource";
import type {TagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson";
import TagInput from "./TagInput/TagInput.svelte";
import FullTagInput from "./TagInput/FullTagInput.svelte";
import type {ConfigMeta} from "./configMeta";
import {PencilAltIcon} from "@rgossiaux/svelte-heroicons/solid";
import { onDestroy } from "svelte";
@ -40,13 +40,14 @@
<div class="interactive border-interactive">
<h3>{schema.hints.question ?? "What tags should be applied?"}</h3>
{schema.description}
<TagInput {tag}/>
<FullTagInput {tag}/>
<div class="flex justify-end">
<button class="primary w-fit" on:click={() => {mode = "set"}}>
Save
</button>
</div>
<div class="subtle">RegisteredTagInput based on schema: {JSON.stringify(schema)}</div>
</div>
{:else}
<div class="low-interaction flex justify-between">

View file

@ -10,16 +10,18 @@
export let state: EditLayerState;
export let schema: ConfigMeta;
let title = schema.path.at(-1);
let singular = title;
if (title.endsWith("s")) {
if (title?.endsWith("s")) {
singular = title.slice(0, title.length - 1);
}
let article = "a";
if (singular.match(/^[aeoui]/)) {
if (singular?.match(/^[aeoui]/)) {
article = "an";
}
export let path: (string | number)[] = [];
const isTagRenderingBlock = path.length === 1 && path[0] === "tagRenderings";
const subparts = state.getSchemaStartingWith(schema.path);
@ -39,8 +41,11 @@
let createdItems = values.data.length;
function createItem() {
function createItem(valueToSet?: any) {
values.data.push(createdItems);
if (valueToSet) {
state.setValueAt([...path, createdItems], valueToSet);
}
createdItems++;
values.ping();
}
@ -87,16 +92,24 @@
{/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={() => {del(value)}}>
<TrashIcon class="w-4 h-4" />
</button>
</div>
{#if !isTagRenderingBlock}
<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={() => {del(value)}}>
<TrashIcon class="w-4 h-4" />
</button>
</div>
{/if}
<div class="border border-black">
{#if path.length === 1 && path[0] === "tagRenderings"}
<TagRenderingInput path={path.concat(value)} {state} {schema}/>
{#if isTagRenderingBlock}
<TagRenderingInput path={path.concat(value)} {state} {schema} >
<button slot="upper-right" class="border-black border rounded-full p-1 w-fit h-fit"
on:click={() => {del(value)}}>
<TrashIcon class="w-4 h-4" />
</button>
</TagRenderingInput>
{:else}
{#each subparts as subpart}
<SchemaBasedInput {state} path={fusePath(value, subpart.path)} schema={subpart} />
@ -105,5 +118,11 @@
</div>
{/each}
{/if}
<button on:click={createItem}>Add {article} {singular}</button>
<div class="flex">
<button on:click={() => createItem()}>Add {article} {singular}</button>
{#if path.length === 1 && path[0] === "tagRenderings"}
<button on:click={() => {createItem();}}>Add a builtin tagRendering</button>
{/if}
<slot name="extra-button" />
</div>
</div>

View file

@ -10,6 +10,7 @@
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
import EditLayerState from "./EditLayerState";
import { onDestroy } from "svelte";
import type { JsonSchemaType } from "./jsonSchema";
export let state: EditLayerState
@ -21,6 +22,9 @@
if(type === "rendered"){
type = "translation"
}
if(type.endsWith("[]")){
type = type.substring(0, type.length - 2)
}
const isTranslation =schema.hints.typehint === "translation" || schema.hints.typehint === "rendered"
const configJson: QuestionableTagRenderingConfigJson = {
@ -47,7 +51,23 @@
}]
}
if (schema.type === "boolean" || (Array.isArray(schema.type) && schema.type.some(t => t["type"] === "boolean"))) {
function mightBeBoolean(type: undefined | JsonSchemaType): boolean {
if(type === undefined){
return false
}
if(type["type"]){
type = type["type"]
}
if(type === "boolean"){
return true
}
if(!Array.isArray(type)){
return false
}
return type.some(t => mightBeBoolean(t) )
}
if (mightBeBoolean(schema.type)) {
configJson.mappings = configJson.mappings ?? []
configJson.mappings.push(
{
@ -101,7 +121,7 @@
{#if err !== undefined}
<span class="alert">{err}</span>
{:else}
<div class="w-full">
<div class="w-full flex flex-col">
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags}/>
</div>
{/if}

View file

@ -1,31 +1,25 @@
<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)[] = []
console.log("Constructing", path,"with schema", schema)
import type { ConfigMeta } from "./configMeta";
import SchemaBasedField from "./SchemaBasedField.svelte";
import EditLayerState from "./EditLayerState";
import SchemaBasedArray from "./SchemaBasedArray.svelte";
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte";
import SchemaBasedTranslationInput from "./SchemaBasedTranslationInput.svelte";
export let schema: ConfigMeta;
export let state: EditLayerState;
export let path: (string | number)[] = [];
</script>
{#if schema.hints.typehint === "tagrendering[]"}
<!-- We cheat a bit here by matching this 'magical' type... -->
<SchemaBasedArray {path} {state} {schema}/>
<!-- We cheat a bit here by matching this 'magical' type... -->
<SchemaBasedArray {path} {state} {schema} />
{:else if schema.type === "array"}
<SchemaBasedArray {path} {state} {schema}/>
{:else if schema.hints.typehint === "tag"}
<RegisteredTagInput {state} {path} {schema}/>
<SchemaBasedArray {path} {state} {schema} />
{:else if schema.type === "translation"}
<SchemaBasedTranslationInput {path} {state} {schema}/>
<SchemaBasedTranslationInput {path} {state} {schema} />
{:else if schema.hints.types}
<SchemaBaseMultiType {path} {state} {schema}/>
<SchemaBasedMultiType {path} {state} {schema} />
{:else}
<SchemaBasedField {path} {state} {schema}/>
<SchemaBasedField {path} {state} {schema} />
{/if}

View file

@ -26,7 +26,6 @@
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);
}

View file

@ -2,118 +2,121 @@
* Allows to create `and` and `or` expressions graphically
*/
import {UIEventSource} from "../../Logic/UIEventSource";
import type {TagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson";
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";
import FullTagInput from "./TagInput/FullTagInput.svelte";
import { TrashIcon } from "@babeard/svelte-heroicons/mini";
export let tag: UIEventSource<TagConfigJson>
let mode: "and" | "or" = "and"
export let tag: UIEventSource<TagConfigJson>;
let mode: "and" | "or" = "and";
let basicTags: UIEventSource<UIEventSource<string>[]> = new UIEventSource([])
let basicTags: UIEventSource<UIEventSource<string>[]> = new UIEventSource([]);
/**
* Sub-expressions
*/
let expressions: UIEventSource<UIEventSource<TagConfigJson>[]> = new UIEventSource([])
export let uploadableOnly: boolean
export let overpassSupportNeeded: boolean
let expressions: UIEventSource<UIEventSource<TagConfigJson>[]> = new UIEventSource([]);
export let uploadableOnly: boolean;
export let overpassSupportNeeded: boolean;
export let silent: boolean;
function update(_) {
let config: TagConfigJson = <any>{}
let config: TagConfigJson = <any>{};
if (!mode) {
return
return;
}
const tags = []
const tags = [];
const subpartSources = (<UIEventSource<string | TagConfigJson>[]>basicTags.data).concat(expressions.data)
const subpartSources = (<UIEventSource<string | TagConfigJson>[]>basicTags.data).concat(expressions.data);
for (const src of subpartSources) {
const t = src.data
const t = src.data;
if (!t) {
// We indicate upstream that this value is invalid
tag.setData(undefined)
return
tag.setData(undefined);
return;
}
tags.push(t)
tags.push(t);
}
if (tags.length === 1) {
tag.setData(tags[0])
tag.setData(tags[0]);
} else {
config[mode] = tags
tag.setData(config)
config[mode] = tags;
tag.setData(config);
}
}
function addBasicTag(value?: string) {
const src = new UIEventSource(value)
const src = new UIEventSource(value);
basicTags.data.push(src);
basicTags.ping()
src.addCallbackAndRunD(_ => update(_))
basicTags.ping();
src.addCallbackAndRunD(_ => update(_));
}
function removeTag(basicTag: UIEventSource<any>) {
const index = basicTags.data.indexOf(basicTag)
console.log("Removing", index, basicTag)
const index = basicTags.data.indexOf(basicTag);
console.log("Removing", index, basicTag);
if (index >= 0) {
basicTag.setData(undefined)
basicTags.data.splice(index, 1)
basicTags.ping()
basicTag.setData(undefined);
basicTags.data.splice(index, 1);
basicTags.ping();
}
}
function removeExpression(expr: UIEventSource<any>) {
const index = expressions.data.indexOf(expr)
const index = expressions.data.indexOf(expr);
if (index >= 0) {
expr.setData(undefined)
expressions.data.splice(index, 1)
expressions.ping()
expr.setData(undefined);
expressions.data.splice(index, 1);
expressions.ping();
}
}
function addExpression(expr?: TagConfigJson) {
const src = new UIEventSource(expr)
const src = new UIEventSource(expr);
expressions.data.push(src);
expressions.ping()
src.addCallbackAndRunD(_ => update(_))
expressions.ping();
src.addCallbackAndRunD(_ => update(_));
}
$: update(mode)
expressions.addCallback(_ => update(_))
basicTags.addCallback(_ => update(_))
$: update(mode);
expressions.addCallback(_ => update(_));
basicTags.addCallback(_ => update(_));
let initialTag: TagConfigJson = tag.data
let initialTag: TagConfigJson = tag.data;
function initWith(initialTag: TagConfigJson) {
if (typeof initialTag === "string") {
addBasicTag(initialTag)
return
addBasicTag(initialTag);
return;
}
mode = <"or" | "and">Object.keys(initialTag)[0]
const subExprs = (<TagConfigJson[]>initialTag[mode])
if (subExprs.length == 0) {
return
mode = <"or" | "and">Object.keys(initialTag)[0];
const subExprs = (<TagConfigJson[]>initialTag[mode]);
if (!subExprs || subExprs.length == 0) {
return;
}
if (subExprs.length == 1) {
initWith(subExprs[0])
initWith(subExprs[0]);
return;
}
for (const subExpr of subExprs) {
if (typeof subExpr === "string") {
addBasicTag(subExpr)
addBasicTag(subExpr);
} else {
addExpression(subExpr)
addExpression(subExpr);
}
}
}
if (!initialTag) {
addBasicTag()
addBasicTag();
} else {
initWith(initialTag)
initWith(initialTag);
}
@ -122,37 +125,44 @@ if (!initialTag) {
<div class="flex items-center">
<select bind:value={mode}>
<option value="and">and</option>
{#if !uploadableOnly}
{#if !uploadableOnly}
<select bind:value={mode}>
<option value="and">and</option>
<option value="or">or</option>
{/if}
</select>
</select>
{/if}
<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>
<BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} />
{#if $basicTags.length + $expressions.length > 1}
<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>
{/if}
</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"/>
<FullTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={expression}>
<button class="small" slot="delete" on:click={() => removeExpression(expression)}>
<TrashIcon class="w-3 h-3 p-0" />
Delete subexpression
</button>
</TagInput>
</FullTagInput>
{/each}
<div class="flex">
<button class="w-fit" on:click={() => addBasicTag()}>
<button class="w-fit small" on:click={() => addBasicTag()}>
Add a tag
</button>
<button class="w-fit" on:click={() => addExpression()}>
Add an expression
</button>
<slot name="delete"/>
{#if !uploadableOnly}
<!-- Do not allow to add an expression, as everything is 'and' anyway -->
<button class="w-fit small" on:click={() => addExpression()}>
Add an expression
</button>
{/if}
<slot name="delete" />
</div>
</div>

View file

@ -2,58 +2,61 @@
* 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 { 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 { twMerge } from "tailwind-merge";
import Loading from "../Base/Loading.svelte";
export let tag: UIEventSource<string>
const tagStabilized = tag.stabilized(500)
export let silent = false;
export let tag: UIEventSource<string>;
const tagStabilized = tag.stabilized(500);
const tagInfoStats: Store<TagInfoStats> = tagStabilized.bind(tag => {
if (!tag) {
return undefined
return undefined;
}
try {
const t = TagUtils.Tag(tag)
const k = t["key"]
let v = t["value"]
const t = TagUtils.Tag(tag);
const k = t["key"];
let v = t["value"];
if (typeof v !== "string") {
v = undefined
v = undefined;
}
if (!k) {
return undefined
return undefined;
}
return UIEventSource.FromPromise(TagInfo.global.getStats(k, v))
return UIEventSource.FromPromise(TagInfo.global.getStats(k, v));
} catch (e) {
return undefined
return undefined;
}
})
});
const tagInfoUrl: Store<string> = tagStabilized.mapD(tag => {
try {
const t = TagUtils.Tag(tag)
const k = t["key"]
let v = t["value"]
const t = TagUtils.Tag(tag);
const k = t["key"];
let v = t["value"];
if (typeof v !== "string") {
v = undefined
v = undefined;
}
if (!k) {
return undefined
return undefined;
}
return TagInfo.global.webUrl(k, v)
return TagInfo.global.webUrl(k, v);
} catch (e) {
return undefined
return undefined;
}
})
const total = tagInfoStats.mapD(data => data.data.find(i => i.type === "all").count)
});
const total = tagInfoStats.mapD(data => data.data.find(i => i.type === "all").count);
</script>
{#if $tagStabilized !== $tag}
<Loading/>
{:else if $tagInfoStats }
{#if !silent}
<Loading />
{/if}
{:else if $tagInfoStats && (!silent || $total < 250) }
<a href={$tagInfoUrl} target="_blank" class={twMerge(($total < 250) ? "alert" : "thanks", "w-fit link-underline")}>
{$total} features on OSM have this tag
</a>

View file

@ -6,11 +6,14 @@
import Tr from "../../Base/Tr.svelte";
import {TagUtils} from "../../../Logic/Tags/TagUtils";
import TagInfoStats from "../TagInfoStats.svelte";
import { Translation } from "../../i18n/Translation";
export let tag: UIEventSource<string> = new UIEventSource<string>(undefined)
export let uploadableOnly: boolean
export let overpassSupportNeeded: boolean
export let silent : boolean = false
let feedbackGlobal = tag.map(tag => {
if (!tag) {
return undefined
@ -24,11 +27,11 @@
})
let feedbackKey = new UIEventSource<string>(undefined)
let feedbackKey = new UIEventSource<Translation>(undefined)
let keyValue = new UIEventSource<string>(undefined)
let feedbackValue = new UIEventSource<string>(undefined)
let feedbackValue = new UIEventSource<Translation>(undefined)
/**
* The value of the tag. The name is a bit confusing
*/
@ -79,7 +82,11 @@
function setTag(_) {
const k = keyValue.data
const v = valueValue.data
const v = valueValue.data ?? ""
if(k === undefined || k === ""){
tag.setData(undefined)
return
}
const t = k + mode + v
try {
TagUtils.Tag(t)
@ -116,5 +123,5 @@
{:else if $feedbackGlobal}
<Tr cls="alert" t={$feedbackGlobal}/>
{/if}
<TagInfoStats {tag}/>
<TagInfoStats {silent} {tag}/>
</div>

View file

@ -0,0 +1,19 @@
<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<string | TagConfigJson>
export let uploadableOnly: boolean
export let overpassSupportNeeded: boolean
export let silent: boolean
</script>
<div class="m-2">
<TagExpression {silent} {overpassSupportNeeded} {tag} {uploadableOnly}>
<slot name="delete" slot="delete"/>
</TagExpression>
</div>

View file

@ -1,18 +0,0 @@
<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,14 @@
<script lang="ts">
import SchemaBasedInput from "./SchemaBasedInput.svelte";
import EditLayerState from "./EditLayerState";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
export let state: EditLayerState
export let path : (number | string)[]
let schema : TagRenderingConfig
</script>
XYZ

View file

@ -12,6 +12,11 @@ import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
import { UIEventSource } from "../../Logic/UIEventSource";
import * as questions from "../../assets/generated/layers/questions.json";
import MappingInput from "./MappingInput.svelte";
import { TrashIcon } from "@rgossiaux/svelte-heroicons/outline";
import questionableTagRenderingSchemaRaw from "../../assets/schemas/questionabletagrenderingconfigmeta.json";
import SchemaBasedField from "./SchemaBasedField.svelte";
import Region from "./Region.svelte";
export let state: EditLayerState;
export let schema: ConfigMeta;
@ -19,11 +24,11 @@ export let path: (string | number)[];
let value = state.getCurrentValueFor(path);
let mappings: MappingConfigJson[] = [];
let mappingsBuiltin: MappingConfigJson[] = [];
for (const tr of questions.tagRenderings) {
let description = tr["description"] ?? tr["question"] ?? "No description available";
description = description["en"] ?? description;
mappings.push({
mappingsBuiltin.push({
if: "value=" + tr["id"],
then: {
"en": "Builtin <b>" + tr["id"] + "</b> <div class='subtle'>" + description + "</div>"
@ -34,28 +39,77 @@ for (const tr of questions.tagRenderings) {
const configBuiltin = new TagRenderingConfig(<QuestionableTagRenderingConfigJson>{
question: "Which builtin element should be shown?",
mappings
mappings: mappingsBuiltin
});
const configOverride = <QuestionableTagRenderingConfigJson>{
render: "This is a builtin question which changes some properties. Editing those is not possible within MapComplete Studio"
};
const tags = new UIEventSource({ value });
tags.addCallbackAndRunD(tgs => {
state.setValueAt(path, tgs["value"]);
});
let mappings: UIEventSource<MappingConfigJson[]> = state.getStoreFor([...path, "mappings"]);
const topLevelItems: Record<string, ConfigMeta> = {};
for (const item of questionableTagRenderingSchemaRaw) {
if (item.path.length === 1) {
topLevelItems[item.path[0]] = <ConfigMeta>item;
}
}
function initMappings() {
if (mappings.data === undefined) {
mappings.setData([]);
}
}
const freeformSchema = <ConfigMeta[]> questionableTagRenderingSchemaRaw.filter(schema => schema.path.length >= 1 && schema.path[0] === "freeform");
console.log("FreeformSchema:", freeformSchema)
</script>
{#if typeof value === "string"}
<TagRenderingEditable config={configBuiltin} selectedElement={undefined} showQuestionIfUnknown={true} {state}
{tags} />
{:else}
<div>
TR{JSON.stringify(state.getCurrentValueFor(path))}
<div class="flex low-interaction">
<TagRenderingEditable config={configBuiltin} selectedElement={undefined} showQuestionIfUnknown={true} {state}
{tags} />
<slot name="upper-right" />
</div>
{:else}
<div class="flex flex-col w-full p-1 gap-y-1">
<div class="flex justify-end">
<slot name="upper-right" />
</div>
<SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]}></SchemaBasedField>
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]}></SchemaBasedField>
{#each ($mappings ?? []) as mapping, i (mapping)}
<div class="flex interactive w-full">
<MappingInput {mapping} {state} path={path.concat(["mappings", i])}>
<button slot="delete" class="rounded-full no-image-background" on:click={() => {
initMappings();
mappings.data.splice(i, 1)
mappings.ping()
}}>
<TrashIcon class="w-4 h-4" />
</button>
</MappingInput>
</div>
{/each}
<button class="small primary"
on:click={() =>{ initMappings(); mappings.data.push({if: undefined, then: {}}); mappings.ping()} }>
Add a mapping
</button>
<Region {state} {path} configs={freeformSchema}/>
<!-- {JSON.stringify(state.getCurrentValueFor(path))} <!-->
</div>
<!--
<Region configs={freeformSchema} {state} path={[...path, "freeform"]} /> -->
{/if}

View file

@ -38,6 +38,11 @@
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
Main action (disabled)
</button>
<button class="small">
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
Small button
</button>
</div>
<div class="flex">
<button>
@ -91,6 +96,10 @@
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
Main action (disabled)
</button>
<button class="small">
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
Small button
</button>
</div>
<div class="flex">
<button>

View file

@ -57,7 +57,7 @@ export class Translation extends BaseUIElement {
if (count === 0) {
console.error(
"Constructing a translation, but the object containing translations is empty " +
context
(context ?? "No context given")
)
throw `Constructing a translation, but the object containing translations is empty (${context})`
}