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
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,8 +58,8 @@
 | 
			
		|||
  let layers: Store<{ owner: number; id: string }[]> = layersWithErr.mapD((l) =>
 | 
			
		||||
    l["success"]?.filter((l) => l.category === "layers")
 | 
			
		||||
  )
 | 
			
		||||
  let selfLayers = layers.mapD((ls) => ls.filter((l) => l.owner === uid.data), [uid])
 | 
			
		||||
  let otherLayers = layers.mapD(
 | 
			
		||||
  let selfLayers: Store<{ owner: number; id: string }[]> = layers.mapD((ls) => ls.filter((l) => l.owner === uid.data), [uid])
 | 
			
		||||
  let otherLayers: Store<{ owner: number; id: string }[]> = layers.mapD(
 | 
			
		||||
    (ls) => ls.filter((l) => l.owner !== undefined && l.owner !== uid.data),
 | 
			
		||||
    [uid]
 | 
			
		||||
  )
 | 
			
		||||
| 
						 | 
				
			
			@ -291,7 +291,7 @@
 | 
			
		|||
        </BackButton>
 | 
			
		||||
      </EditLayer>
 | 
			
		||||
    {:else if state === "editing_theme"}
 | 
			
		||||
      <EditTheme state={editThemeState}>
 | 
			
		||||
      <EditTheme state={editThemeState} selfLayers={$selfLayers} otherLayers={$otherLayers} {osmConnection}>
 | 
			
		||||
        <BackButton
 | 
			
		||||
          clss="small p-1"
 | 
			
		||||
          imageClass="w-8 h-8"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue