diff --git a/Customizations/JSON/CustomLayoutFromJSON.ts b/Customizations/JSON/CustomLayoutFromJSON.ts index 34f52db..e298394 100644 --- a/Customizations/JSON/CustomLayoutFromJSON.ts +++ b/Customizations/JSON/CustomLayoutFromJSON.ts @@ -10,18 +10,78 @@ import FixedText from "../Questions/FixedText"; import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; +export interface TagRenderingConfigJson { + // If this key is present, then... + key?: string, + // Use this string to render + render: string, + // One of string, int, nat, float, pfloat, email, phone. Default: string + type?: string, + // If it is not known (and no mapping below matches), this question is asked; a textfield is inserted in the rendering above + question?: string, + // If a value is added with the textfield, this extra tag is addded. Optional field + addExtraTags?: string | string[] | { k: string, v: string }[]; + // Alternatively, these tags are shown if they match - even if the key above is not there + // If unknown, these become a radio button + mappings?: + { + if: string, + then: string + }[] +} + +export interface LayerConfigJson { + + id: string; + icon: string; + title: TagRenderingConfigJson; + description: string; + minzoom: number, + color: string; + overpassTags: string | string[] | { k: string, v: string }[]; + presets: [ + { + // icon: optional. Uses the layer icon by default + icon?: string; + // title: optional. Uses the layer title by default + title?: string; + // description: optional. Uses the layer description by default + description?: string; + // tags: optional list {k:string, v:string}[] + tags?: string | string[] | { k: string, v: string }[] + } + ], + tagRenderings: TagRenderingConfigJson [] +} + +export interface LayoutConfigJson { + name: string; + title: string; + description: string; + language: string; + layers: LayerConfigJson[], + startZoom: number; + startLat: number; + startLon: number; + /** + * Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64,' + */ + icon: string; +} + export class CustomLayoutFromJSON { - public static exampleLayer = { + public static exampleLayer: LayerConfigJson = { id: "bookcase", icon: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgaWQ9InN2ZzExMzgyIgogICBoZWlnaHQ9IjkwMCIKICAgd2lkdGg9IjkwMCIKICAgdmVyc2lvbj0iMS4wIj4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGExMCI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgICA8ZGM6dGl0bGU+PC9kYzp0aXRsZT4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGRlZnMKICAgICBpZD0iZGVmczExMzg0IiAvPgogIDxnCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLjkwMTAzMjU4LDAsMCwwLjkwMTAzMjU4LDExMi44NDA1OCwtMS45MDYwMTc3KSI+CiAgICA8ZwogICAgICAgaWQ9ImcxMTQ3NiI+CiAgICAgIDxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTE0NzIiCiAgICAgICAgIHN0eWxlPSJmb250LXN0eWxlOm5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zaXplOjEyMDEuOTI0OTI2NzZweDtmb250LWZhbWlseTonQml0c3RyZWFtIFZlcmEgU2Fucyc7dGV4dC1hbGlnbjpjZW50ZXI7dGV4dC1hbmNob3I6bWlkZGxlO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICAgIGQ9Ik0gNDc0LjUwODg4LDcxOC4yMjg0MSBIIDMwMy40OTU0NyB2IC0yMi4zMDEzNCBjIC0yLjRlLTQsLTM3Ljk1MTA4IDQuMzAzNTIsLTY4Ljc2MjExIDEyLjkxMTMsLTkyLjQzMzE5IDguNjA3MjgsLTIzLjY3MDMyIDIzLjYzMzUyLC00NS4yODY5NSA0MC42NTMyNCwtNjQuODQ5OTYgMTcuMDE5MTQsLTE5LjU2MjExIDQxLjk4NzM0LC0yNi4zMzI2NCAxMDEuNDU3OTMsLTc1LjYzMDg1IDMxLjY5MDk1LC0yNS44MjIwMyA1NS4yODEzLC03Ny4xNTIzIDU1LjI4MTc1LC05OC42NzE3NCAyLjIxMjMyLC01Ni45MjI0NSAtMTMuOTM5ODMsLTc5LjM0MjIgLTM0LjU2Mjg3LC05OS45NjUyNCAtMjIuNjczNTUsLTE5LjY3NzE3IC02MC42NzAyNywtMzAuMDY5OTggLTkwLjk5ODkyLC0zMC4wNjk5OCAtMjcuNzc5MjEsNi45ZS00IC02OC40NjczNSw4LjA4ODcxIC04Ny43NjY2LDI1LjM3MDQ3IC0yNS45MzgxNywxNy4yODMwOCAtNjUuMjM3NDcsNzMuNzA2MTEgLTU3LjA0Njg3LDEzMC41NDU3NyBsIC0xOTQuNTE2OTQzLDEuNzAyMjIgYyAwLC0xNTcuMjEzOTkgMjkuMzkzNjk5LC0xOTguNjk0NjUgOTkuMDA0MTEzLC0yNjMuMDMwMzIgNjcuMzk3MzksLTU0LjM3NjY0MyAxMjYuNTMxMjgsLTczLjI2ODM2NSAyNDMuODQ3NTcsLTczLjI2ODM2NSA4OS43MTc5MSwwIDE2MS44OTcyOCwxNy44MDI4MSAyMTQuMzI1NTIsNTMuNDA1ODU1IDcxLjIwNzE0LDQ4LjEyNDcyIDEyMi4zMDEwNSwxMTEuMTgzNTQgMTIyLjMwMTA1LDIzMC4xMTI4MSAtNi45ZS00LDQ0LjMyMDgxIC0xOS4xNTI1Myw5MC43ODYzOCAtNDMuMDcyNiwxMjguMzMyOTkgLTE4LjM4OTQ3LDMwLjkwOTM4IC02MC4zNzUxMSw2Ni40NTIzNiAtMTE4LjIxMjM3LDEwNC40MTYyOCAtNDIuODM2MDcsMjUuNzY4NiAtNjYuNjcxOTYsNTMuMTE5MjYgLTc3LjAzOTY0LDcyLjA5NDYgLTEwLjM2ODYzLDE4Ljk3NjAzIC0xNS41NTI3MSw0My43MjI2NyAtMTUuNTUyMjUsNzQuMjM5OTkgeiIgLz4KICAgICAgPHBhdGgKICAgICAgICAgaWQ9InBhdGgxMTQ3NCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWNhcDpzcXVhcmU7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMS4xMDYzODMsLTUuNTMxOTE0OSkiCiAgICAgICAgIGQ9Im0gNDgyLjM4Mjk4LDg2OS44MDkwMiBhIDk0LjA0MjU1Nyw3My4wMjEyNzggMCAxIDEgLTE4OC4wODUxMSwwIDk0LjA0MjU1Nyw3My4wMjEyNzggMCAxIDEgMTg4LjA4NTExLDAgeiIgLz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPgo=", - title: "Bookcase", + title: {render: "Bookcase"}, description: "A small, public cabinet with books. Anyone can leave or take a book", minzoom: 12, color: "#0000ff", overpassTags: "amenity=public_bookcase", presets: [ { + title: "bookcase" // icon: optional. Uses the layer icon by default // title: optional. Uses the layer title by default // description: optional. Uses the layer description by default @@ -55,7 +115,7 @@ export class CustomLayoutFromJSON { ] } - public static exampleLayout = { + public static exampleLayout: LayoutConfigJson = { name: "bookcases", title: "Custom Open bookcases map", description: "Welcome to a custom layout", @@ -84,7 +144,7 @@ export class CustomLayoutFromJSON { } let freeform = undefined; - if (json.key !== undefined && json.render !== undefined) { + if (json.key !== undefined && json.key !== "" && json.render !== undefined) { const type = json.type ?? "text"; freeform = { key: json.key, @@ -142,10 +202,11 @@ export class CustomLayoutFromJSON { }; } - private static TagFromJson(json: any): Tag { + private static TagFromJson(json: string | { k: string, v: string }): Tag { if (json === undefined) { return undefined; } + console.log(json) if (typeof (json) === "string") { const kv = json.split("="); return new Tag(kv[0].trim(), kv[1].trim()); @@ -153,8 +214,8 @@ export class CustomLayoutFromJSON { return new Tag(json.k.trim(), json.v.trim()) } - private static TagsFromJson(json: any): Tag[] { - if (json === undefined) { + private static TagsFromJson(json: string | { k: string, v: string }[]): Tag[] { + if (json === undefined || json === "") { return undefined; } if (typeof (json) === "string") { diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index c7dfb06..f8a922e 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -21,11 +21,10 @@ export class OsmConnection { public userDetails: UIEventSource; private _dryRun: boolean; - constructor(dryRun: boolean, oauth_token: UIEventSource) { + constructor(dryRun: boolean, oauth_token: UIEventSource, singlePage: boolean = true) { let pwaStandAloneMode = false; try { - if (window.matchMedia('(display-mode: standalone)').matches || window.matchMedia('(display-mode: fullscreen)').matches) { pwaStandAloneMode = true; } @@ -36,7 +35,7 @@ export class OsmConnection { const iframeMode = window !== window.top; - if ( iframeMode) { + if ( iframeMode || !singlePage) { // In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway... // Same for an iframe... this.auth = new osmAuth({ diff --git a/State.ts b/State.ts index e865444..734873d 100644 --- a/State.ts +++ b/State.ts @@ -24,7 +24,7 @@ export class State { // The singleton of the global state public static state: State; - public static vNumber = "0.0.3"; + public static vNumber = "0.0.4"; public static runningFromConsole: boolean = false; @@ -32,6 +32,7 @@ export class State { THe layout to use */ public readonly layoutToUse = new UIEventSource(undefined); + public layoutDefinition : string; /** The mapping from id -> UIEventSource diff --git a/UI/ShareScreen.ts b/UI/ShareScreen.ts index 1e3088a..eaccc84 100644 --- a/UI/ShareScreen.ts +++ b/UI/ShareScreen.ts @@ -22,6 +22,7 @@ export class ShareScreen extends UIElement { private _iframeCode: UIElement; private _link: UIElement; private _linkStatus: UIElement; + private _editLayout: UIElement; constructor() { super(undefined) @@ -129,11 +130,16 @@ export class ShareScreen extends UIElement { const parts = Utils.NoNull(optionParts.map((eventSource) => eventSource.data)); - if (parts.length === 0) { - return literalText; + let hash = ""; + if (State.state.layoutDefinition !== undefined) { + hash = ("#" + State.state.layoutDefinition) } - return literalText + "?" + parts.join("&"); + if (parts.length === 0) { + return literalText + hash; + } + + return literalText + "?" + parts.join("&") + hash; }, optionParts); this._iframeCode = new VariableUiElement( url.map((url) => { @@ -150,6 +156,13 @@ export class ShareScreen extends UIElement { }) ); + this._editLayout = new FixedUiElement(""); + if(State.state.layoutDefinition !== undefined){ + this._editLayout = + new FixedUiElement(`

Edit this theme

`+ + `Click here to edit`) + + } const status = new UIEventSource(" "); this._linkStatus = new VariableUiElement(status); @@ -200,7 +213,8 @@ export class ShareScreen extends UIElement { tr.addToHomeScreen, tr.embedIntro, this._options, - this._iframeCode + this._iframeCode, + this._editLayout ]).Render() } diff --git a/customGenerator.html b/customGenerator.html new file mode 100644 index 0000000..9dfcfd5 --- /dev/null +++ b/customGenerator.html @@ -0,0 +1,58 @@ + + + + + Custom Theme Generator for Mapcomplete + + + + + + +
+

Custom theme generator

+ + Welcome to the custom theme creator.
+ + In order to use this theme generator, you need at least 500 changesets.
+ + As the spirit of mapcomplete is to not have any kind of hosted backend, the custom themes are encoded in the + URL: + the full configuration is saved in a JSON, which is base64-encoded and appended to the hash of the URL.
+ + This means that closing this page removes your theme.
+ +
'loggedIn' not attached
+
+
+
'preview' not attached
+ + + \ No newline at end of file diff --git a/customGenerator.ts b/customGenerator.ts new file mode 100644 index 0000000..5c8309b --- /dev/null +++ b/customGenerator.ts @@ -0,0 +1,20 @@ +import {OsmConnection, UserDetails} from "./Logic/Osm/OsmConnection"; +import {UIEventSource} from "./UI/UIEventSource"; +import {VariableUiElement} from "./UI/Base/VariableUIElement"; +import {Preview, ThemeGenerator} from "./themeGenerator"; + +const connection = new OsmConnection(true, new UIEventSource(undefined), false); +connection.AttemptLogin(); + +new VariableUiElement(connection.userDetails.map((userdetails : UserDetails) => { + if(userdetails.loggedIn){ + return "Logged in as "+userdetails.name + }else{ + return "Not logged in" + } +})).AttachTo("loggedIn").onClick(() => connection.LogOut()); + +const themeGenerator = new ThemeGenerator(connection, window.location.hash?.substr(1)); +themeGenerator.AttachTo("layoutCreator") + +new Preview(themeGenerator.url).AttachTo("preview"); \ No newline at end of file diff --git a/index.ts b/index.ts index 457423e..14a0c55 100644 --- a/index.ts +++ b/index.ts @@ -70,7 +70,7 @@ let layoutToUse: Layout = AllKnownLayouts.allSets[defaultLayout] ?? AllKnownLayo const layoutFromBase64 = QueryParameters.GetQueryParameter("userlayout", "false").data; -if(layoutFromBase64 === "true"){ +if(layoutFromBase64 !== "false"){ layoutToUse = CustomLayoutFromJSON.FromQueryParam(hash.substr(1)); } @@ -86,6 +86,9 @@ console.log("Using layout: ", layoutToUse.name); TagRendering.injectFunction(); State.state = new State(layoutToUse); +if(layoutFromBase64 !== "false"){ + State.state.layoutDefinition = hash.substr(1); +} InitUiElements.InitBaseMap(); new FixedUiElement("").AttachTo("decoration"); // Remove the decoration diff --git a/themeGenerator.ts b/themeGenerator.ts new file mode 100644 index 0000000..df9176d --- /dev/null +++ b/themeGenerator.ts @@ -0,0 +1,398 @@ +import {UIElement} from "./UI/UIElement"; +import {OsmConnection, UserDetails} from "./Logic/Osm/OsmConnection"; +import {UIEventSource} from "./UI/UIEventSource"; +import Combine from "./UI/Base/Combine"; +import {TextField} from "./UI/Input/TextField"; +import {VariableUiElement} from "./UI/Base/VariableUIElement"; +import {VerticalCombine} from "./UI/Base/VerticalCombine"; +import {FixedUiElement} from "./UI/Base/FixedUiElement"; +import {TabbedComponent} from "./UI/Base/TabbedComponent"; +import {LayerConfigJson, LayoutConfigJson, TagRenderingConfigJson} from "./Customizations/JSON/CustomLayoutFromJSON"; +import {Button} from "./UI/Base/Button"; +import {type} from "os"; +import {Tag} from "./Logic/TagsFilter"; + +function TagsToString(tags: string | string [] | { k: string, v: string }[]) { + if (tags === undefined) { + return undefined; + } + if (typeof (tags) == "string") { + return tags; + } + const newTags = []; + console.log(tags) + for (const tag of tags) { + if (typeof (tag) == "string") { + newTags.push(tag) + } else { + newTags.push(tag.k + "=" + tag.v); + } + } + return newTags.join(","); +} + +export class Preview extends UIElement { + private url: UIEventSource; + + constructor(url: UIEventSource) { + super(url); + this.url = url; + } + + InnerRender(): string { + const url = this.url.data; + return ""; // `` + } + +} + +class MappingGenerator extends UIElement { + + private elements: UIElement[]; + + constructor(fullConfig: UIEventSource, + layerConfig: LayerConfigJson, + tagRendering: TagRenderingConfigJson, + mapping: { if: string | string[] | { k: string, v: string }[] }, + generateField: (src: UIEventSource, label: string, key: string, root: any, deflt?: string) => UIElement) { + super(undefined); + this.CreateElements(fullConfig, layerConfig, tagRendering, mapping, generateField) + } + + private CreateElements(fullConfig: UIEventSource, layerConfig: LayerConfigJson, + tagRendering: TagRenderingConfigJson, + mapping, + generateField: (src: UIEventSource, label: string, key: string, root: any, deflt?: string) => UIElement) { + { + const self = this; + this.elements = [ + new FixedUiElement("
Mapping
"), + generateField(fullConfig, "If these tags apply", "if", mapping), + generateField(fullConfig, "Then: show this text", "then", mapping), + new Button("Remove this mapping", () => { + for (let i = 0; i < tagRendering.mappings.length; i++) { + if (tagRendering.mappings[i] === mapping) { + tagRendering.mappings.splice(i, 1); + self.elements = [ + new FixedUiElement("Tag mapping removed") + ] + self.Update(); + break; + } + } + }) + ]; + } + } + + InnerRender(): string { + const combine = new VerticalCombine(this.elements); + combine.clss = "bordered"; + return combine.Render(); + } +} + +class TagRenderingGenerator + extends UIElement { + + private elements: UIElement[]; + + constructor(fullConfig: UIEventSource, + layerConfig: LayerConfigJson, + tagRendering: TagRenderingConfigJson, + generateField: (src: UIEventSource, label: string, key: string, root: any, deflt?: string) => UIElement, + isTitle: boolean = false) { + super(undefined); + this.CreateElements(fullConfig, layerConfig, tagRendering, generateField, isTitle) + } + + private CreateElements(fullConfig: UIEventSource, layerConfig: LayerConfigJson, tagRendering: TagRenderingConfigJson, generateField: (src: UIEventSource, label: string, key: string, root: any, deflt?: string) => UIElement, isTitle: boolean) { + + + const self = this; + this.elements = [ + new FixedUiElement(isTitle ? "

Popup title

" : "

TagRendering/TagQuestion

"), + generateField(fullConfig, "Key", "key", tagRendering), + generateField(fullConfig, "Rendering", "render", tagRendering), + generateField(fullConfig, "Type", "type", tagRendering), + generateField(fullConfig, "Question", "question", tagRendering), + generateField(fullConfig, "Extra tags", "addExtraTags", tagRendering), + + ...(tagRendering.mappings ?? []).map((mapping) => { + return new MappingGenerator(fullConfig, layerConfig, tagRendering, mapping, + generateField) + }), + new Button("Add mapping", () => { + tagRendering.mappings.push({if: "", then: ""}); + self.CreateElements(fullConfig, layerConfig, tagRendering, generateField, isTitle); + self.Update(); + }) + + ] + + if (!isTitle) { + const b = new Button("Remove this preset", () => { + for (let i = 0; i < layerConfig.tagRenderings.length; i++) { + if (layerConfig.tagRenderings[i] === tagRendering) { + layerConfig.tagRenderings.splice(i, 1); + self.elements = [ + new FixedUiElement("Tag rendering removed") + ] + self.Update(); + break; + } + } + }); + this.elements.push(b); + } + + } + + InnerRender(): string { + const combine = new VerticalCombine(this.elements); + combine.clss = "bordered"; + return combine.Render(); + } +} + +class PresetGenerator extends UIElement { + + private elements: UIElement[]; + + constructor(fullConfig: UIEventSource, layerConfig: LayerConfigJson, + preset0: { title?: string, description?: string, icon?: string, tags?: string | string[] | { k: string, v: string }[] }, + generateField: (src: UIEventSource, label: string, key: string, root: any, deflt?: string) => UIElement) { + super(undefined); + const self = this; + this.elements = [ + new FixedUiElement("

Preset

"), + generateField(fullConfig, "Title", "title", preset0), + generateField(fullConfig, "Description", "description", preset0, layerConfig.description), + generateField(fullConfig, "icon", "icon", preset0, layerConfig.icon), + generateField(fullConfig, "tags", "tags", preset0, TagsToString(layerConfig.overpassTags)), + new Button("Remove this preset", () => { + for (let i = 0; i < layerConfig.presets.length; i++) { + if (layerConfig.presets[i] === preset0) { + layerConfig.presets.splice(i, 1); + self.elements = [ + new FixedUiElement("Preset removed") + ] + self.Update(); + break; + } + } + }) + ] + + } + + InnerRender(): string { + const combine = new VerticalCombine(this.elements); + combine.clss = "bordered"; + return combine.Render(); + } + +} + +class LayerGenerator extends UIElement { + private fullConfig: UIEventSource; + private layerConfig: UIEventSource; + private generateField: ((label: string, key: string, root: any, deflt?: string) => UIElement); + private uielements: UIElement[]; + + constructor(fullConfig: UIEventSource, + layerConfig: LayerConfigJson, + generateField: ((src: UIEventSource, label: string, key: string, root: any, deflt?: string) => UIElement)) { + super(undefined); + this.layerConfig = new UIEventSource(layerConfig); + this.fullConfig = fullConfig; + this.CreateElements(fullConfig, layerConfig, generateField) + + } + + private CreateElements(fullConfig: UIEventSource, layerConfig: LayerConfigJson, generateField: (src: UIEventSource, label: string, key: string, root: any, deflt?: string) => UIElement) { + const self = this; + this.uielements = [ + generateField(fullConfig, "id", "id", layerConfig), + generateField(fullConfig, "The title of this layer", "title", layerConfig), + generateField(fullConfig, "A description of objects for this layer", "description", layerConfig), + generateField(fullConfig, "The icon of this layer, either a URL or a base64-encoded svg", "icon", layerConfig), + generateField(fullConfig, "The default stroke color", "color", layerConfig), + generateField(fullConfig, "The minimal needed zoom to start loading", "minzoom", layerConfig), + generateField(fullConfig, "The tags to load from overpass", "overpassTags", layerConfig), + ...layerConfig.presets.map(preset => new PresetGenerator(fullConfig, layerConfig, preset, generateField)), + new Button("Add a preset", () => { + layerConfig.presets.push({ + icon: undefined, + title: "", + description: "", + tags: TagsToString(layerConfig.overpassTags) + }); + self.CreateElements(fullConfig, layerConfig, generateField); + self.Update(); + }), + new TagRenderingGenerator(fullConfig, layerConfig, layerConfig.title, generateField, true), + ...layerConfig.tagRenderings.map(tr => new TagRenderingGenerator(fullConfig, layerConfig, tr, generateField)), + new Button("Add a tag rendering", () => { + layerConfig.tagRenderings.push({ + key: "", + addExtraTags: "", + mappings: [], + question: "", + render: "", + type: "text" + }); + self.CreateElements(fullConfig, layerConfig, generateField); + self.Update(); + }), + + ] + } + + InnerRender(): string { + return new VerticalCombine(this.uielements).Render(); + } +} + + +class AllLayerComponent extends UIElement { + + private tabs: TabbedComponent; + private config: UIEventSource; + private generateField: ((src: UIEventSource, label: string, key: string, root: any, deflt?: string) => UIElement); + + constructor(config: UIEventSource, generateField: ((src: UIEventSource, label: string, key: string, root: any, deflt?: string) => UIElement)) { + super(undefined); + this.generateField = generateField; + this.config = config; + const self = this; + let previousLayerAmount = config.data.layers.length; + config.addCallback((data) => { + if (data.layers.length != previousLayerAmount) { + previousLayerAmount = data.layers.length; + self.UpdateTabs(); + self.Update(); + } + }); + this.UpdateTabs(); + + } + + private UpdateTabs() { + const layerPanes: { header: UIElement | string, content: UIElement | string }[] = []; + const config = this.config; + for (const layer of this.config.data.layers) { + const header = this.config.map(() => { + return `` + }); + layerPanes.push({ + header: new VariableUiElement(header), + content: new LayerGenerator(config, layer, this.generateField) + }) + } + + + layerPanes.push({ + header: "", + content: new Button("Add a new layer", () => { + config.data.layers.push({ + id: "", + title: { + render: "Title" + }, + icon: "./assets/bug.svg", + color: "", + description: "", + minzoom: 12, + overpassTags: "", + presets: [{}], + tagRenderings: [] + }); + + config.ping(); + }) + }) + + this.tabs = new TabbedComponent(layerPanes); + } + + InnerRender(): string { + return this.tabs.Render(); + } + +} + + +export class ThemeGenerator extends UIElement { + + private readonly userDetails: UIEventSource; + + private readonly themeObject: UIEventSource; + private readonly allQuestionFields: UIElement[]; + public url: UIEventSource; + + + constructor(connection: OsmConnection, windowHash) { + super(connection.userDetails); + this.userDetails = connection.userDetails; + + const defaultTheme = {layers: [], icon: "./assets/bug.svg"}; + let loadedTheme = undefined; + if (windowHash !== undefined && windowHash.length > 4) { + loadedTheme = JSON.parse(atob(windowHash)); + } + this.themeObject = new UIEventSource(loadedTheme ?? defaultTheme); + const jsonObjectRoot = this.themeObject.data; + + const base64 = this.themeObject.map(JSON.stringify).map(btoa); + this.url = base64.map((data) => `${window.location.origin}/index.html?userlayout=true#` + data); + const self = this; + this.allQuestionFields = [ + this.JsonField(this.themeObject, "Name of this theme", "name", jsonObjectRoot), + this.JsonField(this.themeObject, "Title (shown in the window and in the welcome message)", "title", jsonObjectRoot), + this.JsonField(this.themeObject, "Description (shown in the welcome message and various other places)", "description", jsonObjectRoot), + this.JsonField(this.themeObject, "The supported language", "language", jsonObjectRoot), + this.JsonField(this.themeObject, "startLat", "startLat", jsonObjectRoot), + this.JsonField(this.themeObject, "startLon", "startLon", jsonObjectRoot), + this.JsonField(this.themeObject, "startzoom", "startZoom", jsonObjectRoot), + this.JsonField(this.themeObject, "icon: either a URL to an image file, a relative url to a MapComplete asset ('./asset/help.svg') or a base64-encoded value (including 'data:image/svg+xml;base64,'", "icon", jsonObjectRoot, "./assets/bug.svg"), + + new AllLayerComponent(this.themeObject, self.JsonField) + ] + + + } + + + private JsonField(themeObject: UIEventSource, label: string, key: string, root: any, deflt: string = "") { + const value = new UIEventSource(TagsToString(root[key]) ?? deflt); + value.addCallback((v) => { + root[key] = v; + themeObject.ping(); // We assume the root is a part of the themeObject + }) + return new Combine([ + label, + new TextField({ + fromString: (str) => str, + toString: (str) => str, + value: value + })]); + } + + InnerRender(): string { + + if (!this.userDetails.data.loggedIn) { + return "Not logged in" + } + if (this.userDetails.data.csCount < 500) { + return "You need at least 500 changesets to create your own theme"; + } + + + return new VerticalCombine([ + // new VariableUiElement(this.themeObject.map(JSON.stringify)), + new VariableUiElement(this.url.map((url) => `Current URL: Click here to open`)), + ...this.allQuestionFields, + ]).Render(); + } +} \ No newline at end of file