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