forked from MapComplete/MapComplete
		
	Add custom theme generator
This commit is contained in:
		
							parent
							
								
									14930e2f93
								
							
						
					
					
						commit
						8d3c8ed9d9
					
				
					 8 changed files with 570 additions and 16 deletions
				
			
		|  | @ -10,18 +10,78 @@ import FixedText from "../Questions/FixedText"; | ||||||
| import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload"; | 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 { | export class CustomLayoutFromJSON { | ||||||
| 
 | 
 | ||||||
|     public static exampleLayer = { |     public static exampleLayer: LayerConfigJson = { | ||||||
|         id: "bookcase", |         id: "bookcase", | ||||||
|         icon: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgaWQ9InN2ZzExMzgyIgogICBoZWlnaHQ9IjkwMCIKICAgd2lkdGg9IjkwMCIKICAgdmVyc2lvbj0iMS4wIj4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGExMCI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgICA8ZGM6dGl0bGU+PC9kYzp0aXRsZT4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGRlZnMKICAgICBpZD0iZGVmczExMzg0IiAvPgogIDxnCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLjkwMTAzMjU4LDAsMCwwLjkwMTAzMjU4LDExMi44NDA1OCwtMS45MDYwMTc3KSI+CiAgICA8ZwogICAgICAgaWQ9ImcxMTQ3NiI+CiAgICAgIDxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTE0NzIiCiAgICAgICAgIHN0eWxlPSJmb250LXN0eWxlOm5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zaXplOjEyMDEuOTI0OTI2NzZweDtmb250LWZhbWlseTonQml0c3RyZWFtIFZlcmEgU2Fucyc7dGV4dC1hbGlnbjpjZW50ZXI7dGV4dC1hbmNob3I6bWlkZGxlO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICAgIGQ9Ik0gNDc0LjUwODg4LDcxOC4yMjg0MSBIIDMwMy40OTU0NyB2IC0yMi4zMDEzNCBjIC0yLjRlLTQsLTM3Ljk1MTA4IDQuMzAzNTIsLTY4Ljc2MjExIDEyLjkxMTMsLTkyLjQzMzE5IDguNjA3MjgsLTIzLjY3MDMyIDIzLjYzMzUyLC00NS4yODY5NSA0MC42NTMyNCwtNjQuODQ5OTYgMTcuMDE5MTQsLTE5LjU2MjExIDQxLjk4NzM0LC0yNi4zMzI2NCAxMDEuNDU3OTMsLTc1LjYzMDg1IDMxLjY5MDk1LC0yNS44MjIwMyA1NS4yODEzLC03Ny4xNTIzIDU1LjI4MTc1LC05OC42NzE3NCAyLjIxMjMyLC01Ni45MjI0NSAtMTMuOTM5ODMsLTc5LjM0MjIgLTM0LjU2Mjg3LC05OS45NjUyNCAtMjIuNjczNTUsLTE5LjY3NzE3IC02MC42NzAyNywtMzAuMDY5OTggLTkwLjk5ODkyLC0zMC4wNjk5OCAtMjcuNzc5MjEsNi45ZS00IC02OC40NjczNSw4LjA4ODcxIC04Ny43NjY2LDI1LjM3MDQ3IC0yNS45MzgxNywxNy4yODMwOCAtNjUuMjM3NDcsNzMuNzA2MTEgLTU3LjA0Njg3LDEzMC41NDU3NyBsIC0xOTQuNTE2OTQzLDEuNzAyMjIgYyAwLC0xNTcuMjEzOTkgMjkuMzkzNjk5LC0xOTguNjk0NjUgOTkuMDA0MTEzLC0yNjMuMDMwMzIgNjcuMzk3MzksLTU0LjM3NjY0MyAxMjYuNTMxMjgsLTczLjI2ODM2NSAyNDMuODQ3NTcsLTczLjI2ODM2NSA4OS43MTc5MSwwIDE2MS44OTcyOCwxNy44MDI4MSAyMTQuMzI1NTIsNTMuNDA1ODU1IDcxLjIwNzE0LDQ4LjEyNDcyIDEyMi4zMDEwNSwxMTEuMTgzNTQgMTIyLjMwMTA1LDIzMC4xMTI4MSAtNi45ZS00LDQ0LjMyMDgxIC0xOS4xNTI1Myw5MC43ODYzOCAtNDMuMDcyNiwxMjguMzMyOTkgLTE4LjM4OTQ3LDMwLjkwOTM4IC02MC4zNzUxMSw2Ni40NTIzNiAtMTE4LjIxMjM3LDEwNC40MTYyOCAtNDIuODM2MDcsMjUuNzY4NiAtNjYuNjcxOTYsNTMuMTE5MjYgLTc3LjAzOTY0LDcyLjA5NDYgLTEwLjM2ODYzLDE4Ljk3NjAzIC0xNS41NTI3MSw0My43MjI2NyAtMTUuNTUyMjUsNzQuMjM5OTkgeiIgLz4KICAgICAgPHBhdGgKICAgICAgICAgaWQ9InBhdGgxMTQ3NCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWNhcDpzcXVhcmU7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMS4xMDYzODMsLTUuNTMxOTE0OSkiCiAgICAgICAgIGQ9Im0gNDgyLjM4Mjk4LDg2OS44MDkwMiBhIDk0LjA0MjU1Nyw3My4wMjEyNzggMCAxIDEgLTE4OC4wODUxMSwwIDk0LjA0MjU1Nyw3My4wMjEyNzggMCAxIDEgMTg4LjA4NTExLDAgeiIgLz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPgo=", |         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", |         description: "A small, public cabinet with books. Anyone can leave or take a book", | ||||||
|         minzoom: 12, |         minzoom: 12, | ||||||
|         color: "#0000ff", |         color: "#0000ff", | ||||||
|         overpassTags: "amenity=public_bookcase", |         overpassTags: "amenity=public_bookcase", | ||||||
|         presets: [ |         presets: [ | ||||||
|             { |             { | ||||||
|  |                 title: "bookcase" | ||||||
|                 // icon: optional. Uses the layer icon by default
 |                 // icon: optional. Uses the layer icon by default
 | ||||||
|                 // title: optional. Uses the layer title by default
 |                 // title: optional. Uses the layer title by default
 | ||||||
|                 // description: optional. Uses the layer description 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", |         name: "bookcases", | ||||||
|         title: "Custom Open bookcases map", |         title: "Custom Open bookcases map", | ||||||
|         description: "Welcome to a custom layout", |         description: "Welcome to a custom layout", | ||||||
|  | @ -84,7 +144,7 @@ export class CustomLayoutFromJSON { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let freeform = undefined; |         let freeform = undefined; | ||||||
|         if (json.key !== undefined && json.render !== undefined) { |         if (json.key !== undefined && json.key !== "" && json.render !== undefined) { | ||||||
|             const type = json.type ?? "text"; |             const type = json.type ?? "text"; | ||||||
|             freeform = { |             freeform = { | ||||||
|                 key: json.key, |                 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) { |         if (json === undefined) { | ||||||
|             return undefined; |             return undefined; | ||||||
|         } |         } | ||||||
|  |         console.log(json) | ||||||
|         if (typeof (json) === "string") { |         if (typeof (json) === "string") { | ||||||
|             const kv = json.split("="); |             const kv = json.split("="); | ||||||
|             return new Tag(kv[0].trim(), kv[1].trim()); |             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()) |         return new Tag(json.k.trim(), json.v.trim()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static TagsFromJson(json: any): Tag[] { |     private static TagsFromJson(json: string | { k: string, v: string }[]): Tag[] { | ||||||
|         if (json === undefined) { |         if (json === undefined || json === "") { | ||||||
|             return undefined; |             return undefined; | ||||||
|         } |         } | ||||||
|         if (typeof (json) === "string") { |         if (typeof (json) === "string") { | ||||||
|  |  | ||||||
|  | @ -21,11 +21,10 @@ export class OsmConnection { | ||||||
|     public userDetails: UIEventSource<UserDetails>; |     public userDetails: UIEventSource<UserDetails>; | ||||||
|     private _dryRun: boolean; |     private _dryRun: boolean; | ||||||
| 
 | 
 | ||||||
|     constructor(dryRun: boolean, oauth_token: UIEventSource<string>) { |     constructor(dryRun: boolean, oauth_token: UIEventSource<string>, singlePage: boolean = true) { | ||||||
| 
 | 
 | ||||||
|         let pwaStandAloneMode = false; |         let pwaStandAloneMode = false; | ||||||
|         try { |         try { | ||||||
| 
 |  | ||||||
|             if (window.matchMedia('(display-mode: standalone)').matches || window.matchMedia('(display-mode: fullscreen)').matches) { |             if (window.matchMedia('(display-mode: standalone)').matches || window.matchMedia('(display-mode: fullscreen)').matches) { | ||||||
|                 pwaStandAloneMode = true; |                 pwaStandAloneMode = true; | ||||||
|             } |             } | ||||||
|  | @ -36,7 +35,7 @@ export class OsmConnection { | ||||||
|         const iframeMode = window !== window.top; |         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...
 |             // In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
 | ||||||
|             // Same for an iframe...
 |             // Same for an iframe...
 | ||||||
|             this.auth = new osmAuth({ |             this.auth = new osmAuth({ | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										3
									
								
								State.ts
									
										
									
									
									
								
							|  | @ -24,7 +24,7 @@ export class State { | ||||||
|     // The singleton of the global state
 |     // The singleton of the global state
 | ||||||
|     public static state: State; |     public static state: State; | ||||||
|      |      | ||||||
|     public static vNumber = "0.0.3"; |     public static vNumber = "0.0.4"; | ||||||
| 
 | 
 | ||||||
|     public static runningFromConsole: boolean = false;  |     public static runningFromConsole: boolean = false;  | ||||||
| 
 | 
 | ||||||
|  | @ -32,6 +32,7 @@ export class State { | ||||||
|      THe layout to use |      THe layout to use | ||||||
|      */ |      */ | ||||||
|     public readonly layoutToUse = new UIEventSource<Layout>(undefined); |     public readonly layoutToUse = new UIEventSource<Layout>(undefined); | ||||||
|  |     public layoutDefinition : string; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      The mapping from id -> UIEventSource<properties> |      The mapping from id -> UIEventSource<properties> | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ export class ShareScreen extends UIElement { | ||||||
|     private _iframeCode: UIElement; |     private _iframeCode: UIElement; | ||||||
|     private _link: UIElement; |     private _link: UIElement; | ||||||
|     private _linkStatus: UIElement; |     private _linkStatus: UIElement; | ||||||
|  |     private _editLayout: UIElement; | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(undefined) |         super(undefined) | ||||||
|  | @ -129,11 +130,16 @@ export class ShareScreen extends UIElement { | ||||||
| 
 | 
 | ||||||
|             const parts = Utils.NoNull(optionParts.map((eventSource) => eventSource.data)); |             const parts = Utils.NoNull(optionParts.map((eventSource) => eventSource.data)); | ||||||
| 
 | 
 | ||||||
|             if (parts.length === 0) { |             let hash = ""; | ||||||
|                 return literalText; |             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); |         }, optionParts); | ||||||
|         this._iframeCode = new VariableUiElement( |         this._iframeCode = new VariableUiElement( | ||||||
|             url.map((url) => { |             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(`<h3>Edit this theme</h3>`+ | ||||||
|  |                     `<a target='_blank' https://pietervdvn.github.io/MapComplete/customGenerator.html#${State.state.layoutDefinition}'>Click here to edit</a>`) | ||||||
|  |              | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         const status = new UIEventSource(" "); |         const status = new UIEventSource(" "); | ||||||
|         this._linkStatus = new VariableUiElement(status); |         this._linkStatus = new VariableUiElement(status); | ||||||
|  | @ -200,7 +213,8 @@ export class ShareScreen extends UIElement { | ||||||
|             tr.addToHomeScreen, |             tr.addToHomeScreen, | ||||||
|             tr.embedIntro, |             tr.embedIntro, | ||||||
|             this._options, |             this._options, | ||||||
|             this._iframeCode |             this._iframeCode, | ||||||
|  |             this._editLayout | ||||||
|         ]).Render() |         ]).Render() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										58
									
								
								customGenerator.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								customGenerator.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,58 @@ | ||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  |     <link href="index.css" rel="stylesheet"/> | ||||||
|  |     <title>Custom Theme Generator for Mapcomplete</title> | ||||||
|  | 
 | ||||||
|  |     <style type="text/css"> | ||||||
|  |         #maindiv { | ||||||
|  |             position: absolute; | ||||||
|  |             width: 50vw; | ||||||
|  |             height: 100%; | ||||||
|  |             left: 0; | ||||||
|  |             top: 0; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         #preview { | ||||||
|  |             position: absolute; | ||||||
|  |             width: 50vw; | ||||||
|  |             height: 100vh; | ||||||
|  |             right: 0; | ||||||
|  |             top: 0; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .bordered { | ||||||
|  |             border: 1px solid black; | ||||||
|  |             display:block; | ||||||
|  |             padding: 0.5em; | ||||||
|  |             border-radius: 0.5em; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         body { | ||||||
|  |             height: 100%; | ||||||
|  |         } | ||||||
|  |     </style> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  | <div id="maindiv"> | ||||||
|  |     <h1>Custom theme generator</h1> | ||||||
|  | 
 | ||||||
|  |     Welcome to the custom theme creator.<br/> | ||||||
|  | 
 | ||||||
|  |     In order to use this theme generator, you need at least 500 changesets.<br/> | ||||||
|  | 
 | ||||||
|  |     As the spirit of mapcomplete is to not have <b>any</b> 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.<br/> | ||||||
|  | 
 | ||||||
|  |     This means that <b>closing this page removes your theme</b>.</br> | ||||||
|  | 
 | ||||||
|  |     <div id="loggedIn">'loggedIn' not attached</div> | ||||||
|  |     <div id="layoutCreator"></div> | ||||||
|  | </div> | ||||||
|  | <div id="preview">'preview' not attached</div> | ||||||
|  | <script src="./customGenerator.ts"></script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										20
									
								
								customGenerator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								customGenerator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<string>(undefined), false); | ||||||
|  | connection.AttemptLogin(); | ||||||
|  | 
 | ||||||
|  | new VariableUiElement(connection.userDetails.map<string>((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"); | ||||||
							
								
								
									
										5
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										5
									
								
								index.ts
									
										
									
									
									
								
							|  | @ -70,7 +70,7 @@ let layoutToUse: Layout = AllKnownLayouts.allSets[defaultLayout] ?? AllKnownLayo | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| const layoutFromBase64 = QueryParameters.GetQueryParameter("userlayout", "false").data; | const layoutFromBase64 = QueryParameters.GetQueryParameter("userlayout", "false").data; | ||||||
| if(layoutFromBase64 === "true"){ | if(layoutFromBase64 !== "false"){ | ||||||
|     layoutToUse = CustomLayoutFromJSON.FromQueryParam(hash.substr(1)); |     layoutToUse = CustomLayoutFromJSON.FromQueryParam(hash.substr(1)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -86,6 +86,9 @@ console.log("Using layout: ", layoutToUse.name); | ||||||
| 
 | 
 | ||||||
| TagRendering.injectFunction(); | TagRendering.injectFunction(); | ||||||
| State.state = new State(layoutToUse); | State.state = new State(layoutToUse); | ||||||
|  | if(layoutFromBase64 !== "false"){ | ||||||
|  |     State.state.layoutDefinition = hash.substr(1); | ||||||
|  | } | ||||||
| InitUiElements.InitBaseMap(); | InitUiElements.InitBaseMap(); | ||||||
| 
 | 
 | ||||||
| new FixedUiElement("").AttachTo("decoration"); // Remove the decoration
 | new FixedUiElement("").AttachTo("decoration"); // Remove the decoration
 | ||||||
|  |  | ||||||
							
								
								
									
										398
									
								
								themeGenerator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										398
									
								
								themeGenerator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<string>; | ||||||
|  | 
 | ||||||
|  |     constructor(url: UIEventSource<string>) { | ||||||
|  |         super(url); | ||||||
|  |         this.url = url; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     InnerRender(): string { | ||||||
|  |         const url = this.url.data; | ||||||
|  |         return ""; // `<iframe src="${url}" width="100%" height="100%" title="Test"></iframe>`
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class MappingGenerator extends UIElement { | ||||||
|  | 
 | ||||||
|  |     private elements: UIElement[]; | ||||||
|  | 
 | ||||||
|  |     constructor(fullConfig: UIEventSource<LayoutConfigJson>, | ||||||
|  |                 layerConfig: LayerConfigJson, | ||||||
|  |                 tagRendering: TagRenderingConfigJson, | ||||||
|  |                 mapping: { if: string | string[] | { k: string, v: string }[] }, | ||||||
|  |                 generateField: (src: UIEventSource<any>, label: string, key: string, root: any, deflt?: string) => UIElement) { | ||||||
|  |         super(undefined); | ||||||
|  |         this.CreateElements(fullConfig, layerConfig, tagRendering, mapping, generateField) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson, | ||||||
|  |                            tagRendering: TagRenderingConfigJson, | ||||||
|  |                            mapping, | ||||||
|  |                            generateField: (src: UIEventSource<any>, label: string, key: string, root: any, deflt?: string) => UIElement) { | ||||||
|  |         { | ||||||
|  |             const self = this; | ||||||
|  |             this.elements = [ | ||||||
|  |                 new FixedUiElement("<h5>Mapping</h5>"), | ||||||
|  |                 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<LayoutConfigJson>, | ||||||
|  |                 layerConfig: LayerConfigJson, | ||||||
|  |                 tagRendering: TagRenderingConfigJson, | ||||||
|  |                 generateField: (src: UIEventSource<any>, 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<LayoutConfigJson>, layerConfig: LayerConfigJson, tagRendering: TagRenderingConfigJson, generateField: (src: UIEventSource<any>, label: string, key: string, root: any, deflt?: string) => UIElement, isTitle: boolean) { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         const self = this; | ||||||
|  |         this.elements = [ | ||||||
|  |             new FixedUiElement(isTitle ? "<h3>Popup title</h3>" : "<h3>TagRendering/TagQuestion</h3>"), | ||||||
|  |             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<LayoutConfigJson>, layerConfig: LayerConfigJson, | ||||||
|  |                 preset0: { title?: string, description?: string, icon?: string, tags?: string | string[] | { k: string, v: string }[] }, | ||||||
|  |                 generateField: (src: UIEventSource<any>, label: string, key: string, root: any, deflt?: string) => UIElement) { | ||||||
|  |         super(undefined); | ||||||
|  |         const self = this; | ||||||
|  |         this.elements = [ | ||||||
|  |             new FixedUiElement("<h3>Preset</h3>"), | ||||||
|  |             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<LayoutConfigJson>; | ||||||
|  |     private layerConfig: UIEventSource<LayerConfigJson>; | ||||||
|  |     private generateField: ((label: string, key: string, root: any, deflt?: string) => UIElement); | ||||||
|  |     private uielements: UIElement[]; | ||||||
|  | 
 | ||||||
|  |     constructor(fullConfig: UIEventSource<LayoutConfigJson>, | ||||||
|  |                 layerConfig: LayerConfigJson, | ||||||
|  |                 generateField: ((src: UIEventSource<any>, label: string, key: string, root: any, deflt?: string) => UIElement)) { | ||||||
|  |         super(undefined); | ||||||
|  |         this.layerConfig = new UIEventSource<LayerConfigJson>(layerConfig); | ||||||
|  |         this.fullConfig = fullConfig; | ||||||
|  |         this.CreateElements(fullConfig, layerConfig, generateField) | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private CreateElements(fullConfig: UIEventSource<LayoutConfigJson>, layerConfig: LayerConfigJson, generateField: (src: UIEventSource<any>, 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<LayoutConfigJson>; | ||||||
|  |     private generateField: ((src: UIEventSource<any>, label: string, key: string, root: any, deflt?: string) => UIElement); | ||||||
|  | 
 | ||||||
|  |     constructor(config: UIEventSource<LayoutConfigJson>, generateField: ((src: UIEventSource<any>, 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 `<img src="${layer?.icon ?? "./assets/help.svg"}">` | ||||||
|  |             }); | ||||||
|  |             layerPanes.push({ | ||||||
|  |                 header: new VariableUiElement(header), | ||||||
|  |                 content: new LayerGenerator(config, layer, this.generateField) | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         layerPanes.push({ | ||||||
|  |             header: "<img src='./assets/add.svg'>", | ||||||
|  |             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<UserDetails>; | ||||||
|  | 
 | ||||||
|  |     private readonly themeObject: UIEventSource<LayoutConfigJson>; | ||||||
|  |     private readonly allQuestionFields: UIElement[]; | ||||||
|  |     public url: UIEventSource<string>; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     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<LayoutConfigJson>(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<LayoutConfigJson>, label: string, key: string, root: any, deflt: string = "") { | ||||||
|  |         const value = new UIEventSource<string>(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<string>({ | ||||||
|  |                 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: <a href="${url}" target="_blank">Click here to open</a>`)), | ||||||
|  |             ...this.allQuestionFields, | ||||||
|  |         ]).Render(); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue