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-11-07 18:51:50 +01:00
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
2024-01-19 17:31:35 +01:00
import { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
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-11-07 18:51:50 +01:00
public readonly showIntro : UIEventSource < "no" | "intro" | "tagrenderings" > = < any > (
LocalStorageSource . Get ( "studio-show-intro" , "intro" )
)
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 ( )
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-12-19 22:08:00 +01:00
public async delete ( ) {
2023-12-02 00:24:55 +01:00
await this . server . delete ( this . getId ( ) . data , this . category )
}
2024-01-19 17:31:35 +01:00
2023-10-17 00:32:54 +02:00
public getStoreFor < T > ( path : ReadonlyArray < string | number > ) : UIEventSource < T | undefined > {
const key = path . join ( "." )
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 )
2024-01-24 23:45:20 +01:00
this . configuration . addCallbackD ( ( ) = > {
2023-11-05 12:05:00 +01:00
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 ) = > {
2024-01-22 01:01:38 +01:00
if ( msg . level === "debug" || msg . level === "information" ) {
return false
}
2023-10-24 22:01:10 +02:00
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
} )
}
}
2024-01-19 17:31:35 +01:00
class ContextRewritingStep < T > extends Conversion < LayerConfigJson , T > {
private readonly _step : Conversion < LayerConfigJson , T >
private readonly _state : DesugaringContext
private readonly _getTagRenderings : ( t : T ) = > TagRenderingConfigJson [ ]
constructor (
state : DesugaringContext ,
step : Conversion < LayerConfigJson , T > ,
getTagRenderings : ( t : T ) = > TagRenderingConfigJson [ ]
) {
super (
"When validating a layer, the tagRenderings are first expanded. Some builtin tagRendering-calls (e.g. `contact`) will introduce _multiple_ tagRenderings, causing the count to be off. This class rewrites the error messages to fix this" ,
[ ] ,
"ContextRewritingStep"
)
this . _state = state
this . _step = step
this . _getTagRenderings = getTagRenderings
}
convert ( json : LayerConfigJson , context : ConversionContext ) : T {
const converted = this . _step . convert ( json , context )
const originalIds = json . tagRenderings ? . map (
( tr ) = > ( < QuestionableTagRenderingConfigJson > tr ) [ "id" ]
)
if ( ! originalIds ) {
return converted
}
let newTagRenderings : TagRenderingConfigJson [ ]
if ( converted === undefined ) {
const prepared = new PrepareLayer ( this . _state )
newTagRenderings = < TagRenderingConfigJson [ ] > (
prepared . convert ( json , context ) . tagRenderings
)
} else {
newTagRenderings = this . _getTagRenderings ( converted )
}
context . rewriteMessages ( ( path ) = > {
if ( path [ 0 ] !== "tagRenderings" ) {
return undefined
}
const newPath = [ . . . path ]
const idToSearch = newTagRenderings [ newPath [ 1 ] ] . id
const oldIndex = originalIds . indexOf ( idToSearch )
if ( oldIndex < 0 ) {
console . warn ( "Original ID was not found: " , idToSearch )
return undefined // We don't modify the message
}
newPath [ 1 ] = oldIndex
return newPath
} )
return converted
}
}
2023-10-26 13:58:45 +02:00
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
2023-12-19 22:08:00 +01:00
function cleanArray ( data : object , key : string ) : boolean {
if ( ! data ) {
2023-12-03 03:33:02 +01:00
return false
}
2023-12-03 03:01:43 +01:00
if ( data [ key ] ) {
2023-11-02 04:35:32 +01:00
// A bit of cleanup
2023-12-03 03:01:43 +01:00
const lBefore = data [ key ] . length
const cleaned = Utils . NoNull ( data [ key ] )
2023-11-02 04:35:32 +01:00
if ( cleaned . length != lBefore ) {
2023-12-03 03:01:43 +01:00
data [ key ] = cleaned
return true
2023-11-02 04:35:32 +01:00
}
}
2023-12-03 03:01:43 +01:00
return false
}
this . configuration . addCallbackAndRunD ( ( layer ) = > {
2023-12-03 03:33:02 +01:00
let changed = cleanArray ( layer , "tagRenderings" ) || cleanArray ( layer , "pointRenderings" )
for ( const tr of layer . tagRenderings ? ? [ ] ) {
2023-12-19 22:08:00 +01:00
if ( typeof tr === "string" ) {
2023-12-03 03:33:02 +01:00
continue
}
2023-12-19 22:08:00 +01:00
const qtr = < QuestionableTagRenderingConfigJson > tr
if ( qtr . freeform && Object . keys ( qtr . freeform ) . length === 0 ) {
2023-12-03 03:33:02 +01:00
delete qtr . freeform
changed = true
}
}
2023-12-19 22:08:00 +01:00
if ( changed ) {
2023-12-03 03:01:43 +01:00
this . configuration . ping ( )
}
2023-11-02 04:35:32 +01:00
} )
2023-10-26 13:58:45 +02:00
}
protected buildValidation ( state : DesugaringContext ) {
2024-01-19 17:31:35 +01:00
return new ContextRewritingStep (
state ,
new Pipe ( new PrepareLayer ( state ) , new ValidateLayer ( "dynamic" , false , undefined , true ) ) ,
( t ) = > < TagRenderingConfigJson [ ] > t . raw . tagRenderings
2023-10-26 13:58:45 +02:00
)
}
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
}