2023-06-18 00:44:57 +02:00
|
|
|
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"
|
2023-10-12 16:55:26 +02:00
|
|
|
import {
|
2023-10-26 13:58:45 +02:00
|
|
|
Conversion,
|
2023-10-12 16:55:26 +02:00
|
|
|
ConversionMessage,
|
|
|
|
DesugaringContext,
|
|
|
|
Pipe,
|
|
|
|
} from "../../Models/ThemeConfig/Conversion/Conversion"
|
|
|
|
import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer"
|
2023-10-26 13:58:45 +02:00
|
|
|
import { ValidateLayer, ValidateTheme } from "../../Models/ThemeConfig/Conversion/Validation"
|
2023-10-12 16:55:26 +02:00
|
|
|
import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
|
|
|
|
import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
2023-10-13 18:46:56 +02:00
|
|
|
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
|
|
|
import StudioServer from "./StudioServer"
|
2023-10-17 00:32:54 +02:00
|
|
|
import { Utils } from "../../Utils"
|
2023-10-24 22:01:10 +02:00
|
|
|
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
2023-10-25 00:03:51 +02:00
|
|
|
import { OsmTags } from "../../Models/OsmFeature"
|
|
|
|
import { Feature, Point } from "geojson"
|
|
|
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
2023-10-26 13:58:45 +02:00
|
|
|
import { LayoutConfigJson } from "../../Models/ThemeConfig/Json/LayoutConfigJson"
|
|
|
|
import { PrepareTheme } from "../../Models/ThemeConfig/Conversion/PrepareTheme"
|
2023-11-05 12:05:00 +01:00
|
|
|
import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext"
|
2023-06-16 02:36:11 +02:00
|
|
|
|
2023-10-25 00:03:51 +02:00
|
|
|
export interface HighlightedTagRendering {
|
|
|
|
path: ReadonlyArray<string | number>
|
|
|
|
schema: ConfigMeta
|
|
|
|
}
|
|
|
|
|
2023-10-26 13:58:45 +02:00
|
|
|
export abstract class EditJsonState<T> {
|
2023-06-18 00:44:57 +02:00
|
|
|
public readonly schema: ConfigMeta[]
|
2023-10-26 13:58:45 +02:00
|
|
|
public readonly category: "layers" | "themes"
|
|
|
|
public readonly server: StudioServer
|
2023-06-18 00:44:57 +02:00
|
|
|
|
2023-11-07 02:13:16 +01:00
|
|
|
public readonly expertMode: UIEventSource<boolean>
|
|
|
|
|
2023-10-26 13:58:45 +02:00
|
|
|
public readonly configuration: UIEventSource<Partial<T>> = new UIEventSource<Partial<T>>({})
|
2023-10-12 16:55:26 +02:00
|
|
|
public readonly messages: Store<ConversionMessage[]>
|
2023-10-25 00:03:51 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The EditLayerUI shows a 'schemaBasedInput' for this path to pop advanced questions out
|
|
|
|
*/
|
|
|
|
public readonly highlightedItem: UIEventSource<HighlightedTagRendering> = new UIEventSource(
|
|
|
|
undefined
|
|
|
|
)
|
2023-11-05 12:05:00 +01:00
|
|
|
private sendingUpdates = false
|
2023-10-24 22:01:10 +02:00
|
|
|
private readonly _stores = new Map<string, UIEventSource<any>>()
|
2023-06-20 01:32:24 +02:00
|
|
|
|
2023-11-07 02:13:16 +01:00
|
|
|
constructor(
|
|
|
|
schema: ConfigMeta[],
|
|
|
|
server: StudioServer,
|
|
|
|
category: "layers" | "themes",
|
|
|
|
options?: {
|
|
|
|
expertMode?: UIEventSource<boolean>
|
|
|
|
}
|
|
|
|
) {
|
2023-06-18 00:44:57 +02:00
|
|
|
this.schema = schema
|
2023-10-13 18:46:56 +02:00
|
|
|
this.server = server
|
2023-10-26 13:58:45 +02:00
|
|
|
this.category = category
|
2023-11-07 02:13:16 +01:00
|
|
|
this.expertMode = options?.expertMode ?? new UIEventSource<boolean>(false)
|
2023-10-20 19:04:55 +02:00
|
|
|
|
2023-10-26 13:58:45 +02:00
|
|
|
this.messages = this.setupErrorsForLayers()
|
2023-10-25 00:03:51 +02:00
|
|
|
|
2023-10-26 13:58:45 +02:00
|
|
|
const layerId = this.getId()
|
2023-11-05 12:05:00 +01:00
|
|
|
this.highlightedItem.addCallbackD((hl) => console.log("Highlighted item is", hl))
|
2023-10-26 13:58:45 +02:00
|
|
|
this.configuration
|
2023-11-02 04:35:32 +01:00
|
|
|
.mapD((config) => {
|
|
|
|
if (!this.sendingUpdates) {
|
|
|
|
console.log("Not sending updates yet! Trigger 'startSendingUpdates' first")
|
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
return JSON.stringify(config, null, " ")
|
|
|
|
})
|
2023-10-26 13:58:45 +02:00
|
|
|
.stabilized(100)
|
|
|
|
.addCallbackD(async (config) => {
|
|
|
|
const id = layerId.data
|
|
|
|
if (id === undefined) {
|
|
|
|
console.warn("No id found in layer, not updating")
|
|
|
|
return
|
2023-10-25 00:03:51 +02:00
|
|
|
}
|
2023-11-02 04:35:32 +01:00
|
|
|
await this.server.update(id, config, this.category)
|
2023-10-26 13:58:45 +02:00
|
|
|
})
|
2023-06-20 01:32:24 +02:00
|
|
|
}
|
|
|
|
|
2023-11-02 04:35:32 +01:00
|
|
|
public startSavingUpdates(enabled = true) {
|
|
|
|
this.sendingUpdates = enabled
|
|
|
|
if (enabled) {
|
|
|
|
this.configuration.ping()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-23 16:14:43 +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
|
|
|
}
|
2023-06-23 16:14:43 +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-11-05 12:05:00 +01:00
|
|
|
this.configuration.addCallbackD((config) => {
|
|
|
|
store.setData(this.getCurrentValueFor(path))
|
|
|
|
})
|
2023-08-23 11:11:53 +02:00
|
|
|
return store
|
|
|
|
}
|
|
|
|
|
2023-06-23 16:14:43 +02:00
|
|
|
public register(
|
|
|
|
path: ReadonlyArray<string | number>,
|
|
|
|
value: Store<any>,
|
2023-11-02 04:35:32 +01:00
|
|
|
noInitialSync: boolean = true
|
2023-06-23 16:14:43 +02:00
|
|
|
): () => void {
|
2023-10-24 22:01:10 +02:00
|
|
|
const unsync = value.addCallback((v) => {
|
|
|
|
this.setValueAt(path, v)
|
|
|
|
})
|
2023-06-23 16:14:43 +02:00
|
|
|
if (!noInitialSync) {
|
2023-06-23 17:28:44 +02:00
|
|
|
this.setValueAt(path, value.data)
|
2023-06-23 16:14:43 +02:00
|
|
|
}
|
|
|
|
return unsync
|
2023-06-16 02:36:11 +02:00
|
|
|
}
|
2023-06-18 00:44:57 +02:00
|
|
|
|
|
|
|
public getSchemaStartingWith(path: string[]) {
|
|
|
|
return this.schema.filter(
|
|
|
|
(sch) =>
|
|
|
|
!path.some((part, i) => !(sch.path.length > path.length && sch.path[i] === part))
|
|
|
|
)
|
|
|
|
}
|
2023-06-21 17:13:09 +02:00
|
|
|
|
2023-06-30 13:36:02 +02:00
|
|
|
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
|
|
|
|
2023-06-30 13:36:02 +02:00
|
|
|
public getSchema(path: string[]): ConfigMeta[] {
|
2023-09-15 01:16:33 +02:00
|
|
|
const schemas = this.schema.filter(
|
2023-06-21 17:13:09 +02:00
|
|
|
(sch) =>
|
2023-06-23 16:14:43 +02:00
|
|
|
sch !== undefined &&
|
2023-06-21 17:13:09 +02:00
|
|
|
!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-21 17:13:09 +02:00
|
|
|
}
|
2023-06-23 16:14:43 +02:00
|
|
|
|
2023-06-23 17:28:44 +02:00
|
|
|
public setValueAt(path: ReadonlyArray<string | number>, v: any) {
|
2023-10-06 23:56:50 +02:00
|
|
|
let entry = this.configuration.data
|
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
|
|
|
|
2023-10-06 23:56:50 +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
|
|
|
|
}
|
2023-10-06 23:56:50 +02:00
|
|
|
entry[breadcrumb] = typeof path[i + 1] === "number" ? [] : {}
|
2023-06-23 16:14:43 +02:00
|
|
|
}
|
2023-10-06 23:56:50 +02:00
|
|
|
entry = entry[breadcrumb]
|
|
|
|
}
|
2023-10-24 22:01:10 +02:00
|
|
|
|
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]) {
|
|
|
|
delete entry[lastBreadcrumb]
|
2023-10-24 22:01:10 +02:00
|
|
|
this.configuration.ping()
|
2023-10-17 01:36:22 +02:00
|
|
|
}
|
2023-10-24 22:01:10 +02:00
|
|
|
} else if (entry[lastBreadcrumb] !== v) {
|
2023-10-17 01:36:22 +02:00
|
|
|
entry[lastBreadcrumb] = v
|
2023-10-24 22:01:10 +02:00
|
|
|
this.configuration.ping()
|
2023-06-23 16:14:43 +02:00
|
|
|
}
|
2023-10-24 22:01:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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-23 16:14:43 +02:00
|
|
|
}
|
2023-10-26 13:58:45 +02:00
|
|
|
|
|
|
|
protected abstract buildValidation(state: DesugaringContext): Conversion<T, any>
|
|
|
|
|
|
|
|
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) {
|
2023-10-30 13:45:44 +01:00
|
|
|
console.error(e)
|
2023-10-26 13:58:45 +02:00
|
|
|
context.err(e)
|
|
|
|
}
|
|
|
|
return context.messages
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default class EditLayerState extends EditJsonState<LayerConfigJson> {
|
|
|
|
// Needed for the special visualisations
|
|
|
|
public readonly osmConnection: OsmConnection
|
|
|
|
public readonly imageUploadManager = {
|
|
|
|
getCountsFor() {
|
|
|
|
return 0
|
|
|
|
},
|
|
|
|
}
|
|
|
|
public readonly layout: { getMatchingLayer: (key: any) => LayerConfig }
|
|
|
|
public readonly featureSwitches: {
|
|
|
|
featureSwitchIsDebugging: UIEventSource<boolean>
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Used to preview and interact with the questions
|
|
|
|
*/
|
|
|
|
public readonly testTags = new UIEventSource<OsmTags>({ id: "node/-12345" })
|
|
|
|
public readonly exampleFeature: Feature<Point> = {
|
|
|
|
type: "Feature",
|
|
|
|
properties: this.testTags.data,
|
|
|
|
geometry: {
|
|
|
|
type: "Point",
|
|
|
|
coordinates: [3.21, 51.2],
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2023-11-07 02:13:16 +01:00
|
|
|
constructor(
|
|
|
|
schema: ConfigMeta[],
|
|
|
|
server: StudioServer,
|
|
|
|
osmConnection: OsmConnection,
|
|
|
|
options: { expertMode: UIEventSource<boolean> }
|
|
|
|
) {
|
|
|
|
super(schema, server, "layers", options)
|
2023-10-26 13:58:45 +02:00
|
|
|
this.osmConnection = osmConnection
|
|
|
|
this.layout = {
|
|
|
|
getMatchingLayer: (_) => {
|
|
|
|
try {
|
|
|
|
return new LayerConfig(<LayerConfigJson>this.configuration.data, "dynamic")
|
|
|
|
} catch (e) {
|
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
this.featureSwitches = {
|
|
|
|
featureSwitchIsDebugging: new UIEventSource<boolean>(true),
|
|
|
|
}
|
|
|
|
|
|
|
|
this.addMissingTagRenderingIds()
|
2023-11-02 04:35:32 +01:00
|
|
|
|
|
|
|
this.configuration.addCallbackAndRunD((layer) => {
|
|
|
|
if (layer.tagRenderings) {
|
|
|
|
// A bit of cleanup
|
|
|
|
const lBefore = layer.tagRenderings.length
|
|
|
|
const cleaned = Utils.NoNull(layer.tagRenderings)
|
|
|
|
if (cleaned.length != lBefore) {
|
|
|
|
layer.tagRenderings = cleaned
|
|
|
|
this.configuration.ping()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2023-10-26 13:58:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
protected buildValidation(state: DesugaringContext) {
|
|
|
|
return new Pipe(
|
|
|
|
new PrepareLayer(state),
|
|
|
|
new ValidateLayer("dynamic", false, undefined, true)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
protected getId(): Store<string> {
|
|
|
|
return this.configuration.mapD((config) => config.id)
|
|
|
|
}
|
|
|
|
|
|
|
|
private addMissingTagRenderingIds() {
|
|
|
|
this.configuration.addCallbackD((config) => {
|
|
|
|
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
|
2023-11-05 12:05:00 +01:00
|
|
|
let id = "" + i + "_" + Utils.randomString(5)
|
2023-10-26 13:58:45 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class EditThemeState extends EditJsonState<LayoutConfigJson> {
|
2023-11-07 02:13:16 +01:00
|
|
|
constructor(
|
|
|
|
schema: ConfigMeta[],
|
|
|
|
server: StudioServer,
|
|
|
|
options: { expertMode: UIEventSource<boolean> }
|
|
|
|
) {
|
|
|
|
super(schema, server, "themes", options)
|
2023-11-02 04:35:32 +01:00
|
|
|
}
|
|
|
|
|
2023-10-26 13:58:45 +02:00
|
|
|
protected buildValidation(state: DesugaringContext): Conversion<LayoutConfigJson, any> {
|
|
|
|
return new Pipe(
|
|
|
|
new PrepareTheme(state),
|
|
|
|
new ValidateTheme(undefined, "", false, new Set(state.tagRenderings.keys()))
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
protected getId(): Store<string> {
|
|
|
|
return this.configuration.mapD((config) => config.id)
|
|
|
|
}
|
2023-06-16 02:36:11 +02:00
|
|
|
}
|