MapComplete/src/UI/Studio/EditLayerState.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

233 lines
8.3 KiB
TypeScript
Raw Normal View History

import { ConfigMeta } from "./configMeta"
2023-06-20 01:32:24 +02:00
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import {
ConversionContext,
ConversionMessage,
DesugaringContext,
Pipe,
} from "../../Models/ThemeConfig/Conversion/Conversion"
import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer"
import { ValidateLayer } from "../../Models/ThemeConfig/Conversion/Validation"
import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import StudioServer from "./StudioServer"
2023-10-17 00:32:54 +02:00
import { Utils } from "../../Utils"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
2023-09-15 01:16:33 +02:00
/**
* Sends changes back to the server
*/
export class LayerStateSender {
constructor(layerState: EditLayerState) {
const layerId = layerState.configuration.map((config) => config.id)
layerState.configuration
.mapD((config) => JSON.stringify(config, null, " "))
.stabilized(100)
.addCallbackD(async (config) => {
const id = layerId.data
if (id === undefined) {
console.warn("No id found in layer, not updating")
return
}
await layerState.server.updateLayer(id, config)
})
2023-09-15 01:16:33 +02:00
}
}
2023-06-16 02:36:11 +02:00
export default class EditLayerState {
public readonly schema: ConfigMeta[]
2023-06-20 01:32:24 +02:00
public readonly featureSwitches: { featureSwitchIsDebugging: UIEventSource<boolean> }
public readonly configuration: UIEventSource<Partial<LayerConfigJson>> = new UIEventSource<
Partial<LayerConfigJson>
>({})
public readonly messages: Store<ConversionMessage[]>
public readonly server: StudioServer
// Needed for the special visualisations
public readonly osmConnection: OsmConnection
private readonly _stores = new Map<string, UIEventSource<any>>()
2023-06-20 01:32:24 +02:00
constructor(schema: ConfigMeta[], server: StudioServer, osmConnection: OsmConnection) {
this.schema = schema
this.server = server
this.osmConnection = osmConnection
2023-06-20 01:32:24 +02:00
this.featureSwitches = {
featureSwitchIsDebugging: new UIEventSource<boolean>(true),
}
let state: DesugaringContext
{
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)
}
state = {
tagRenderings: sharedQuestions,
sharedLayers: layers,
}
}
2023-10-20 19:04:55 +02:00
const prepare = new Pipe(
new PrepareLayer(state),
new ValidateLayer("dynamic", false, undefined, true)
)
this.messages = this.configuration.mapD((config) => {
2023-10-17 00:32:54 +02:00
const trs = Utils.NoNull(config.tagRenderings ?? [])
for (let i = 0; i < trs.length; i++) {
const tr = trs[i]
if (typeof tr === "string") {
continue
}
if (!tr["id"] && !tr["override"]) {
const qtr = <QuestionableTagRenderingConfigJson>tr
let id = "" + i
if (qtr?.freeform?.key) {
id = qtr?.freeform?.key
} else if (qtr.mappings?.[0]?.if) {
id =
qtr.freeform?.key ??
TagUtils.Tag(qtr.mappings[0].if).usedKeys()?.[0] ??
"" + i
}
qtr["id"] = id
}
}
const context = ConversionContext.construct([], ["prepare"])
prepare.convert(<LayerConfigJson>config, context)
return context.messages
})
2023-06-20 01:32:24 +02:00
}
public getCurrentValueFor(path: ReadonlyArray<string | number>): any | undefined {
// Walk the path down to see if we find something
let entry = this.configuration.data
for (let i = 0; i < path.length; i++) {
if (entry === undefined) {
// We reached a dead end - no old vlaue
return undefined
2023-06-20 01:32:24 +02:00
}
const breadcrumb = path[i]
entry = entry[breadcrumb]
}
return entry
}
2023-10-17 00:32:54 +02:00
public getStoreFor<T>(path: ReadonlyArray<string | number>): UIEventSource<T | undefined> {
const key = path.join(".")
// TODO check if this gives problems when changing the order of e.g. mappings and questions
if (this._stores.has(key)) {
return this._stores.get(key)
}
2023-08-23 11:11:53 +02:00
const store = new UIEventSource<any>(this.getCurrentValueFor(path))
store.addCallback((v) => {
this.setValueAt(path, v)
})
2023-10-17 00:32:54 +02:00
this._stores.set(key, store)
2023-08-23 11:11:53 +02:00
return store
}
public register(
path: ReadonlyArray<string | number>,
value: Store<any>,
noInitialSync: boolean = false
): () => void {
const unsync = value.addCallback((v) => {
this.setValueAt(path, v)
})
if (!noInitialSync) {
2023-06-23 17:28:44 +02:00
this.setValueAt(path, value.data)
}
return unsync
2023-06-16 02:36:11 +02:00
}
public getSchemaStartingWith(path: string[]) {
return this.schema.filter(
(sch) =>
!path.some((part, i) => !(sch.path.length > path.length && sch.path[i] === part))
)
}
public getTranslationAt(path: string[]): ConfigMeta {
const origConfig = this.getSchema(path)[0]
return {
path,
type: "translation",
hints: {
typehint: "translation",
},
required: origConfig.required ?? false,
description: origConfig.description ?? "A translatable object",
}
}
2023-09-15 01:16:33 +02:00
public getSchema(path: string[]): ConfigMeta[] {
2023-09-15 01:16:33 +02:00
const schemas = this.schema.filter(
(sch) =>
sch !== undefined &&
!path.some((part, i) => !(sch.path.length == path.length && sch.path[i] === part))
)
2023-09-15 01:16:33 +02:00
if (schemas.length == 0) {
console.warn("No schemas found for path", path.join("."))
}
return schemas
}
2023-06-23 17:28:44 +02:00
public setValueAt(path: ReadonlyArray<string | number>, v: any) {
let entry = this.configuration.data
console.log("Setting value at", path, v)
2023-10-17 00:32:54 +02:00
const isUndefined =
2023-10-17 01:36:22 +02:00
v === undefined ||
v === null ||
v === "" ||
(typeof v === "object" && Object.keys(v).length === 0)
2023-10-17 00:32:54 +02:00
for (let i = 0; i < path.length - 1; i++) {
const breadcrumb = path[i]
if (entry[breadcrumb] === undefined) {
2023-10-17 01:36:22 +02:00
if (isUndefined) {
// we have a dead end _and_ we do not need to set a value - we do an early return
return
}
entry[breadcrumb] = typeof path[i + 1] === "number" ? [] : {}
}
entry = entry[breadcrumb]
}
2023-10-17 01:36:22 +02:00
const lastBreadcrumb = path.at(-1)
2023-10-17 00:32:54 +02:00
if (isUndefined) {
2023-10-17 01:36:22 +02:00
if (entry && entry[lastBreadcrumb]) {
console.log("Deleting", lastBreadcrumb, "of", path.join("."))
delete entry[lastBreadcrumb]
this.configuration.ping()
2023-10-17 01:36:22 +02:00
}
} else if (entry[lastBreadcrumb] !== v) {
console.log("Assigning and pinging at", path)
2023-10-17 01:36:22 +02:00
entry[lastBreadcrumb] = v
this.configuration.ping()
}
}
public messagesFor(path: ReadonlyArray<string | number>): Store<ConversionMessage[]> {
return this.messages.map((msgs) => {
if (!msgs) {
return []
}
return msgs.filter((msg) => {
const pth = msg.context.path
for (let i = 0; i < Math.min(pth.length, path.length); i++) {
if (pth[i] !== path[i]) {
return false
}
}
return true
})
})
}
2023-06-16 02:36:11 +02:00
}