Studio: studio now supports loading self-made layers in themes

This commit is contained in:
Pieter Vander Vennet 2024-04-23 15:35:18 +02:00
parent 9716bc5425
commit 28bf8cca9f
24 changed files with 826 additions and 464 deletions

View file

@ -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
}
}

View file

@ -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

View file

@ -64,16 +64,6 @@
}
}
newPath.push(...toAdd)
console.log(
"Fused path ",
path.join("."),
"+",
i,
"+",
subpartPath.join("."),
"into",
newPath.join(".")
)
return newPath
}

View file

@ -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
}

View file

@ -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"