forked from MapComplete/MapComplete
		
	Studio: add slideshow, add useability tweaks
This commit is contained in:
		
							parent
							
								
									2df9aa8564
								
							
						
					
					
						commit
						8bc555fbe0
					
				
					 26 changed files with 440 additions and 316 deletions
				
			
		|  | @ -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 }) | ||||||
|  |  | ||||||
|  | @ -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) { | ||||||
|  |  | ||||||
|  | @ -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,9 +725,11 @@ 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) { | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -19,4 +19,4 @@ let tag: UIEventSource<string | TagConfigJson> = value | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| <FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} /> | <FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} on:submit/> | ||||||
|  |  | ||||||
|  | @ -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]); | ||||||
|   })); |   })); | ||||||
|  |  | ||||||
|  | @ -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"} | ||||||
|  |  | ||||||
|  | @ -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")  | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|                             } |                             } | ||||||
|                         }) |                         }) | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  | @ -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,80 +41,122 @@ | ||||||
|   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} | ||||||
|   <slot /> |  | ||||||
|   <h3>Editing layer {$title}</h3> |  | ||||||
|   {#if $hasErrors > 0} |  | ||||||
|     <div class="alert">{$hasErrors} errors detected</div> |  | ||||||
|   {:else} |  | ||||||
|     <a class="primary button" href={baseUrl+state.server.layerUrl(title.data)} target="_blank" rel="noopener"> |  | ||||||
|       Try it out |  | ||||||
|       <ChevronRightIcon class= "h-6 w-6 shrink-0"/> |  | ||||||
|     </a> |  | ||||||
|   {/if} |  | ||||||
| </div> |  | ||||||
| <div class="m4"> |  | ||||||
|   <TabbedGroup> |  | ||||||
|     <div slot="title0" class="flex">General properties |  | ||||||
|       <ErrorIndicatorForRegion firstPaths={firstPathsFor(...baselayerRegions)} {state} /> |  | ||||||
|     </div> |  | ||||||
|     <div class="flex flex-col" slot="content0"> |  | ||||||
|       {#each baselayerRegions as region} |  | ||||||
|         <Region {state} configs={perRegion[region]} title={region} /> |  | ||||||
|       {/each} |  | ||||||
|     </div> |  | ||||||
|     <div slot="title1" class="flex">Information panel (questions and answers) |  | ||||||
|       <ErrorIndicatorForRegion firstPaths={firstPathsFor("title","tagrenderings","editing")} {state} /></div> |  | ||||||
|     <div slot="content1"> |  | ||||||
|       <Region configs={perRegion["title"]} {state} title="Popup title" /> |  | ||||||
|       <Region configs={perRegion["tagrenderings"]} {state} title="Popup contents" /> |  | ||||||
|       <Region configs={perRegion["editing"]} {state} title="Other editing elements" /> |  | ||||||
|     </div> |  | ||||||
| 
 | 
 | ||||||
|     <div slot="title2" class="flex">Rendering on the map |   {#each requiredFields as required} | ||||||
|       <ErrorIndicatorForRegion firstPaths={firstPathsFor("linerendering","pointrendering")} {state} /></div> |     <SchemaBasedInput {state} | ||||||
|     <div slot="content2"> |                       schema={configForRequiredField(required)} | ||||||
|       <Region configs={perRegion["linerendering"]} {state} /> |                       path={[required]} /> | ||||||
|       <Region configs={perRegion["pointrendering"]} {state} /> |   {/each} | ||||||
|     </div> | {:else} | ||||||
|  |   <div class="w-full flex justify-between my-2"> | ||||||
|  |     <slot /> | ||||||
|  |     <h3>Editing layer {$title}</h3> | ||||||
|  |     {#if $hasErrors > 0} | ||||||
|  |       <div class="alert">{$hasErrors} errors detected</div> | ||||||
|  |     {:else} | ||||||
|  |       <a class="primary button" href={baseUrl+state.server.layerUrl(title.data)} target="_blank" rel="noopener"> | ||||||
|  |         Try it out | ||||||
|  |         <ChevronRightIcon class="h-6 w-6 shrink-0" /> | ||||||
|  |       </a> | ||||||
|  |     {/if} | ||||||
|  |   </div> | ||||||
|  |   <div class="m4"> | ||||||
|  |     <TabbedGroup> | ||||||
|  |       <div slot="title0" class="flex">General properties | ||||||
|  |         <ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} /> | ||||||
|  |       </div> | ||||||
|  |       <div class="flex flex-col" slot="content0"> | ||||||
|  |         <Region {state} configs={perRegion["Basic"]} /> | ||||||
| 
 | 
 | ||||||
|     <div slot="title3" class="flex">Advanced functionality |  | ||||||
|       <ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced","expert")} {state} /></div> |  | ||||||
|     <div slot="content3"> |  | ||||||
|       <Region configs={perRegion["advanced"]} {state} /> |  | ||||||
|       <Region configs={perRegion["expert"]} {state} /> |  | ||||||
|     </div> |  | ||||||
|     <div slot="title4">Configuration file</div> |  | ||||||
|     <div slot="content4"> |  | ||||||
|       <div> |  | ||||||
|         Below, you'll find the raw configuration file in `.json`-format. |  | ||||||
|         This is mostly for debugging purposes |  | ||||||
|       </div> |       </div> | ||||||
|       <div class="literal-code"> | 
 | ||||||
|         {JSON.stringify($configuration, null, "  ")} | 
 | ||||||
|  |       <div slot="title1" class="flex">Information panel (questions and answers) | ||||||
|  |         <ErrorIndicatorForRegion firstPaths={firstPathsFor("title","tagrenderings","editing")} {state} /> | ||||||
|       </div> |       </div> | ||||||
|       {#each $messages as message} |       <div slot="content1"> | ||||||
|         <li> |         <Region configs={perRegion["title"]} {state} title="Popup title" /> | ||||||
|           {message.level} |         <Region configs={perRegion["tagrenderings"]} {state} title="Popup contents" /> | ||||||
|           <span class="literal-code">{message.context.path.join(".")}</span> |         <Region configs={perRegion["editing"]} {state} title="Other editing elements" /> | ||||||
|           {message.message} |       </div> | ||||||
|           <span class="literal-code"> | 
 | ||||||
|  |       <div slot="title2"> | ||||||
|  |         <ErrorIndicatorForRegion firstPaths={firstPathsFor("presets")} {state} /> | ||||||
|  |         Creating a new point | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <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["pointrendering"]} {state} /> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <div slot="title4" class="flex">Advanced functionality | ||||||
|  |         <ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced","expert")} {state} /> | ||||||
|  |       </div> | ||||||
|  |       <div slot="content4"> | ||||||
|  |         <Region configs={perRegion["advanced"]} {state} /> | ||||||
|  |         <Region configs={perRegion["expert"]} {state} /> | ||||||
|  |       </div> | ||||||
|  |       <div slot="title5">Configuration file</div> | ||||||
|  |       <div slot="content5"> | ||||||
|  |         <div> | ||||||
|  |           Below, you'll find the raw configuration file in `.json`-format. | ||||||
|  |           This is mosSendertly for debugging purposes | ||||||
|  |         </div> | ||||||
|  |         <div class="literal-code"> | ||||||
|  |           {JSON.stringify($configuration, null, "  ")} | ||||||
|  |         </div> | ||||||
|  |         {#each $messages as message} | ||||||
|  |           <li> | ||||||
|  |             {message.level} | ||||||
|  |             <span class="literal-code">{message.context.path.join(".")}</span> | ||||||
|  |             {message.message} | ||||||
|  |             <span class="literal-code"> | ||||||
|           {message.context.operation.join(".")} |           {message.context.operation.join(".")} | ||||||
|           </span> |           </span> | ||||||
|         </li> |           </li> | ||||||
|       {/each} |         {/each} | ||||||
|     </div> |       </div> | ||||||
|   </TabbedGroup> |     </TabbedGroup> | ||||||
| 
 | 
 | ||||||
| </div> |   </div> | ||||||
|  | {/if} | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|             if (id === undefined) { |             .mapD((config) => JSON.stringify(config, null, "  ")) | ||||||
|                 console.warn("No id found in layer, not updating") |             .stabilized(100) | ||||||
|                 return |             .addCallbackD(async (config) => { | ||||||
|             } |                 const id = layerId.data | ||||||
|             await layerState.server.updateLayer(<LayerConfigJson>config) |                 if (id === undefined) { | ||||||
|         }) |                     console.warn("No id found in layer, not updating") | ||||||
|  |                     return | ||||||
|  |                 } | ||||||
|  |                 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] | ||||||
|  |                 this.configuration.ping() | ||||||
|             } |             } | ||||||
|         } else { |         } else if (entry[lastBreadcrumb] !== v) { | ||||||
|  |             console.log("Assigning and pinging at", path) | ||||||
|             entry[lastBreadcrumb] = v |             entry[lastBreadcrumb] = v | ||||||
|  |             this.configuration.ping() | ||||||
|         } |         } | ||||||
|         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 | ||||||
|  |             }) | ||||||
|  |         }) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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" /> | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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} /> | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -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` | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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)}> | ||||||
|  |  | ||||||
|  | @ -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} | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -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"> | ||||||
|  |  | ||||||
|  | @ -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,73 +14,15 @@ | ||||||
|   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 layers = layersWithErr.mapD(l => l.success); |  | ||||||
|   let state: undefined | "edit_layer" | "new_layer" | "edit_theme" | "new_theme" | "editing_layer" | "loading" = undefined; |  | ||||||
| 
 |  | ||||||
|   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; |  | ||||||
| 
 |  | ||||||
|   let editLayerState = new EditLayerState(layerSchema, studio); |  | ||||||
|   let layerId = editLayerState.configuration.map(layerConfig => layerConfig.id); |  | ||||||
| 
 |  | ||||||
|   function fetchIconDescription(layerId): any { |  | ||||||
|     return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function createNewLayer() { |  | ||||||
|     if (layerIdFeedback.data !== undefined) { |  | ||||||
|       console.warn("There is still some feedback - not starting to create a new layer"); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     state = "loading"; |  | ||||||
|     const id = newLayerId.data; |  | ||||||
|     const createdBy = osmConnection.userDetails.data.name; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     try { |  | ||||||
| 
 |  | ||||||
|       const loaded = await studio.fetchLayer(id); |  | ||||||
|       initialLayerConfig = loaded ?? { |  | ||||||
|         id, credits: createdBy, |  | ||||||
|         minzoom: 15, |  | ||||||
|         pointRendering: [ |  | ||||||
|           { |  | ||||||
|             location: ["point", "centroid"], |  | ||||||
|             marker: [{ |  | ||||||
|               icon: "circle", |  | ||||||
|               color: "white" |  | ||||||
|             }] |  | ||||||
|           } |  | ||||||
|         ], |  | ||||||
|         lineRendering: [{ |  | ||||||
|           width: 1, |  | ||||||
|           color: "blue" |  | ||||||
|         }] |  | ||||||
|       }; |  | ||||||
|     } catch (e) { |  | ||||||
|       initialLayerConfig = { id, credits: createdBy }; |  | ||||||
|     } |  | ||||||
|     state = "editing_layer"; |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   let osmConnection = new OsmConnection(new OsmConnection({ |   let osmConnection = new OsmConnection(new OsmConnection({ | ||||||
|     oauth_token: QueryParameters.GetQueryParameter( |     oauth_token: QueryParameters.GetQueryParameter( | ||||||
|  | @ -92,6 +31,55 @@ | ||||||
|       "Used to complete the login" |       "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 }; | ||||||
|  | 
 | ||||||
|  |   const layerSchema: ConfigMeta[] = <any>layerSchemaRaw; | ||||||
|  | 
 | ||||||
|  |   let editLayerState = new EditLayerState(layerSchema, studio, osmConnection); | ||||||
|  |   let layerId = editLayerState.configuration.map(layerConfig => layerConfig.id); | ||||||
|  | 
 | ||||||
|  |   let showIntro = UIEventSource.asBoolean(LocalStorageSource.Get("studio-show-intro", "true")); | ||||||
|  | 
 | ||||||
|  |   async function editLayer(event: Event) { | ||||||
|  |     const layerId = event.detail; | ||||||
|  |     state = "loading"; | ||||||
|  |     initialLayerConfig = await studio.fetchLayer(layerId); | ||||||
|  |     state = "editing_layer"; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async function createNewLayer() { | ||||||
|  |     state = "loading"; | ||||||
|  |     initialLayerConfig = { | ||||||
|  |       credits: createdBy, | ||||||
|  |       minzoom: 15, | ||||||
|  |       pointRendering: [ | ||||||
|  |         { | ||||||
|  |           location: ["point", "centroid"], | ||||||
|  |           marker: [{ | ||||||
|  |             icon: "circle", | ||||||
|  |             color: "white" | ||||||
|  |           }] | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       lineRendering: [{ | ||||||
|  |         width: 1, | ||||||
|  |         color: "blue" | ||||||
|  |       }] | ||||||
|  |     }; | ||||||
|  |     state = "editing_layer"; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -125,75 +113,61 @@ | ||||||
|       </NextButton> |       </NextButton> | ||||||
|     </div> |     </div> | ||||||
|     {#if state === undefined} |     {#if state === undefined} | ||||||
|       <h1>MapComplete Studio</h1> |       <div class="m-4"> | ||||||
|       <div class="w-full flex flex-col"> |         <h1>MapComplete Studio</h1> | ||||||
|  |         <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> | ||||||
|         <!-- |           <!-- | ||||||
|         <NextButton on:click={() => state = "edit_theme"}> |           <NextButton on:click={() => state = "edit_theme"}> | ||||||
|           Edit a theme |             Edit a theme | ||||||
|         </NextButton> |           </NextButton> | ||||||
|         <NextButton on:click={() => state = "new_theme"}> |           <NextButton on:click={() => state = "new_theme"}> | ||||||
|           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} | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue