Studio: add slideshow, add useability tweaks

This commit is contained in:
Pieter Vander Vennet 2023-10-24 22:01:10 +02:00
parent 2df9aa8564
commit 8bc555fbe0
26 changed files with 440 additions and 316 deletions

View file

@ -28,6 +28,18 @@ async function prepareFile(url: string): Promise<string> {
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
return fs.readFileSync(filePath, "utf8") return fs.readFileSync(filePath, "utf8")
} }
while (url.startsWith("/")) {
url = url.slice(1)
}
const sliced = url.split("/").slice(1)
if (!sliced) {
return
}
const backupFile = path.join(STATIC_PATH, ...sliced)
console.log("Using bakcup path", backupFile)
if (fs.existsSync(backupFile)) {
return fs.readFileSync(backupFile, "utf8")
}
return null return null
} }
@ -51,7 +63,9 @@ http.createServer(async (req, res) => {
for (let i = 1; i < paths.length; i++) { for (let i = 1; i < paths.length; i++) {
const p = paths.slice(0, i) const p = paths.slice(0, i)
const dir = STATIC_PATH + p.join("/") const dir = STATIC_PATH + p.join("/")
console.log("Checking if", dir, "exists...")
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
console.log("Creating new directory", dir)
fs.mkdirSync(dir) fs.mkdirSync(dir)
} }
} }
@ -61,22 +75,28 @@ http.createServer(async (req, res) => {
res.end() res.end()
return return
} }
if (req.url.endsWith("/overview")) {
const url = new URL(`http://127.0.0.1/` + req.url)
if (url.pathname.endsWith("overview")) {
console.log("Giving overview") console.log("Giving overview")
let userId = url.searchParams.get("userId")
const allFiles = ScriptUtils.readDirRecSync(STATIC_PATH) const allFiles = ScriptUtils.readDirRecSync(STATIC_PATH)
.filter((p) => p.endsWith(".json") && !p.endsWith("license_info.json")) .filter(
(p) =>
p.endsWith(".json") &&
!p.endsWith("license_info.json") &&
(p.startsWith("layers") ||
p.startsWith("themes") ||
userId !== undefined ||
p.startsWith(userId))
)
.map((p) => p.substring(STATIC_PATH.length + 1)) .map((p) => p.substring(STATIC_PATH.length + 1))
res.writeHead(200, { "Content-Type": MIME_TYPES.json }) res.writeHead(200, { "Content-Type": MIME_TYPES.json })
res.write(JSON.stringify({ allFiles })) res.write(JSON.stringify({ allFiles }))
res.end() res.end()
return return
} }
if (!fs.existsSync(STATIC_PATH + req.url)) {
res.writeHead(404, { "Content-Type": MIME_TYPES.html })
res.write("<html><body><p>Not found...</p></body></html>")
res.end()
return
}
const file = await prepareFile(req.url) const file = await prepareFile(req.url)
if (file === null) { if (file === null) {
res.writeHead(404, { "Content-Type": MIME_TYPES.html }) res.writeHead(404, { "Content-Type": MIME_TYPES.html })

View file

@ -435,7 +435,6 @@ class MappedStore<TIn, T> extends Store<T> {
* const mapped = src.map(i => i * 2) * const mapped = src.map(i => i * 2)
* src.setData(3) * src.setData(3)
* mapped.data // => 6 * mapped.data // => 6
*
*/ */
get data(): T { get data(): T {
if (!this._callbacksAreRegistered) { if (!this._callbacksAreRegistered) {

View file

@ -666,25 +666,29 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
} }
if (json.freeform) { if (json.freeform) {
const c = context.enters("freeform", "render")
if (json.render === undefined) { if (json.render === undefined) {
c.err( context
"This tagRendering allows to set a freeform, but does not define a way to `render` this value" .enter("render")
.err(
"This tagRendering allows to set a value to key " +
json.freeform.key +
", but does not define a `render`. Please, add a value here which contains `{" +
json.freeform.key +
"}`"
) )
} else { } else {
const render = new Translation(<any>json.render) const render = new Translation(<any>json.render)
for (const ln in render.translations) { for (const ln in render.translations) {
if (ln.startsWith("_")) { if (ln.startsWith("_")) {
continue continue
} }
const txt: string = render.translations[ln] const txt: string = render.translations[ln]
if (txt === "") { if (txt === "") {
c.err(" Rendering for language " + ln + " is empty") context.enter("render").err(" Rendering for language " + ln + " is empty")
} }
if ( if (
txt.indexOf("{" + json.freeform.key + "}") >= 0 || txt.indexOf("{" + json.freeform.key + "}") >= 0 ||
txt.indexOf("&LBRACE" + json.freeform.key + "&RBRACE") txt.indexOf("&LBRACE" + json.freeform.key + "&RBRACE") >= 0
) { ) {
continue continue
} }
@ -721,8 +725,10 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
) { ) {
continue continue
} }
c.err( context
`The rendering for language ${ln} does not contain the freeform key {${json.freeform.key}}. This is a bug, as this rendering should show exactly this freeform key!\nThe rendering is ${txt} ` .enter("render")
.err(
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. This is a bug, as this rendering should show exactly this freeform key!`
) )
} }
} }
@ -783,7 +789,7 @@ export class ValidateLayer extends Conversion<
private readonly _path?: string private readonly _path?: string
private readonly _isBuiltin: boolean private readonly _isBuiltin: boolean
private readonly _doesImageExist: DoesImageExist private readonly _doesImageExist: DoesImageExist
private _studioValidations: boolean private readonly _studioValidations: boolean
constructor( constructor(
path: string, path: string,
@ -816,7 +822,7 @@ export class ValidateLayer extends Conversion<
} }
if (json.id === undefined) { if (json.id === undefined) {
context.err(`Not a valid layer: id is undefined: ${JSON.stringify(json)}`) context.enter("id").err(`Not a valid layer: id is undefined`)
} }
if (json.source === undefined) { if (json.source === undefined) {
@ -922,6 +928,21 @@ export class ValidateLayer extends Conversion<
"Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set." "Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set."
) )
} }
{
// Check for multiple, identical builtin questions - usability for studio users
const duplicates = Utils.Duplicates(
<string[]>json.tagRenderings.filter((tr) => typeof tr === "string")
)
for (let i = 0; i < json.tagRenderings.length; i++) {
const tagRendering = json.tagRenderings[i]
if (typeof tagRendering === "string" && duplicates.indexOf(tagRendering) > 0) {
context
.enters("tagRenderings", i)
.err(`This builtin question is used multiple times (${tagRendering})`)
}
}
}
} }
if (json["builtin"] !== undefined) { if (json["builtin"] !== undefined) {

View file

@ -15,11 +15,18 @@ import { Translatable } from "./Translatable"
*/ */
export interface LayerConfigJson { export interface LayerConfigJson {
/** /**
* The id of this layer.
* This should be a simple, lowercase, human readable string that is used to identify the layer.
*
* group: Basic
* question: What is the identifier of this layer? * question: What is the identifier of this layer?
*
* This should be a simple, lowercase, human readable string that is used to identify the layer.
* A good ID is:
* - a noun
* - written in singular
* - describes the object
* - in english
* - only has lowercase letters, numbers or underscores. Do not use a space or a dash
*
* type: id
* group: Basic
*/ */
id: string id: string

View file

@ -26,8 +26,7 @@
} }
const apiState = state.osmConnection.apiIsOnline const apiState = state.osmConnection.apiIsOnline
</script> </script>
<slot />
<!--
{#if $badge} {#if $badge}
{#if !ignoreLoading && $loadingStatus === "loading"} {#if !ignoreLoading && $loadingStatus === "loading"}
<slot name="loading"> <slot name="loading">
@ -43,4 +42,4 @@
{:else if $loadingStatus === "not-attempted"} {:else if $loadingStatus === "not-attempted"}
<slot name="not-logged-in" /> <slot name="not-logged-in" />
{/if} {/if}
{/if} --> {/if}

View file

@ -22,7 +22,7 @@ $: documentation = TagUtils.modeDocumentation[mode];
</script> </script>
<BasicTagInput bind:mode={mode} {dropdownFocussed} {overpassSupportNeeded} {silent} tag={value} {uploadableOnly} /> <BasicTagInput bind:mode={mode} {dropdownFocussed} {overpassSupportNeeded} {silent} tag={value} {uploadableOnly} on:submit />
{#if $dropdownFocussed} {#if $dropdownFocussed}
<div class="border border-dashed border-black p-2 m-2"> <div class="border border-dashed border-black p-2 m-2">
<b>{documentation.name}</b> <b>{documentation.name}</b>

View file

@ -19,4 +19,4 @@ let tag: UIEventSource<string | TagConfigJson> = value
</script> </script>
<FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} /> <FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} on:submit/>

View file

@ -5,20 +5,14 @@
import { createEventDispatcher, onDestroy } from "svelte"; import { createEventDispatcher, onDestroy } from "svelte";
import ValidatedInput from "../ValidatedInput.svelte"; import ValidatedInput from "../ValidatedInput.svelte";
export let value: UIEventSource<string> = new UIEventSource<string>(""); export let value: UIEventSource<Record<string, string>> = new UIEventSource<Record<string, string>>({});
export let args: string[] = [] export let args: string[] = []
let prefix = args[0] let prefix = args[0] ?? ""
let postfix = args[1] let postfix = args[1] ?? ""
let translations: UIEventSource<Record<string, string>> = value.sync((s) => { let translations: UIEventSource<Record<string, string>> = value
try {
return JSON.parse(s);
} catch (e) {
return {};
}
}, [], v => JSON.stringify(v));
const allLanguages: string[] = LanguageUtils.usedLanguagesSorted; const allLanguages: string[] = LanguageUtils.usedLanguagesSorted;
let currentLang = new UIEventSource("en"); let currentLang = new UIEventSource("en");
@ -28,6 +22,9 @@
function update() { function update() {
const v = currentVal.data; const v = currentVal.data;
const l = currentLang.data; const l = currentLang.data;
if(translations.data === "" || translations.data === undefined){
translations.data = {}
}
if (translations.data[l] === v) { if (translations.data[l] === v) {
return; return;
} }
@ -37,6 +34,9 @@
onDestroy(currentLang.addCallbackAndRunD(currentLang => { onDestroy(currentLang.addCallbackAndRunD(currentLang => {
console.log("Applying current lang:", currentLang); console.log("Applying current lang:", currentLang);
if(!translations.data){
translations.data = {}
}
translations.data[currentLang] = translations.data[currentLang] ?? ""; translations.data[currentLang] = translations.data[currentLang] ?? "";
currentVal.setData(translations.data[currentLang]); currentVal.setData(translations.data[currentLang]);
})); }));

View file

@ -27,14 +27,13 @@
let properties = { feature, args: args ?? [] }; let properties = { feature, args: args ?? [] };
let dispatch = createEventDispatcher<{ let dispatch = createEventDispatcher<{
selected, selected
submit
}>(); }>();
</script> </script>
{#if type === "translation" } {#if type === "translation" }
<TranslationInput {value} on:submit={() => dispatch("submit")} {args} /> <TranslationInput {value} on:submit {args} />
{:else if type === "direction"} {:else if type === "direction"}
<DirectionInput {value} mapProperties={InputHelpers.constructMapProperties(properties)} /> <DirectionInput {value} mapProperties={InputHelpers.constructMapProperties(properties)} />
{:else if type === "date"} {:else if type === "date"}
@ -44,9 +43,9 @@
{:else if type === "image"} {:else if type === "image"}
<ImageHelper { value } /> <ImageHelper { value } />
{:else if type === "tag"} {:else if type === "tag"}
<TagInput { value } /> <TagInput { value } on:submit />
{:else if type === "simple_tag"} {:else if type === "simple_tag"}
<SimpleTagInput { value } {args} /> <SimpleTagInput { value } {args} on:submit />
{:else if type === "opening_hours"} {:else if type === "opening_hours"}
<OpeningHoursInput { value } /> <OpeningHoursInput { value } />
{:else if type === "wikidata"} {:else if type === "wikidata"}

View file

@ -109,7 +109,7 @@
* Dispatches the submit, but only if the value is valid * Dispatches the submit, but only if the value is valid
*/ */
function sendSubmit(){ function sendSubmit(){
if(feedback.data){ if(feedback?.data){
console.log("Not sending a submit as there is feedback") console.log("Not sending a submit as there is feedback")
} }
dispatch("submit") dispatch("submit")

View file

@ -259,7 +259,6 @@
value={freeformInput} value={freeformInput}
on:selected={() => (selectedMapping = config.mappings?.length)} on:selected={() => (selectedMapping = config.mappings?.length)}
on:submit={onSave} on:submit={onSave}
submit={onSave}
/> />
</label> </label>
{/if} {/if}

View file

@ -1336,6 +1336,7 @@ export default class SpecialVisualizations {
const tr = typeof v === "string" ? JSON.parse(v) : v const tr = typeof v === "string" ? JSON.parse(v) : v
return new Translation(tr).SetClass("font-bold") return new Translation(tr).SetClass("font-bold")
} catch (e) { } catch (e) {
console.error("Cannot create a translation for", v, "due to", e)
return JSON.stringify(v) return JSON.stringify(v)
} }
}) })

View file

@ -0,0 +1,28 @@
<script lang="ts">
import Marker from "../Map/Marker.svelte";
import NextButton from "../Base/NextButton.svelte";
import { createEventDispatcher } from "svelte";
import { AllSharedLayers } from "../../Customizations/AllSharedLayers";
export let layerIds : { id: string }[]
const dispatch = createEventDispatcher<{layerSelected: string}>()
function fetchIconDescription(layerId): any {
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
}
</script>
{#if layerIds.length > 0}
<slot name="title"/>
<div class="flex flex-wrap">
{#each Array.from(layerIds) as layer}
<NextButton clss="small" on:click={() => dispatch("layerSelected", layer.id)}>
<div class="w-4 h-4 mr-1">
<Marker icons={fetchIconDescription(layer.id)} />
</div>
{layer.id}
</NextButton>
{/each}
</div>
{/if}

View file

@ -11,6 +11,7 @@
import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion"; import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion";
import ErrorIndicatorForRegion from "./ErrorIndicatorForRegion.svelte"; import ErrorIndicatorForRegion from "./ErrorIndicatorForRegion.svelte";
import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid"; import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid";
import SchemaBasedInput from "./SchemaBasedInput.svelte";
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw; const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
@ -25,7 +26,6 @@
* Blacklist of regions for the general area tab * Blacklist of regions for the general area tab
* These are regions which are handled by a different tab * These are regions which are handled by a different tab
*/ */
const regionBlacklist = ["hidden", undefined, "infobox", "tagrenderings", "maprendering", "editing", "title", "linerendering", "pointrendering"];
const allNames = Utils.Dedup(layerSchema.map(meta => meta.hints.group)); const allNames = Utils.Dedup(layerSchema.map(meta => meta.hints.group));
const perRegion: Record<string, ConfigMeta[]> = {}; const perRegion: Record<string, ConfigMeta[]> = {};
@ -33,12 +33,7 @@
perRegion[region] = layerSchema.filter(meta => meta.hints.group === region); perRegion[region] = layerSchema.filter(meta => meta.hints.group === region);
} }
const baselayerRegions: string[] = ["Basic", "presets", "filters"];
for (const baselayerRegion of baselayerRegions) {
if (perRegion[baselayerRegion] === undefined) {
console.error("BaseLayerRegions in editLayer: no items have group '" + baselayerRegion + "\"");
}
}
const title: Store<string> = state.getStoreFor(["id"]); const title: Store<string> = state.getStoreFor(["id"]);
const wl = window.location; const wl = window.location;
const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout="; const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout=";
@ -46,18 +41,46 @@
function firstPathsFor(...regionNames: string[]): Set<string> { function firstPathsFor(...regionNames: string[]): Set<string> {
const pathNames = new Set<string>(); const pathNames = new Set<string>();
for (const regionName of regionNames) { for (const regionName of regionNames) {
const region: ConfigMeta[] = perRegion[regionName] const region: ConfigMeta[] = perRegion[regionName];
for (const configMeta of region) { for (const configMeta of region) {
pathNames.add(configMeta.path[0]) pathNames.add(configMeta.path[0]);
} }
} }
return pathNames; return pathNames;
} }
function configForRequiredField(id: string): ConfigMeta{
let config = layerSchema.find(config => config.path.length === 1 && config.path[0] === id)
config = Utils.Clone(config)
config.required = true
console.log(">>>", config)
config.hints.ifunset = undefined
return config
}
let requiredFields = ["id", "name", "description"];
let currentlyMissing = state.configuration.map(config => {
const missing = [];
for (const requiredField of requiredFields) {
if (!config[requiredField]) {
missing.push(requiredField);
}
}
return missing;
});
</script> </script>
<div class="w-full flex justify-between"> {#if $currentlyMissing.length > 0}
{#each requiredFields as required}
<SchemaBasedInput {state}
schema={configForRequiredField(required)}
path={[required]} />
{/each}
{:else}
<div class="w-full flex justify-between my-2">
<slot /> <slot />
<h3>Editing layer {$title}</h3> <h3>Editing layer {$title}</h3>
{#if $hasErrors > 0} {#if $hasErrors > 0}
@ -65,46 +88,59 @@
{:else} {:else}
<a class="primary button" href={baseUrl+state.server.layerUrl(title.data)} target="_blank" rel="noopener"> <a class="primary button" href={baseUrl+state.server.layerUrl(title.data)} target="_blank" rel="noopener">
Try it out Try it out
<ChevronRightIcon class= "h-6 w-6 shrink-0"/> <ChevronRightIcon class="h-6 w-6 shrink-0" />
</a> </a>
{/if} {/if}
</div> </div>
<div class="m4"> <div class="m4">
<TabbedGroup> <TabbedGroup>
<div slot="title0" class="flex">General properties <div slot="title0" class="flex">General properties
<ErrorIndicatorForRegion firstPaths={firstPathsFor(...baselayerRegions)} {state} /> <ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} />
</div> </div>
<div class="flex flex-col" slot="content0"> <div class="flex flex-col" slot="content0">
{#each baselayerRegions as region} <Region {state} configs={perRegion["Basic"]} />
<Region {state} configs={perRegion[region]} title={region} />
{/each}
</div> </div>
<div slot="title1" class="flex">Information panel (questions and answers) <div slot="title1" class="flex">Information panel (questions and answers)
<ErrorIndicatorForRegion firstPaths={firstPathsFor("title","tagrenderings","editing")} {state} /></div> <ErrorIndicatorForRegion firstPaths={firstPathsFor("title","tagrenderings","editing")} {state} />
</div>
<div slot="content1"> <div slot="content1">
<Region configs={perRegion["title"]} {state} title="Popup title" /> <Region configs={perRegion["title"]} {state} title="Popup title" />
<Region configs={perRegion["tagrenderings"]} {state} title="Popup contents" /> <Region configs={perRegion["tagrenderings"]} {state} title="Popup contents" />
<Region configs={perRegion["editing"]} {state} title="Other editing elements" /> <Region configs={perRegion["editing"]} {state} title="Other editing elements" />
</div> </div>
<div slot="title2" class="flex">Rendering on the map <div slot="title2">
<ErrorIndicatorForRegion firstPaths={firstPathsFor("linerendering","pointrendering")} {state} /></div> <ErrorIndicatorForRegion firstPaths={firstPathsFor("presets")} {state} />
Creating a new point
</div>
<div slot="content2"> <div slot="content2">
<Region {state} configs={perRegion["presets"]} />
</div>
<div slot="title3" class="flex">Rendering on the map
<ErrorIndicatorForRegion firstPaths={firstPathsFor("linerendering","pointrendering")} {state} />
</div>
<div slot="content3">
<Region configs={perRegion["linerendering"]} {state} /> <Region configs={perRegion["linerendering"]} {state} />
<Region configs={perRegion["pointrendering"]} {state} /> <Region configs={perRegion["pointrendering"]} {state} />
</div> </div>
<div slot="title3" class="flex">Advanced functionality <div slot="title4" class="flex">Advanced functionality
<ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced","expert")} {state} /></div> <ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced","expert")} {state} />
<div slot="content3"> </div>
<div slot="content4">
<Region configs={perRegion["advanced"]} {state} /> <Region configs={perRegion["advanced"]} {state} />
<Region configs={perRegion["expert"]} {state} /> <Region configs={perRegion["expert"]} {state} />
</div> </div>
<div slot="title4">Configuration file</div> <div slot="title5">Configuration file</div>
<div slot="content4"> <div slot="content5">
<div> <div>
Below, you'll find the raw configuration file in `.json`-format. Below, you'll find the raw configuration file in `.json`-format.
This is mostly for debugging purposes This is mosSendertly for debugging purposes
</div> </div>
<div class="literal-code"> <div class="literal-code">
{JSON.stringify($configuration, null, " ")} {JSON.stringify($configuration, null, " ")}
@ -122,4 +158,5 @@
</div> </div>
</TabbedGroup> </TabbedGroup>
</div> </div>
{/if}

View file

@ -1,8 +1,6 @@
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { ConfigMeta } from "./configMeta" import { ConfigMeta } from "./configMeta"
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import { QueryParameters } from "../../Logic/Web/QueryParameters"
import { import {
ConversionContext, ConversionContext,
ConversionMessage, ConversionMessage,
@ -16,25 +14,29 @@ import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Jso
import { TagUtils } from "../../Logic/Tags/TagUtils" import { TagUtils } from "../../Logic/Tags/TagUtils"
import StudioServer from "./StudioServer" import StudioServer from "./StudioServer"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
/** /**
* Sends changes back to the server * Sends changes back to the server
*/ */
export class LayerStateSender { export class LayerStateSender {
constructor(layerState: EditLayerState) { constructor(layerState: EditLayerState) {
layerState.configuration.addCallback(async (config) => { const layerId = layerState.configuration.map((config) => config.id)
const id = config.id layerState.configuration
.mapD((config) => JSON.stringify(config, null, " "))
.stabilized(100)
.addCallbackD(async (config) => {
const id = layerId.data
if (id === undefined) { if (id === undefined) {
console.warn("No id found in layer, not updating") console.warn("No id found in layer, not updating")
return return
} }
await layerState.server.updateLayer(<LayerConfigJson>config) await layerState.server.updateLayer(id, config)
}) })
} }
} }
export default class EditLayerState { export default class EditLayerState {
public readonly osmConnection: OsmConnection
public readonly schema: ConfigMeta[] public readonly schema: ConfigMeta[]
public readonly featureSwitches: { featureSwitchIsDebugging: UIEventSource<boolean> } public readonly featureSwitches: { featureSwitchIsDebugging: UIEventSource<boolean> }
@ -44,17 +46,14 @@ export default class EditLayerState {
>({}) >({})
public readonly messages: Store<ConversionMessage[]> public readonly messages: Store<ConversionMessage[]>
public readonly server: StudioServer public readonly server: StudioServer
// Needed for the special visualisations
public readonly osmConnection: OsmConnection
private readonly _stores = new Map<string, UIEventSource<any>>()
constructor(schema: ConfigMeta[], server: StudioServer) { constructor(schema: ConfigMeta[], server: StudioServer, osmConnection: OsmConnection) {
this.schema = schema this.schema = schema
this.server = server this.server = server
this.osmConnection = new OsmConnection({ this.osmConnection = osmConnection
oauth_token: QueryParameters.GetQueryParameter(
"oauth_token",
undefined,
"Used to complete the login"
),
})
this.featureSwitches = { this.featureSwitches = {
featureSwitchIsDebugging: new UIEventSource<boolean>(true), featureSwitchIsDebugging: new UIEventSource<boolean>(true),
} }
@ -118,7 +117,6 @@ export default class EditLayerState {
return entry return entry
} }
private readonly _stores = new Map<string, UIEventSource<any>>()
public getStoreFor<T>(path: ReadonlyArray<string | number>): UIEventSource<T | undefined> { public getStoreFor<T>(path: ReadonlyArray<string | number>): UIEventSource<T | undefined> {
const key = path.join(".") const key = path.join(".")
@ -139,7 +137,9 @@ export default class EditLayerState {
value: Store<any>, value: Store<any>,
noInitialSync: boolean = false noInitialSync: boolean = false
): () => void { ): () => void {
const unsync = value.addCallback((v) => this.setValueAt(path, v)) const unsync = value.addCallback((v) => {
this.setValueAt(path, v)
})
if (!noInitialSync) { if (!noInitialSync) {
this.setValueAt(path, value.data) this.setValueAt(path, value.data)
} }
@ -180,6 +180,7 @@ export default class EditLayerState {
public setValueAt(path: ReadonlyArray<string | number>, v: any) { public setValueAt(path: ReadonlyArray<string | number>, v: any) {
let entry = this.configuration.data let entry = this.configuration.data
console.log("Setting value at", path, v)
const isUndefined = const isUndefined =
v === undefined || v === undefined ||
v === null || v === null ||
@ -197,15 +198,35 @@ export default class EditLayerState {
} }
entry = entry[breadcrumb] entry = entry[breadcrumb]
} }
const lastBreadcrumb = path.at(-1) const lastBreadcrumb = path.at(-1)
if (isUndefined) { if (isUndefined) {
if (entry && entry[lastBreadcrumb]) { if (entry && entry[lastBreadcrumb]) {
console.log("Deleting", lastBreadcrumb, "of", path.join(".")) console.log("Deleting", lastBreadcrumb, "of", path.join("."))
delete entry[lastBreadcrumb] delete entry[lastBreadcrumb]
}
} else {
entry[lastBreadcrumb] = v
}
this.configuration.ping() this.configuration.ping()
} }
} else if (entry[lastBreadcrumb] !== v) {
console.log("Assigning and pinging at", path)
entry[lastBreadcrumb] = v
this.configuration.ping()
}
}
public messagesFor(path: ReadonlyArray<string | number>): Store<ConversionMessage[]> {
return this.messages.map((msgs) => {
if (!msgs) {
return []
}
return msgs.filter((msg) => {
const pth = msg.context.path
for (let i = 0; i < Math.min(pth.length, path.length); i++) {
if (pth[i] !== path[i]) {
return false
}
}
return true
})
})
}
} }

View file

@ -110,7 +110,7 @@
{/if} {/if}
<div class="border border-black"> <div class="border border-black">
{#if isTagRenderingBlock} {#if isTagRenderingBlock}
<TagRenderingInput path={path.concat(value)} {state} {schema} > <TagRenderingInput path={[...path, (value)]} {state} {schema} >
<button slot="upper-right" class="border-black border rounded-full p-1 w-fit h-fit" <button slot="upper-right" class="border-black border rounded-full p-1 w-fit h-fit"
on:click={() => {del(value)}}> on:click={() => {del(value)}}>
<TrashIcon class="w-4 h-4" /> <TrashIcon class="w-4 h-4" />

View file

@ -21,12 +21,12 @@
const isTranslation = schema.hints.typehint === "translation" || schema.hints.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema); const isTranslation = schema.hints.typehint === "translation" || schema.hints.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema);
let type = schema.hints.typehint ?? "string"; let type = schema.hints.typehint ?? "string";
let rendervalue = ((schema.hints.inline ?? schema.path.join(".")) + " <b>{translated(value)}</b>"); let rendervalue = (schema.hints.inline ?? schema.path.join(".")) + (isTranslation ? " <b>{translated(value)}</b>": " <b>{value}</b>");
if(schema.type === "boolean"){ if(schema.type === "boolean"){
rendervalue = undefined rendervalue = undefined
} }
if(schema.hints.typehint === "tag") { if(schema.hints.typehint === "tag" || schema.hints.typehint === "simple_tag") {
rendervalue = "{tags()}" rendervalue = "{tags()}"
} }
@ -61,12 +61,12 @@
if (schema.hints.default) { if (schema.hints.default) {
configJson.mappings = [{ configJson.mappings = [{
if: "value=", // We leave this blank 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 ?? "") then: path.at(-1) + " is not set. The default value <b>" + schema.hints.default + "</b> will be used. " + (schema.hints.ifunset ?? "")
}]; }];
} else if (!schema.required) { } else if (!schema.required) {
configJson.mappings = [{ configJson.mappings = [{
if: "value=", if: "value=",
then: schema.path.at(-1) + " is not set. " + (schema.hints.ifunset ?? "") then: path.at(-1) + " is not set. " + (schema.hints.ifunset ?? "")
}]; }];
} }
@ -109,15 +109,7 @@
} }
let config: TagRenderingConfig; let config: TagRenderingConfig;
let err: string = undefined; let err: string = undefined;
let messages = state.messages.mapD(msgs => msgs.filter(msg => { let messages = state.messagesFor(path)
const pth = msg.context.path;
for (let i = 0; i < Math.min(pth.length, path.length); i++) {
if (pth[i] !== path[i]) {
return false;
}
}
return true;
}));
try { try {
config = new TagRenderingConfig(configJson, "config based on " + schema.path.join(".")); config = new TagRenderingConfig(configJson, "config based on " + schema.path.join("."));
} catch (e) { } catch (e) {
@ -130,7 +122,7 @@
onDestroy(state.register(path, tags.map(tgs => { onDestroy(state.register(path, tags.map(tgs => {
const v = tgs["value"]; const v = tgs["value"];
if (typeof v !== "string") { if (typeof v !== "string") {
return v; return { ... v };
} }
if (schema.type === "boolan") { if (schema.type === "boolan") {
return v === "true" || v === "yes" || v === "1"; return v === "true" || v === "yes" || v === "1";
@ -140,7 +132,6 @@
return true; return true;
} }
if (v === "false" || v === "no" || v === "0") { if (v === "false" || v === "no" || v === "0") {
console.log("Setting false...");
return false; return false;
} }
} }
@ -173,7 +164,7 @@
{/each} {/each}
{/if} {/if}
{#if window.location.hostname === "127.0.0.1"} {#if window.location.hostname === "127.0.0.1"}
<span class="subtle">{schema.path.join(".")} {schema.hints.typehint}</span> <span class="subtle">{path.join(".")} {schema.hints.typehint}</span>
{/if} {/if}
</div> </div>
{/if} {/if}

View file

@ -14,7 +14,7 @@
{#if schema.hints.typehint === "tagrendering[]"} {#if schema.hints.typehint === "tagrendering[]"}
<!-- We cheat a bit here by matching this 'magical' type... --> <!-- We cheat a bit here by matching this 'magical' type... -->
<SchemaBasedArray {path} {state} {schema} /> <SchemaBasedArray {path} {state} {schema} />
{:else if schema.type === "array" && schema.hints.multianswer === "true"} {:else if schema.type === "array" && schema.hints.multianswer === "true"}
<ArrayMultiAnswer {path} {state} {schema}/> <ArrayMultiAnswer {path} {state} {schema}/>
{:else if schema.type === "array"} {:else if schema.type === "array"}
<SchemaBasedArray {path} {state} {schema} /> <SchemaBasedArray {path} {state} {schema} />

View file

@ -13,7 +13,6 @@
import type { JsonSchemaType } from "./jsonSchema"; import type { JsonSchemaType } from "./jsonSchema";
// @ts-ignore // @ts-ignore
import nmd from "nano-markdown"; import nmd from "nano-markdown";
import { writable } from "svelte/store";
/** /**
* If 'types' is defined: allow the user to pick one of the types to input. * If 'types' is defined: allow the user to pick one of the types to input.
@ -42,7 +41,7 @@
} }
const configJson: QuestionableTagRenderingConfigJson = { const configJson: QuestionableTagRenderingConfigJson = {
id: "TYPE_OF:" + path.join("_"), id: "TYPE_OF:" + path.join("_"),
question: "Which subcategory is needed for "+schema.path.at(-1)+"?", question: "Which subcategory is needed for " + schema.path.at(-1) + "?",
questionHint: nmd(schema.description), questionHint: nmd(schema.description),
mappings: types.map(opt => opt.trim()).filter(opt => opt.length > 0).map((opt, i) => ({ mappings: types.map(opt => opt.trim()).filter(opt => opt.length > 0).map((opt, i) => ({
if: "chosen_type_index=" + i, if: "chosen_type_index=" + i,
@ -127,14 +126,14 @@
possibleTypes.sort((a, b) => b.optionalMatches - a.optionalMatches); possibleTypes.sort((a, b) => b.optionalMatches - a.optionalMatches);
possibleTypes.sort((a, b) => b.matchingPropertiesCount - a.matchingPropertiesCount); possibleTypes.sort((a, b) => b.matchingPropertiesCount - a.matchingPropertiesCount);
if (possibleTypes.length > 0) { if (possibleTypes.length > 0) {
chosenOption = possibleTypes[0].index chosenOption = possibleTypes[0].index;
tags.setData({ chosen_type_index: "" + chosenOption}); tags.setData({ chosen_type_index: "" + chosenOption });
} }
} else if (defaultOption !== undefined) { } else if (defaultOption !== undefined) {
tags.setData({ chosen_type_index: "" + defaultOption }); tags.setData({ chosen_type_index: "" + defaultOption });
}else{ } else {
chosenOption = defaultOption chosenOption = defaultOption;
} }
if (hasBooleanOption >= 0 || lastIsString) { if (hasBooleanOption >= 0 || lastIsString) {
@ -154,7 +153,7 @@
let subSchemas: ConfigMeta[] = []; let subSchemas: ConfigMeta[] = [];
let subpath = path; let subpath = path;
const store = state.getStoreFor(path) const store = state.getStoreFor(path);
onDestroy(tags.addCallbackAndRun(tags => { onDestroy(tags.addCallbackAndRun(tags => {
if (tags["value"] !== undefined && tags["value"] !== "") { if (tags["value"] !== undefined && tags["value"] !== "") {
chosenOption = undefined; chosenOption = undefined;
@ -170,7 +169,7 @@
for (const key of type?.required ?? []) { for (const key of type?.required ?? []) {
o[key] ??= {}; o[key] ??= {};
} }
store.setData(o) store.setData(o);
} }
if (!type) { if (!type) {
return; return;
@ -191,6 +190,7 @@
subSchemas.push(...(state.getSchema([...cleanPath, crumble]))); subSchemas.push(...(state.getSchema([...cleanPath, crumble])));
} }
})); }));
let messages = state.messagesFor(path);
</script> </script>
@ -209,5 +209,9 @@
<SchemaBasedInput {state} schema={subschema} <SchemaBasedInput {state} schema={subschema}
path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput> path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput>
{/each} {/each}
{:else if $messages.length > 0}
{#each $messages as msg}
<div class="alert">{msg.message}</div>
{/each}
{/if} {/if}
</div> </div>

View file

@ -8,7 +8,7 @@
export let path: (string | number)[] = []; export let path: (string | number)[] = [];
export let schema: ConfigMeta; export let schema: ConfigMeta;
let value = new UIEventSource<string>("{}"); let value = new UIEventSource<string>({});
console.log("Registering translation to path", path) console.log("Registering translation to path", path)
state.register(path, value.mapD(v => JSON.parse(value.data ))); state.register(path, value.mapD(v => JSON.parse(value.data )));
</script> </script>

View file

@ -1,23 +1,51 @@
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import { Store } from "../../Logic/UIEventSource"
export default class StudioServer { export default class StudioServer {
private readonly url: string private readonly url: string
private readonly _userId: Store<number>
constructor(url: string) { constructor(url: string, userId: Store<number>) {
this.url = url this.url = url
this._userId = userId
} }
public async fetchLayerOverview(): Promise<Set<string>> { public async fetchLayerOverview(): Promise<
{
id: string
owner: number
}[]
> {
const uid = this._userId.data
let uidQueryParam = ""
if (this._userId.data !== undefined) {
uidQueryParam = "?userId=" + uid
}
const { allFiles } = <{ allFiles: string[] }>( const { allFiles } = <{ allFiles: string[] }>(
await Utils.downloadJson(this.url + "/overview") await Utils.downloadJson(this.url + "/overview" + uidQueryParam)
) )
const layers = allFiles const layerOverview: {
.filter((f) => f.startsWith("layers/")) id: string
.map((l) => l.substring(l.lastIndexOf("/") + 1, l.length - ".json".length)) owner: number | undefined
.filter((layerId) => Constants.priviliged_layers.indexOf(<any>layerId) < 0) }[] = []
return new Set<string>(layers) for (let file of allFiles) {
let owner = undefined
if (file.startsWith("" + uid)) {
owner = uid
file = file.substring(file.indexOf("/") + 1)
}
if (!file.startsWith("layers/")) {
continue
}
const id = file.substring(file.lastIndexOf("/") + 1, file.length - ".json".length)
if (Constants.priviliged_layers.indexOf(<any>id) > 0) {
continue
}
layerOverview.push({ id, owner })
}
return layerOverview
} }
async fetchLayer(layerId: string): Promise<LayerConfigJson> { async fetchLayer(layerId: string): Promise<LayerConfigJson> {
@ -28,8 +56,7 @@ export default class StudioServer {
} }
} }
async updateLayer(config: LayerConfigJson) { async updateLayer(id: string, config: string) {
const id = config.id
if (id === undefined || id === "") { if (id === undefined || id === "") {
return return
} }
@ -38,11 +65,13 @@ export default class StudioServer {
headers: { headers: {
"Content-Type": "application/json;charset=utf-8", "Content-Type": "application/json;charset=utf-8",
}, },
body: JSON.stringify(config, null, " "), body: config,
}) })
} }
public layerUrl(id: string) { public layerUrl(id: string) {
return `${this.url}/layers/${id}/${id}.json` const uid = this._userId.data
const uidStr = uid !== undefined ? "/" + uid : ""
return `${this.url}${uidStr}/layers/${id}/${id}.json`
} }
} }

View file

@ -139,7 +139,7 @@ if (!initialTag) {
<div class="border-l-4 border-black flex flex-col ml-1 pl-1"> <div class="border-l-4 border-black flex flex-col ml-1 pl-1">
{#each $basicTags as basicTag (basicTag)} {#each $basicTags as basicTag (basicTag)}
<div class="flex"> <div class="flex">
<BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} /> <BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} on:submit />
{#if $basicTags.length + $expressions.length > 1} {#if $basicTags.length + $expressions.length > 1}
<button class="border border-black rounded-full w-fit h-fit p-0" <button class="border border-black rounded-full w-fit h-fit p-0"
on:click={() => removeTag(basicTag)}> on:click={() => removeTag(basicTag)}>

View file

@ -111,7 +111,7 @@
<div class="flex h-fit "> <div class="flex h-fit ">
<ValidatedInput feedback={feedbackKey} placeholder="The key of the tag" type="key" <ValidatedInput feedback={feedbackKey} placeholder="The key of the tag" type="key"
value={keyValue}></ValidatedInput> value={keyValue} on:submit></ValidatedInput>
<select bind:value={mode} on:focusin={() => dropdownFocussed.setData(true)} on:focusout={() => dropdownFocussed.setData(false)}> <select bind:value={mode} on:focusin={() => dropdownFocussed.setData(true)} on:focusout={() => dropdownFocussed.setData(false)}>
{#each modes as option} {#each modes as option}
<option value={option}> <option value={option}>
@ -120,7 +120,7 @@
{/each} {/each}
</select> </select>
<ValidatedInput feedback={feedbackValue} placeholder="The value of the tag" type="string" <ValidatedInput feedback={feedbackValue} placeholder="The value of the tag" type="string"
value={valueValue}></ValidatedInput> value={valueValue} on:submit></ValidatedInput>
</div> </div>
{#if $feedbackKey} {#if $feedbackKey}

View file

@ -13,7 +13,7 @@ export let silent: boolean
</script> </script>
<div class="m-2"> <div class="m-2">
<TagExpression {silent} {overpassSupportNeeded} {tag} {uploadableOnly}> <TagExpression {silent} {overpassSupportNeeded} {tag} {uploadableOnly} on:submit>
<slot name="delete" slot="delete"/> <slot name="delete" slot="delete"/>
</TagExpression> </TagExpression>
</div> </div>

View file

@ -58,7 +58,6 @@ tags.addCallbackAndRunD(tgs => {
let mappings: UIEventSource<MappingConfigJson[]> = state.getStoreFor([...path, "mappings"]); let mappings: UIEventSource<MappingConfigJson[]> = state.getStoreFor([...path, "mappings"]);
$: console.log("Allow questions:", $allowQuestions)
const topLevelItems: Record<string, ConfigMeta> = {}; const topLevelItems: Record<string, ConfigMeta> = {};
for (const item of questionableTagRenderingSchemaRaw) { for (const item of questionableTagRenderingSchemaRaw) {
if (item.path.length === 1) { if (item.path.length === 1) {
@ -81,7 +80,6 @@ const missing: string[] = questionableTagRenderingSchemaRaw.filter(schema => sch
</script> </script>
{#if typeof value === "string"} {#if typeof value === "string"}
<div class="flex low-interaction"> <div class="flex low-interaction">
<TagRenderingEditable config={configBuiltin} selectedElement={undefined} showQuestionIfUnknown={true} {state} <TagRenderingEditable config={configBuiltin} selectedElement={undefined} showQuestionIfUnknown={true} {state}
{tags} /> {tags} />
@ -92,12 +90,9 @@ const missing: string[] = questionableTagRenderingSchemaRaw.filter(schema => sch
<div class="flex justify-end"> <div class="flex justify-end">
<slot name="upper-right" /> <slot name="upper-right" />
</div> </div>
{#if $allowQuestions} {#if $allowQuestions}
<SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]} /> <SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]} />
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]} /> <SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]} />
{:else}
{/if} {/if}
{#each ($mappings ?? []) as mapping, i (mapping)} {#each ($mappings ?? []) as mapping, i (mapping)}
<div class="flex interactive w-full"> <div class="flex interactive w-full">

View file

@ -2,13 +2,10 @@
import NextButton from "./Base/NextButton.svelte"; import NextButton from "./Base/NextButton.svelte";
import { UIEventSource } from "../Logic/UIEventSource"; import { Store, UIEventSource } from "../Logic/UIEventSource";
import ValidatedInput from "./InputElement/ValidatedInput.svelte";
import EditLayerState from "./Studio/EditLayerState"; import EditLayerState from "./Studio/EditLayerState";
import EditLayer from "./Studio/EditLayer.svelte"; import EditLayer from "./Studio/EditLayer.svelte";
import Loading from "../assets/svg/Loading.svelte"; import Loading from "../assets/svg/Loading.svelte";
import Marker from "./Map/Marker.svelte";
import { AllSharedLayers } from "../Customizations/AllSharedLayers";
import StudioServer from "./Studio/StudioServer"; import StudioServer from "./Studio/StudioServer";
import LoginToggle from "./Base/LoginToggle.svelte"; import LoginToggle from "./Base/LoginToggle.svelte";
import { OsmConnection } from "../Logic/Osm/OsmConnection"; import { OsmConnection } from "../Logic/Osm/OsmConnection";
@ -17,53 +14,54 @@
import layerSchemaRaw from "../../src/assets/schemas/layerconfigmeta.json"; import layerSchemaRaw from "../../src/assets/schemas/layerconfigmeta.json";
import If from "./Base/If.svelte"; import If from "./Base/If.svelte";
import BackButton from "./Base/BackButton.svelte"; import BackButton from "./Base/BackButton.svelte";
import ChooseLayerToEdit from "./Studio/ChooseLayerToEdit.svelte";
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource";
import FloatOver from "./Base/FloatOver.svelte";
import Walkthrough from "./Walkthrough/Walkthrough.svelte";
import * as intro from "../assets/studio_introduction.json";
import { QuestionMarkCircleIcon } from "@babeard/svelte-heroicons/mini";
import type { ConfigMeta } from "./Studio/configMeta";
export let studioUrl = window.location.hostname === "127.0.0.1" ? "http://127.0.0.1:1235" : "https://studio.mapcomplete.org"; export let studioUrl = window.location.hostname === "127.0.0.1" ? "http://127.0.0.1:1235" : "https://studio.mapcomplete.org";
const studio = new StudioServer(studioUrl);
let layersWithErr = UIEventSource.FromPromiseWithErr(studio.fetchLayerOverview()); let osmConnection = new OsmConnection(new OsmConnection({
let layers = layersWithErr.mapD(l => l.success); oauth_token: QueryParameters.GetQueryParameter(
let state: undefined | "edit_layer" | "new_layer" | "edit_theme" | "new_theme" | "editing_layer" | "loading" = undefined; "oauth_token",
undefined,
"Used to complete the login"
)
}));
const createdBy = osmConnection.userDetails.data.name;
const uid = osmConnection.userDetails.map(ud => ud?.uid);
const studio = new StudioServer(studioUrl, uid);
let layersWithErr = uid.bind(uid => UIEventSource.FromPromiseWithErr(studio.fetchLayerOverview()));
let layers: Store<{ owner: number }[]> = layersWithErr.mapD(l => l.success);
let selfLayers = layers.mapD(ls => ls.filter(l => l.owner === uid.data), [uid]);
let otherLayers = layers.mapD(ls => ls.filter(l => l.owner !== uid.data), [uid]);
let state: undefined | "edit_layer" | "edit_theme" | "new_theme" | "editing_layer" | "loading" = undefined;
let initialLayerConfig: { id: string }; let initialLayerConfig: { id: string };
let newLayerId = new UIEventSource<string>("");
/**
* Also used in the input field as 'feedback', hence not a mappedStore as it must be writable
*/
let layerIdFeedback = new UIEventSource<string>(undefined);
newLayerId.addCallbackD(layerId => {
if (layerId === "") {
return;
}
if (layers.data?.has(layerId)) {
layerIdFeedback.setData("This id is already used");
}
}, [layers]);
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw; const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
let editLayerState = new EditLayerState(layerSchema, studio); let editLayerState = new EditLayerState(layerSchema, studio, osmConnection);
let layerId = editLayerState.configuration.map(layerConfig => layerConfig.id); let layerId = editLayerState.configuration.map(layerConfig => layerConfig.id);
function fetchIconDescription(layerId): any { let showIntro = UIEventSource.asBoolean(LocalStorageSource.Get("studio-show-intro", "true"));
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
async function editLayer(event: Event) {
const layerId = event.detail;
state = "loading";
initialLayerConfig = await studio.fetchLayer(layerId);
state = "editing_layer";
} }
async function createNewLayer() { async function createNewLayer() {
if (layerIdFeedback.data !== undefined) {
console.warn("There is still some feedback - not starting to create a new layer");
return;
}
state = "loading"; state = "loading";
const id = newLayerId.data; initialLayerConfig = {
const createdBy = osmConnection.userDetails.data.name; credits: createdBy,
try {
const loaded = await studio.fetchLayer(id);
initialLayerConfig = loaded ?? {
id, credits: createdBy,
minzoom: 15, minzoom: 15,
pointRendering: [ pointRendering: [
{ {
@ -79,19 +77,9 @@
color: "blue" color: "blue"
}] }]
}; };
} catch (e) {
initialLayerConfig = { id, credits: createdBy };
}
state = "editing_layer"; state = "editing_layer";
} }
let osmConnection = new OsmConnection(new OsmConnection({
oauth_token: QueryParameters.GetQueryParameter(
"oauth_token",
undefined,
"Used to complete the login"
)
}));
</script> </script>
@ -125,13 +113,14 @@
</NextButton> </NextButton>
</div> </div>
{#if state === undefined} {#if state === undefined}
<div class="m-4">
<h1>MapComplete Studio</h1> <h1>MapComplete Studio</h1>
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<NextButton on:click={() => state = "edit_layer"}> <NextButton on:click={() => state = "edit_layer"}>
Edit an existing layer Edit an existing layer
</NextButton> </NextButton>
<NextButton on:click={() => state = "new_layer"}> <NextButton on:click={() => createNewLayer()}>
Create a new layer Create a new layer
</NextButton> </NextButton>
<!-- <!--
@ -142,58 +131,43 @@
Create a new theme Create a new theme
</NextButton> </NextButton>
--> -->
<NextButton clss="small" on:click={() => {showIntro.setData(true)} }>
<QuestionMarkCircleIcon class="w-6 h-6" />
Show the introduction again
</NextButton>
</div>
</div> </div>
{:else if state === "edit_layer"} {:else if state === "edit_layer"}
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio</BackButton> <div class="flex flex-col m-4">
<h3>Choose a layer to edit</h3> <BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
<div class="flex flex-wrap"> </BackButton>
{#each Array.from($layers) as layerId} <h2>Choose a layer to edit</h2>
<NextButton clss="small" on:click={async () => { <ChooseLayerToEdit layerIds={$selfLayers} on:layerSelected={editLayer}>
state = "loading" <h3 slot="title">Your layers</h3>
initialLayerConfig = await studio.fetchLayer(layerId) </ChooseLayerToEdit>
state = "editing_layer" <h3>Official layers</h3>
}}> <ChooseLayerToEdit layerIds={$otherLayers} on:layerSelected={editLayer} />
<div class="w-4 h-4 mr-1">
<Marker icons={fetchIconDescription(layerId)} />
</div>
{layerId}
</NextButton>
{/each}
</div>
{:else if state === "new_layer"}
<div class="interactive flex m-2 rounded-2xl flex-col p-2">
<h3>Enter the ID for the new layer</h3>
A good ID is:
<ul>
<li>a noun</li>
<li>singular</li>
<li>describes the object</li>
<li>in English</li>
</ul>
<div class="m-2 p-2 w-full">
<ValidatedInput type="id" value={newLayerId} feedback={layerIdFeedback} on:submit={() => createNewLayer()} />
</div>
{#if $layerIdFeedback !== undefined}
<div class="alert">
{$layerIdFeedback}
</div>
{:else }
<NextButton clss="primary" on:click={() => createNewLayer()}>
Create layer {$newLayerId}
</NextButton>
{/if}
</div> </div>
{:else if state === "loading"} {:else if state === "loading"}
<div class="w-8 h-8"> <div class="w-8 h-8">
<Loading /> <Loading />
</div> </div>
{:else if state === "editing_layer"} {:else if state === "editing_layer"}
<EditLayer {initialLayerConfig} state={editLayerState} > <EditLayer {initialLayerConfig} state={editLayerState}>
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio</BackButton> <BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
</BackButton>
</EditLayer> </EditLayer>
{/if} {/if}
</LoginToggle> </LoginToggle>
</If> </If>
{#if $showIntro}
<FloatOver>
<div class="flex p-4 h-full">
<Walkthrough pages={intro.sections} on:done={() => {showIntro.setData(false)}} />
</div>
</FloatOver>
{/if}