forked from MapComplete/MapComplete
		
	Studio: studio now supports loading self-made layers in themes
This commit is contained in:
		
							parent
							
								
									9716bc5425
								
							
						
					
					
						commit
						28bf8cca9f
					
				
					 24 changed files with 826 additions and 464 deletions
				
			
		|  | @ -5,7 +5,7 @@ import { | |||
|     Conversion, | ||||
|     ConversionMessage, | ||||
|     DesugaringContext, | ||||
|     Pipe, | ||||
|     Pipe | ||||
| } from "../../Models/ThemeConfig/Conversion/Conversion" | ||||
| import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer" | ||||
| import { ValidateLayer, ValidateTheme } from "../../Models/ThemeConfig/Conversion/Validation" | ||||
|  | @ -64,7 +64,6 @@ export abstract class EditJsonState<T> { | |||
|         this.category = category | ||||
|         this.expertMode = options?.expertMode ?? new UIEventSource<boolean>(false) | ||||
| 
 | ||||
|         this.messages = this.setupErrorsForLayers() | ||||
| 
 | ||||
|         const layerId = this.getId() | ||||
|         this.configuration | ||||
|  | @ -84,6 +83,8 @@ export abstract class EditJsonState<T> { | |||
|                 } | ||||
|                 await this.server.update(id, config, this.category) | ||||
|             }) | ||||
|         this.messages = this.createMessagesStore() | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public startSavingUpdates(enabled = true) { | ||||
|  | @ -152,10 +153,10 @@ export abstract class EditJsonState<T> { | |||
|             path, | ||||
|             type: "translation", | ||||
|             hints: { | ||||
|                 typehint: "translation", | ||||
|                 typehint: "translation" | ||||
|             }, | ||||
|             required: origConfig.required ?? false, | ||||
|             description: origConfig.description ?? "A translatable object", | ||||
|             description: origConfig.description ?? "A translatable object" | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -227,28 +228,19 @@ export abstract class EditJsonState<T> { | |||
| 
 | ||||
|     protected abstract getId(): Store<string> | ||||
| 
 | ||||
|     private setupErrorsForLayers(): Store<ConversionMessage[]> { | ||||
|         const layers = AllSharedLayers.getSharedLayersConfigs() | ||||
|         const questions = layers.get("questions") | ||||
|         const sharedQuestions = new Map<string, QuestionableTagRenderingConfigJson>() | ||||
|         for (const question of questions.tagRenderings) { | ||||
|             sharedQuestions.set(question["id"], <QuestionableTagRenderingConfigJson>question) | ||||
|         } | ||||
|         let state: DesugaringContext = { | ||||
|             tagRenderings: sharedQuestions, | ||||
|             sharedLayers: layers, | ||||
|         } | ||||
|         const prepare = this.buildValidation(state) | ||||
|         return this.configuration.mapD((config) => { | ||||
|             const context = ConversionContext.construct([], ["prepare"]) | ||||
|             try { | ||||
|                 prepare.convert(<T>config, context) | ||||
|             } catch (e) { | ||||
|                 console.error(e) | ||||
|                 context.err(e) | ||||
|     protected abstract validate(configuration: Partial<T>): Promise<ConversionMessage[]>; | ||||
| 
 | ||||
|     /** | ||||
|      * Creates a store that validates the configuration and which contains all relevant (error)-messages | ||||
|      * @private | ||||
|      */ | ||||
|     private createMessagesStore(): Store<ConversionMessage[]> { | ||||
|         return this.configuration.mapAsyncD(async (config) => { | ||||
|             if(!this.validate){ | ||||
|                 return [] | ||||
|             } | ||||
|             return context.messages | ||||
|         }) | ||||
|             return await this.validate(config) | ||||
|         }).map(messages => messages ?? []) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -314,7 +306,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> { | |||
|     public readonly imageUploadManager = { | ||||
|         getCountsFor() { | ||||
|             return 0 | ||||
|         }, | ||||
|         } | ||||
|     } | ||||
|     public readonly layout: { getMatchingLayer: (key: any) => LayerConfig } | ||||
|     public readonly featureSwitches: { | ||||
|  | @ -330,8 +322,8 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> { | |||
|         properties: this.testTags.data, | ||||
|         geometry: { | ||||
|             type: "Point", | ||||
|             coordinates: [3.21, 51.2], | ||||
|         }, | ||||
|             coordinates: [3.21, 51.2] | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     constructor( | ||||
|  | @ -343,16 +335,16 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> { | |||
|         super(schema, server, "layers", options) | ||||
|         this.osmConnection = osmConnection | ||||
|         this.layout = { | ||||
|             getMatchingLayer: (_) => { | ||||
|             getMatchingLayer: () => { | ||||
|                 try { | ||||
|                     return new LayerConfig(<LayerConfigJson>this.configuration.data, "dynamic") | ||||
|                 } catch (e) { | ||||
|                     return undefined | ||||
|                 } | ||||
|             }, | ||||
|             } | ||||
|         } | ||||
|         this.featureSwitches = { | ||||
|             featureSwitchIsDebugging: new UIEventSource<boolean>(true), | ||||
|             featureSwitchIsDebugging: new UIEventSource<boolean>(true) | ||||
|         } | ||||
| 
 | ||||
|         this.addMissingTagRenderingIds() | ||||
|  | @ -428,6 +420,30 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> { | |||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     protected async validate(configuration: Partial<LayerConfigJson>): Promise<ConversionMessage[]> { | ||||
| 
 | ||||
|         const layers = AllSharedLayers.getSharedLayersConfigs() | ||||
| 
 | ||||
|         const questions = layers.get("questions") | ||||
|         const sharedQuestions = new Map<string, QuestionableTagRenderingConfigJson>() | ||||
|         for (const question of questions.tagRenderings) { | ||||
|             sharedQuestions.set(question["id"], <QuestionableTagRenderingConfigJson>question) | ||||
|         } | ||||
|         const state: DesugaringContext = { | ||||
|             tagRenderings: sharedQuestions, | ||||
|             sharedLayers: layers | ||||
|         } | ||||
|         const prepare = this.buildValidation(state) | ||||
|         const context = ConversionContext.construct([], ["prepare"]) | ||||
|         try { | ||||
|             prepare.convert(<LayerConfigJson>configuration, context) | ||||
|         } catch (e) { | ||||
|             console.error(e) | ||||
|             context.err(e) | ||||
|         } | ||||
|         return context.messages | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class EditThemeState extends EditJsonState<LayoutConfigJson> { | ||||
|  | @ -437,6 +453,7 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> { | |||
|         options: { expertMode: UIEventSource<boolean> } | ||||
|     ) { | ||||
|         super(schema, server, "themes", options) | ||||
|         this.setupFixers() | ||||
|     } | ||||
| 
 | ||||
|     protected buildValidation(state: DesugaringContext): Conversion<LayoutConfigJson, any> { | ||||
|  | @ -449,4 +466,55 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> { | |||
|     protected getId(): Store<string> { | ||||
|         return this.configuration.mapD((config) => config.id) | ||||
|     } | ||||
| 
 | ||||
|     /** Applies a few bandaids to get everything smoothed out in case of errors; a big bunch of hacks basically | ||||
|      */ | ||||
|     public setupFixers() { | ||||
|         this.configuration.addCallbackAndRunD(config => { | ||||
|             if (config.layers) { | ||||
|                 // Remove 'null' and 'undefined' values from the layer array if any are found
 | ||||
|                 for (let i = config.layers.length; i >= 0; i--) { | ||||
|                     if (!config.layers[i]) { | ||||
|                         config.layers.splice(i, 1) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     protected async validate(configuration: Partial<LayoutConfigJson>) { | ||||
| 
 | ||||
|         const layers = AllSharedLayers.getSharedLayersConfigs() | ||||
| 
 | ||||
|         for (const l of configuration.layers ?? []) { | ||||
|             if(typeof l !== "string"){ | ||||
|                 continue | ||||
|             } | ||||
|             if (!l.startsWith("https://")) { | ||||
|                 continue | ||||
|             } | ||||
|             const config = <LayerConfigJson> await Utils.downloadJsonCached(l, 1000*60*10) | ||||
|             layers.set(l, config) | ||||
|         } | ||||
| 
 | ||||
|         const questions = layers.get("questions") | ||||
|         const sharedQuestions = new Map<string, QuestionableTagRenderingConfigJson>() | ||||
|         for (const question of questions.tagRenderings) { | ||||
|             sharedQuestions.set(question["id"], <QuestionableTagRenderingConfigJson>question) | ||||
|         } | ||||
|         const state: DesugaringContext = { | ||||
|             tagRenderings: sharedQuestions, | ||||
|             sharedLayers: layers | ||||
|         } | ||||
|         const prepare = this.buildValidation(state) | ||||
|         const context = ConversionContext.construct([], ["prepare"]) | ||||
|         try { | ||||
|             prepare.convert(<LayoutConfigJson>configuration, context) | ||||
|         } catch (e) { | ||||
|             console.error(e) | ||||
|             context.err(e) | ||||
|         } | ||||
|         return context.messages | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -7,10 +7,50 @@ | |||
|   import ShowConversionMessages from "./ShowConversionMessages.svelte" | ||||
|   import Region from "./Region.svelte" | ||||
|   import RawEditor from "./RawEditor.svelte" | ||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||
| 
 | ||||
|   export let state: EditThemeState | ||||
|   export let osmConnection: OsmConnection | ||||
|   let schema: ConfigMeta[] = state.schema.filter((schema) => schema.path.length > 0) | ||||
|   let config = state.configuration | ||||
| 
 | ||||
|   export let selfLayers: { owner: number; id: string }[] | ||||
|   export let otherLayers: { owner: number; id: string }[] | ||||
|   { | ||||
| 
 | ||||
|     /** | ||||
|      * We modify the schema and inject options for self-declared layers | ||||
|      */ | ||||
| 
 | ||||
|     const layerSchema = schema.find(l => l.path.join(".") === "layers") | ||||
|     const suggestions: { if: string, then: string }[] = layerSchema.hints.suggestions | ||||
|     suggestions.unshift(...selfLayers.map( | ||||
|       l => ({ | ||||
|         if: `value=https://studio.mapcomplete.org/${l.owner}/layers/${l.id}/${l.id}.json`, | ||||
|         then: `<b>${l.id}</b> (made by you)` | ||||
|       }) | ||||
|     )) | ||||
| 
 | ||||
|     for (let i = 0; i < otherLayers.length; i++) { | ||||
|       const l = otherLayers[i] | ||||
|       const mapping = { | ||||
|         if: `value=https://studio.mapcomplete.org/${l.owner}/layers/${l.id}/${l.id}.json`, | ||||
|         then: `<b>${l.id}</b> (made by ${l.owner})` | ||||
|       } | ||||
|       /** | ||||
|        * This is a filthy hack which is time-sensitive and will break | ||||
|        * It downloads the username and patches the suggestion, assuming that the list with all layers will be shown a while _after_ loading the view. | ||||
|        * Caching in 'getInformationAboutUser' helps with this as well | ||||
|        */ | ||||
|       osmConnection.getInformationAboutUser(l.owner).then(userInfo => { | ||||
|         mapping.then = `<b>${l.id}</b> (made by ${userInfo.display_name})` | ||||
|       }) | ||||
|       suggestions.push(mapping) | ||||
|     } | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   let messages = state.messages | ||||
|   let hasErrors = messages.map( | ||||
|     (m: ConversionMessage[]) => m.filter((m) => m.level === "error").length | ||||
|  |  | |||
|  | @ -64,16 +64,6 @@ | |||
|       } | ||||
|     } | ||||
|     newPath.push(...toAdd) | ||||
|     console.log( | ||||
|       "Fused path ", | ||||
|       path.join("."), | ||||
|       "+", | ||||
|       i, | ||||
|       "+", | ||||
|       subpartPath.join("."), | ||||
|       "into", | ||||
|       newPath.join(".") | ||||
|     ) | ||||
|     return newPath | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,6 +4,10 @@ import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" | |||
| import { Store } from "../../Logic/UIEventSource" | ||||
| import { LayoutConfigJson } from "../../Models/ThemeConfig/Json/LayoutConfigJson" | ||||
| 
 | ||||
| /** | ||||
|  * A small class wrapping around the Server API. | ||||
|  * This is _not_ the script which actually hosts! | ||||
|  */ | ||||
| export default class StudioServer { | ||||
|     private readonly url: string | ||||
|     private readonly _userId: Store<number> | ||||
|  | @ -29,7 +33,7 @@ export default class StudioServer { | |||
|             category: "layers" | "themes" | ||||
|         }[] = [] | ||||
|         for (let file of allFiles) { | ||||
|             let parts = file.split("/") | ||||
|             const parts = file.split("/") | ||||
|             let owner = Number(parts[0]) | ||||
|             if (!isNaN(owner)) { | ||||
|                 parts.splice(0, 1) | ||||
|  | @ -55,7 +59,7 @@ export default class StudioServer { | |||
|         uid?: number | ||||
|     ): Promise<LayerConfigJson | LayoutConfigJson> { | ||||
|         try { | ||||
|             return await Utils.downloadJson(this.urlFor(layerId, category, uid)) | ||||
|             return <any> await Utils.downloadJson(this.urlFor(layerId, category, uid)) | ||||
|         } catch (e) { | ||||
|             return undefined | ||||
|         } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue