2023-06-01 14:40:54 +02:00
import {
Concat ,
Conversion ,
DesugaringContext ,
DesugaringStep ,
Each ,
FirstOf ,
Fuse ,
On ,
SetDefault ,
} from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
2023-10-12 16:55:26 +02:00
import {
MinimalTagRenderingConfigJson ,
TagRenderingConfigJson ,
} from "../Json/TagRenderingConfigJson"
2023-06-01 14:40:54 +02:00
import { Utils } from "../../../Utils"
2023-03-09 13:34:03 +01:00
import RewritableConfigJson from "../Json/RewritableConfigJson"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
import Translations from "../../../UI/i18n/Translations"
2023-06-01 14:40:54 +02:00
import { Translation } from "../../../UI/i18n/Translation"
2023-07-15 18:04:30 +02:00
import tagrenderingconfigmeta from "../../../../src/assets/schemas/tagrenderingconfigmeta.json"
2023-06-01 14:40:54 +02:00
import { AddContextToTranslations } from "./AddContextToTranslations"
2023-03-09 13:34:03 +01:00
import FilterConfigJson from "../Json/FilterConfigJson"
2023-07-15 18:04:30 +02:00
import predifined_filters from "../../../../assets/layers/filters/filters.json"
2023-06-01 14:40:54 +02:00
import { TagConfigJson } from "../Json/TagConfigJson"
2023-10-06 23:56:50 +02:00
import PointRenderingConfigJson , { IconConfigJson } from "../Json/PointRenderingConfigJson"
2023-03-31 03:28:11 +02:00
import ValidationUtils from "./ValidationUtils"
2023-11-22 19:39:19 +01:00
import { RenderingSpecification } from "../../../UI/SpecialVisualization"
2023-06-14 20:39:36 +02:00
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
2023-06-21 22:42:26 +02:00
import { ConfigMeta } from "../../../UI/Studio/configMeta"
2023-10-12 16:55:26 +02:00
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
2023-11-02 04:35:32 +01:00
import { ConversionContext } from "./ConversionContext"
2023-12-12 03:43:55 +01:00
import { ExpandRewrite } from "./ExpandRewrite"
2022-09-14 16:29:41 +02:00
2022-10-27 01:50:41 +02:00
class ExpandFilter extends DesugaringStep < LayerConfigJson > {
private static readonly predefinedFilters = ExpandFilter . load_filters ( )
2023-03-09 14:45:36 +01:00
private _state : DesugaringContext
2022-09-14 16:29:41 +02:00
2023-03-09 14:45:36 +01:00
constructor ( state : DesugaringContext ) {
2022-10-27 01:50:41 +02:00
super (
2023-03-09 14:45:36 +01:00
"Expands filters: replaces a shorthand by the value found in 'filters.json'. If the string is formatted 'layername.filtername, it will be looked up into that layer instead" ,
2022-10-27 01:50:41 +02:00
[ "filter" ] ,
"ExpandFilter"
)
2023-03-09 14:45:36 +01:00
this . _state = state
2022-09-14 16:29:41 +02:00
}
2023-02-03 03:57:30 +01:00
private static load_filters ( ) : Map < string , FilterConfigJson > {
2024-04-08 17:05:49 +02:00
const filters = new Map < string , FilterConfigJson > ( )
2023-02-03 03:57:30 +01:00
for ( const filter of < FilterConfigJson [ ] > predifined_filters . filter ) {
filters . set ( filter . id , filter )
}
return filters
}
2023-10-11 04:16:52 +02:00
convert ( json : LayerConfigJson , context : ConversionContext ) : LayerConfigJson {
if ( json ? . filter === undefined || json ? . filter === null ) {
return json // Nothing to change here
2022-09-14 16:29:41 +02:00
}
2022-10-27 01:50:41 +02:00
if ( json . filter [ "sameAs" ] !== undefined ) {
2023-10-11 04:16:52 +02:00
return json // Nothing to change here
2022-09-14 16:29:41 +02:00
}
2022-10-27 01:50:41 +02:00
const newFilters : FilterConfigJson [ ] = [ ]
2024-04-08 01:58:07 +02:00
const filters = < ( FilterConfigJson | string ) [ ] > json . filter
2024-04-13 02:40:21 +02:00
for ( let i = 0 ; i < filters . length ; i ++ ) {
2024-04-08 01:58:07 +02:00
const filter = filters [ i ]
2022-09-14 16:29:41 +02:00
if ( typeof filter !== "string" ) {
newFilters . push ( filter )
continue
}
2024-04-08 01:58:07 +02:00
2024-04-13 02:40:21 +02:00
const matchingTr = < TagRenderingConfigJson > (
json . tagRenderings . find ( ( tr ) = > ! ! tr && tr [ "id" ] === filter )
)
if ( matchingTr ) {
if ( ! ( matchingTr . mappings ? . length >= 1 ) ) {
context
. enters ( "filter" , i )
. err (
"Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings"
)
2024-04-08 01:58:07 +02:00
}
2024-04-13 02:40:21 +02:00
const options = matchingTr . mappings . map ( ( mapping ) = > ( {
2024-04-08 01:58:07 +02:00
question : mapping.then ,
2024-04-13 02:40:21 +02:00
osmTags : mapping.if ,
2024-04-08 01:58:07 +02:00
} ) )
options . unshift ( {
question : {
2024-04-13 02:40:21 +02:00
en : "All types" ,
2024-04-08 01:58:07 +02:00
} ,
2024-04-13 02:40:21 +02:00
osmTags : undefined ,
2024-04-08 01:58:07 +02:00
} )
newFilters . push ( {
id : filter ,
2024-04-13 02:40:21 +02:00
options ,
2024-04-08 01:58:07 +02:00
} )
continue
}
2023-03-09 14:45:36 +01:00
if ( filter . indexOf ( "." ) > 0 ) {
if ( this . _state . sharedLayers . size > 0 ) {
const split = filter . split ( "." )
if ( split . length > 2 ) {
2023-10-11 04:16:52 +02:00
context . err (
"invalid filter name: " + filter + ", expected `layername.filterid`"
2023-03-09 14:45:36 +01:00
)
}
const layer = this . _state . sharedLayers . get ( split [ 0 ] )
if ( layer === undefined ) {
2023-10-11 04:16:52 +02:00
context . err ( "Layer '" + split [ 0 ] + "' not found" )
2023-03-09 14:45:36 +01:00
}
const expectedId = split [ 1 ]
const expandedFilter = ( < ( FilterConfigJson | string ) [ ] > layer . filter ) . find (
( f ) = > typeof f !== "string" && f . id === expectedId
)
newFilters . push ( < FilterConfigJson > expandedFilter )
} else {
// This is a bootstrapping-run, we can safely ignore this
}
continue
}
2022-09-14 16:29:41 +02:00
// Search for the filter:
const found = ExpandFilter . predefinedFilters . get ( filter )
2022-10-27 01:50:41 +02:00
if ( found === undefined ) {
const suggestions = Utils . sortedByLevenshteinDistance (
filter ,
Array . from ( ExpandFilter . predefinedFilters . keys ( ) ) ,
( t ) = > t
)
2023-10-11 04:16:52 +02:00
context
. enter ( filter )
. err (
"While searching for predefined filter " +
filter +
": this filter is not found. Perhaps you meant one of: " +
suggestions
)
2022-09-14 16:29:41 +02:00
}
newFilters . push ( found )
}
2023-10-11 04:16:52 +02:00
return { . . . json , filter : newFilters }
2022-09-14 16:29:41 +02:00
}
}
2022-09-08 21:40:48 +02:00
2022-01-21 01:57:16 +01:00
class ExpandTagRendering extends Conversion <
2023-10-11 04:16:52 +02:00
| string
| TagRenderingConfigJson
| {
builtin : string | string [ ]
override : any
} ,
2022-01-21 01:57:16 +01:00
TagRenderingConfigJson [ ]
> {
2022-02-04 01:05:35 +01:00
private readonly _state : DesugaringContext
2023-05-23 00:34:26 +02:00
private readonly _tagRenderingsByLabel : Map < string , TagRenderingConfigJson [ ] >
2023-10-06 23:56:50 +02:00
// Only used for self-reference
2022-07-11 09:14:26 +02:00
private readonly _self : LayerConfigJson
2022-08-18 14:39:40 +02:00
private readonly _options : {
/* If true, will copy the 'osmSource'-tags into the condition */
applyCondition? : true | boolean
2024-06-16 16:06:26 +02:00
noHardcodedStrings? : false | boolean
2024-05-07 17:25:23 +02:00
addToContext? : false | boolean
2022-01-21 01:57:16 +01:00
}
2022-02-04 01:05:35 +01:00
constructor (
state : DesugaringContext ,
self : LayerConfigJson ,
2023-10-11 04:16:52 +02:00
options ? : {
applyCondition? : true | boolean
2024-06-16 16:06:26 +02:00
noHardcodedStrings? : false | boolean
2024-05-07 17:25:23 +02:00
// If set, a question will be added to the 'sharedTagRenderings'. Should only be used for 'questions.json'
addToContext? : false | boolean
2023-10-11 04:16:52 +02:00
}
2022-02-04 01:05:35 +01:00
) {
2022-09-08 21:40:48 +02:00
super (
2024-05-07 17:25:23 +02:00
"Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question and reusing the builtins" ,
2022-02-04 01:05:35 +01:00
[ ] ,
2022-04-06 03:06:50 +02:00
"ExpandTagRendering"
2022-09-08 21:40:48 +02:00
)
2022-02-04 01:05:35 +01:00
this . _state = state
2022-07-11 09:14:26 +02:00
this . _self = self
2022-08-18 14:39:40 +02:00
this . _options = options
2023-05-23 00:34:26 +02:00
this . _tagRenderingsByLabel = new Map < string , TagRenderingConfigJson [ ] > ( )
2023-07-15 18:04:30 +02:00
for ( const trconfig of state . tagRenderings ? . values ( ) ? ? [ ] ) {
2023-09-15 01:16:33 +02:00
for ( const label of trconfig [ "labels" ] ? ? [ ] ) {
2023-05-23 00:34:26 +02:00
let withLabel = this . _tagRenderingsByLabel . get ( label )
if ( withLabel === undefined ) {
withLabel = [ ]
this . _tagRenderingsByLabel . set ( label , withLabel )
}
withLabel . push ( trconfig )
}
}
2022-09-08 21:40:48 +02:00
}
2023-10-12 16:55:26 +02:00
public convert (
spec : string | any ,
ctx : ConversionContext
) : QuestionableTagRenderingConfigJson [ ] {
const trs = this . convertOnce ( spec , ctx )
const result = [ ]
for ( const tr of trs ) {
if ( typeof tr === "string" || tr [ "builtin" ] !== undefined ) {
const stable = this . convert ( tr , ctx . inOperation ( "recursive_resolve" ) )
result . push ( . . . stable )
2024-06-16 16:06:26 +02:00
if ( this . _options ? . addToContext ) {
2024-05-07 17:25:23 +02:00
for ( const tr of stable ) {
this . _state . tagRenderings ? . set ( tr . id , tr )
}
}
2023-10-12 16:55:26 +02:00
} else {
result . push ( tr )
2024-06-16 16:06:26 +02:00
if ( this . _options ? . addToContext ) {
this . _state . tagRenderings ? . set ( tr [ "id" ] , < QuestionableTagRenderingConfigJson > tr )
2024-05-07 17:25:23 +02:00
}
2023-10-12 16:55:26 +02:00
}
}
return result
}
2024-04-22 23:43:30 +02:00
private lookup ( name : string , ctx : ConversionContext ) : TagRenderingConfigJson [ ] | undefined {
2023-02-03 03:57:30 +01:00
const direct = this . directLookup ( name )
2023-05-23 00:34:26 +02:00
2023-02-03 03:57:30 +01:00
if ( direct === undefined ) {
return undefined
}
const result : TagRenderingConfigJson [ ] = [ ]
for ( const tagRenderingConfigJson of direct ) {
2024-05-07 17:25:23 +02:00
const nm : string | string [ ] | undefined = tagRenderingConfigJson [ "builtin" ]
2023-05-23 00:34:26 +02:00
if ( nm !== undefined ) {
2023-02-03 03:57:30 +01:00
let indirect : TagRenderingConfigJson [ ]
if ( typeof nm === "string" ) {
2024-04-22 23:43:30 +02:00
indirect = this . lookup ( nm , ctx )
2023-02-03 03:57:30 +01:00
} else {
2024-04-22 23:43:30 +02:00
indirect = [ ] . concat ( . . . nm . map ( ( n ) = > this . lookup ( n , ctx ) ) )
2023-02-03 03:57:30 +01:00
}
for ( let foundTr of indirect ) {
foundTr = Utils . Clone < any > ( foundTr )
2024-04-28 11:10:14 +02:00
ctx . MergeObjectsForOverride ( tagRenderingConfigJson [ "override" ] ? ? { } , foundTr )
2023-09-15 01:16:33 +02:00
foundTr [ "id" ] = tagRenderingConfigJson [ "id" ] ? ? foundTr [ "id" ]
2023-02-03 03:57:30 +01:00
result . push ( foundTr )
}
} else {
result . push ( tagRenderingConfigJson )
}
}
return result
}
/ * *
2023-03-09 13:34:03 +01:00
* Looks up a tagRendering or group of tagRenderings based on the name .
2023-02-03 03:57:30 +01:00
* /
2023-03-09 13:34:03 +01:00
private directLookup ( name : string ) : TagRenderingConfigJson [ ] | undefined {
2022-02-04 01:05:35 +01:00
const state = this . _state
2022-01-21 01:57:16 +01:00
if ( state . tagRenderings . has ( name ) ) {
return [ state . tagRenderings . get ( name ) ]
}
2023-06-01 02:52:21 +02:00
if ( this . _tagRenderingsByLabel . has ( name ) ) {
2023-05-23 00:34:26 +02:00
return this . _tagRenderingsByLabel . get ( name )
}
2022-07-11 09:14:26 +02:00
if ( name . indexOf ( "." ) < 0 ) {
return undefined
}
2022-07-27 23:59:04 +02:00
2022-07-11 09:14:26 +02:00
const spl = name . split ( "." )
2023-07-15 18:04:30 +02:00
let layer = state . sharedLayers ? . get ( spl [ 0 ] )
2023-10-06 23:56:50 +02:00
if ( spl [ 0 ] === this . _self ? . id ) {
2022-07-11 09:14:26 +02:00
layer = this . _self
}
2022-01-21 01:57:16 +01:00
2023-07-15 18:04:30 +02:00
if ( spl . length !== 2 || ! layer ) {
2022-07-11 09:14:26 +02:00
return undefined
}
2022-07-27 23:59:04 +02:00
2022-07-11 09:14:26 +02:00
const id = spl [ 1 ]
const layerTrs = < TagRenderingConfigJson [ ] > (
layer . tagRenderings . filter ( ( tr ) = > tr [ "id" ] !== undefined )
2022-09-08 21:40:48 +02:00
)
2022-07-11 09:14:26 +02:00
let matchingTrs : TagRenderingConfigJson [ ]
if ( id === "*" ) {
matchingTrs = layerTrs
} else if ( id . startsWith ( "*" ) ) {
const id_ = id . substring ( 1 )
2023-09-15 01:16:33 +02:00
matchingTrs = layerTrs . filter ( ( tr ) = > tr [ "labels" ] ? . indexOf ( id_ ) >= 0 )
2022-07-11 09:14:26 +02:00
} else {
2023-09-15 01:16:33 +02:00
matchingTrs = layerTrs . filter ( ( tr ) = > tr [ "id" ] === id || tr [ "labels" ] ? . indexOf ( id ) >= 0 )
2022-07-11 09:14:26 +02:00
}
2022-01-21 01:57:16 +01:00
2022-07-11 09:14:26 +02:00
const contextWriter = new AddContextToTranslations < TagRenderingConfigJson > ( "layers:" )
for ( let i = 0 ; i < matchingTrs . length ; i ++ ) {
let found : TagRenderingConfigJson = Utils . Clone ( matchingTrs [ i ] )
2022-08-18 14:39:40 +02:00
if ( this . _options ? . applyCondition ) {
// The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown
2023-03-29 17:56:42 +02:00
if ( typeof layer . source !== "string" ) {
if ( found . condition === undefined ) {
2023-04-14 02:42:57 +02:00
found . condition = layer . source [ "osmTags" ]
2023-03-29 17:56:42 +02:00
} else {
2023-06-11 02:32:14 +02:00
found . condition = { and : [ found . condition , layer . source [ "osmTags" ] ] }
2023-03-29 17:56:42 +02:00
}
2022-08-18 14:39:40 +02:00
}
2022-01-21 01:57:16 +01:00
}
2022-07-11 09:14:26 +02:00
2023-10-11 04:16:52 +02:00
found = contextWriter . convertStrict (
found ,
ConversionContext . construct (
[ layer . id , "tagRenderings" , found [ "id" ] ] ,
[ "AddContextToTranslations" ]
)
)
2022-07-11 09:14:26 +02:00
matchingTrs [ i ] = found
}
if ( matchingTrs . length !== 0 ) {
return matchingTrs
2022-01-21 01:57:16 +01:00
}
return undefined
}
2023-10-11 04:16:52 +02:00
private convertOnce ( tr : string | any , ctx : ConversionContext ) : TagRenderingConfigJson [ ] {
2022-02-04 01:05:35 +01:00
const state = this . _state
2022-01-21 01:57:16 +01:00
if ( typeof tr === "string" ) {
2023-07-15 18:04:30 +02:00
let lookup
if ( this . _state . tagRenderings !== null ) {
2024-04-22 23:43:30 +02:00
lookup = this . lookup ( tr , ctx )
2023-07-15 18:04:30 +02:00
}
2022-02-18 23:10:27 +01:00
if ( lookup === undefined ) {
2023-10-17 16:06:58 +02:00
if (
this . _state . sharedLayers ? . size > 0 &&
ctx . path . at ( - 1 ) !== "icon" &&
! ctx . path . find ( ( p ) = > p === "pointRendering" )
) {
2023-10-11 04:16:52 +02:00
ctx . warn (
` A literal rendering was detected: ${ tr }
Did you perhaps forgot to add a layer name as 'layername.${tr}' ? ` +
2023-06-01 14:40:54 +02:00
Array . from ( state . sharedLayers . keys ( ) ) . join ( ", " )
2023-03-09 13:34:03 +01:00
)
2022-07-03 13:18:05 +02:00
}
2023-03-09 13:34:03 +01:00
2023-07-15 18:04:30 +02:00
if ( this . _options ? . noHardcodedStrings && this . _state ? . sharedLayers ? . size > 0 ) {
2023-10-11 04:16:52 +02:00
ctx . err (
"Detected an invocation to a builtin tagRendering, but this tagrendering was not found: " +
2023-06-01 14:40:54 +02:00
tr +
" \n Did you perhaps forget to add the layer as prefix, such as `icons." +
tr +
"`? "
2023-03-09 13:34:03 +01:00
)
}
2022-02-18 23:10:27 +01:00
return [
2023-09-17 13:45:46 +02:00
< any > {
2022-02-18 23:10:27 +01:00
render : tr ,
2022-07-03 13:18:05 +02:00
id : tr.replace ( /[^a-zA-Z0-9]/g , "" ) ,
2022-02-18 23:10:27 +01:00
} ,
]
2022-01-21 01:57:16 +01:00
}
2022-02-18 23:10:27 +01:00
return lookup
2022-01-21 01:57:16 +01:00
}
if ( tr [ "builtin" ] !== undefined ) {
2022-07-03 13:18:05 +02:00
let names : string | string [ ] = tr [ "builtin" ]
2022-01-21 01:57:16 +01:00
if ( typeof names === "string" ) {
names = [ names ]
}
2023-07-15 18:04:30 +02:00
if ( this . _state . tagRenderings === null ) {
return [ ]
}
2022-01-21 01:57:16 +01:00
for ( const key of Object . keys ( tr ) ) {
if (
key === "builtin" ||
key === "override" ||
key === "id" ||
key . startsWith ( "#" )
) {
continue
}
2023-10-11 04:16:52 +02:00
ctx . err (
"An object calling a builtin can only have keys `builtin` or `override`, but a key with name `" +
2023-06-01 14:40:54 +02:00
key +
"` was found. This won't be picked up! The full object is: " +
JSON . stringify ( tr )
2022-09-08 21:40:48 +02:00
)
2022-01-21 01:57:16 +01:00
}
const trs : TagRenderingConfigJson [ ] = [ ]
for ( const name of names ) {
2024-04-22 23:43:30 +02:00
const lookup = this . lookup ( name , ctx )
2022-01-21 01:57:16 +01:00
if ( lookup === undefined ) {
2022-07-11 09:14:26 +02:00
let candidates = Array . from ( state . tagRenderings . keys ( ) )
if ( name . indexOf ( "." ) > 0 ) {
2022-09-14 16:29:41 +02:00
const [ layerName ] = name . split ( "." )
2022-07-11 09:14:26 +02:00
let layer = state . sharedLayers . get ( layerName )
2023-10-06 23:56:50 +02:00
if ( layerName === this . _self ? . id ) {
2022-07-11 09:14:26 +02:00
layer = this . _self
}
if ( layer === undefined ) {
const candidates = Utils . sortedByLevenshteinDistance (
layerName ,
Array . from ( state . sharedLayers . keys ( ) ) ,
( s ) = > s
2022-09-08 21:40:48 +02:00
)
2022-07-27 23:59:04 +02:00
if ( state . sharedLayers . size === 0 ) {
2023-10-11 04:16:52 +02:00
ctx . warn (
"BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " +
2023-06-01 14:40:54 +02:00
name +
": layer " +
layerName +
2023-10-02 01:23:43 +02:00
" not found for now, but ignoring as this is a bootstrapping run. "
2022-09-08 21:40:48 +02:00
)
2022-07-27 23:59:04 +02:00
} else {
2023-10-11 04:16:52 +02:00
ctx . err (
": While reusing tagrendering: " +
2023-06-01 14:40:54 +02:00
name +
": layer " +
layerName +
2023-10-02 01:23:43 +02:00
" not found. Maybe you meant one of " +
2023-06-01 14:40:54 +02:00
candidates . slice ( 0 , 3 ) . join ( ", " )
2022-09-08 21:40:48 +02:00
)
2022-07-27 23:59:04 +02:00
}
2022-07-11 09:14:26 +02:00
continue
}
candidates = Utils . NoNull ( layer . tagRenderings . map ( ( tr ) = > tr [ "id" ] ) ) . map (
( id ) = > layerName + "." + id
2022-09-08 21:40:48 +02:00
)
2022-07-03 13:18:05 +02:00
}
candidates = Utils . sortedByLevenshteinDistance ( name , candidates , ( i ) = > i )
2023-10-11 04:16:52 +02:00
ctx . err (
"The tagRendering with identifier " +
2023-06-01 14:40:54 +02:00
name +
" was not found.\n\tDid you mean one of " +
candidates . join ( ", " ) +
"?\n(Hint: did you add a new label and are you trying to use this label at the same time? Run 'reset:layeroverview' first"
2022-07-11 09:14:26 +02:00
)
2022-01-21 01:57:16 +01:00
continue
}
for ( let foundTr of lookup ) {
foundTr = Utils . Clone < any > ( foundTr )
2024-04-22 23:43:30 +02:00
ctx . MergeObjectsForOverride ( tr [ "override" ] ? ? { } , foundTr )
2023-07-15 18:04:30 +02:00
if ( names . length == 1 ) {
foundTr [ "id" ] = tr [ "id" ] ? ? foundTr [ "id" ]
}
2022-01-21 01:57:16 +01:00
trs . push ( foundTr )
}
}
return trs
}
return [ tr ]
}
}
2023-04-07 03:54:11 +02:00
class DetectInline extends DesugaringStep < QuestionableTagRenderingConfigJson > {
constructor ( ) {
super (
"If no 'inline' is set on the freeform key, it will be automatically added. If no special renderings are used, it'll be set to true" ,
[ "freeform.inline" ] ,
"DetectInline"
)
}
convert (
json : QuestionableTagRenderingConfigJson ,
2023-10-11 04:16:52 +02:00
context : ConversionContext
) : QuestionableTagRenderingConfigJson {
2023-04-07 03:54:11 +02:00
if ( json . freeform === undefined ) {
2023-10-11 04:16:52 +02:00
return json
2023-04-07 03:54:11 +02:00
}
let spec : Record < string , string >
if ( typeof json . render === "string" ) {
2023-06-14 20:39:36 +02:00
spec = { "*" : json . render }
2023-04-07 03:54:11 +02:00
} else {
2023-04-14 02:42:57 +02:00
spec = < Record < string , string > > json . render
2023-04-07 03:54:11 +02:00
}
for ( const key in spec ) {
if ( spec [ key ] . indexOf ( "<a " ) >= 0 ) {
// We have a link element, it probably contains something that needs to be substituted...
// Let's play this safe and not inline it
2023-10-11 04:16:52 +02:00
return json
2023-04-07 03:54:11 +02:00
}
const fullSpecification = SpecialVisualizations . constructSpecification ( spec [ key ] )
if ( fullSpecification . length > 1 ) {
// We found a special rendering!
if ( json . freeform . inline === true ) {
2023-10-11 04:16:52 +02:00
context . err (
"'inline' is set, but the rendering contains a special visualisation...\n " +
2023-06-14 20:39:36 +02:00
spec [ key ]
2023-04-07 03:54:11 +02:00
)
}
json = JSON . parse ( JSON . stringify ( json ) )
json . freeform . inline = false
2023-10-11 04:16:52 +02:00
return json
2023-04-07 03:54:11 +02:00
}
}
json = JSON . parse ( JSON . stringify ( json ) )
2023-09-15 01:16:33 +02:00
if ( typeof json . freeform === "string" ) {
2023-10-11 04:16:52 +02:00
context . err ( "'freeform' is a string, but should be an object" )
return json
2023-09-15 01:16:33 +02:00
}
2023-10-11 04:16:52 +02:00
json . freeform . inline ? ? = true
return json
2023-04-07 03:54:11 +02:00
}
}
2023-03-31 03:28:11 +02:00
export class AddQuestionBox extends DesugaringStep < LayerConfigJson > {
constructor ( ) {
super (
"Adds a 'questions'-object if no question element is added yet" ,
[ "tagRenderings" ] ,
"AddQuestionBox"
)
}
2024-04-13 01:16:53 +02:00
/ * *
* const action = new AddQuestionBox ( )
* const tagRenderings = [ { id : "questions" , render : { "*" : "{questions()}" } } ]
* const conv = action . convert ( { tagRenderings } , ConversionContext . construct ( [ "test" ] , [ ] ) )
* conv . tagRenderings // => [{id:"questions", render: {"*": "{questions()}" } }]
* /
2023-10-11 04:16:52 +02:00
convert ( json : LayerConfigJson , context : ConversionContext ) : LayerConfigJson {
2023-04-14 02:42:57 +02:00
if (
json . tagRenderings === undefined ||
json . tagRenderings . some ( ( tr ) = > tr [ "id" ] === "leftover-questions" )
) {
2023-10-11 04:16:52 +02:00
return json
2023-03-31 03:28:11 +02:00
}
2023-11-03 01:05:17 +01:00
if ( json . source === "special" ) {
return json
}
2023-10-31 11:49:14 +01:00
json = { . . . json }
json . tagRenderings = [ . . . json . tagRenderings ]
const allSpecials : Exclude < RenderingSpecification , string > [ ] = < any > (
2024-04-13 02:40:21 +02:00
ValidationUtils . getAllSpecialVisualisations (
< QuestionableTagRenderingConfigJson [ ] > json . tagRenderings
) . filter ( ( spec ) = > typeof spec !== "string" )
2023-10-31 11:49:14 +01:00
)
2023-03-31 03:28:11 +02:00
const questionSpecials = allSpecials . filter ( ( sp ) = > sp . func . funcName === "questions" )
const noLabels = questionSpecials . filter (
( sp ) = > sp . args . length === 0 || sp . args [ 0 ] . trim ( ) === ""
)
if ( noLabels . length > 1 ) {
2023-10-11 04:16:52 +02:00
context . err (
"Multiple 'questions'-visualisations found which would show _all_ questions. Don't do this"
2023-03-31 03:28:11 +02:00
)
}
// ALl labels that are used in this layer
const allLabels = new Set (
2023-09-17 13:45:46 +02:00
[ ] . concat (
. . . json . tagRenderings . map (
( tr ) = > ( < QuestionableTagRenderingConfigJson > tr ) . labels ? ? [ ]
)
)
2023-03-31 03:28:11 +02:00
)
2024-04-13 01:16:53 +02:00
const seen : Set < string > = new Set ( )
2023-03-31 03:28:11 +02:00
for ( const questionSpecial of questionSpecials ) {
if ( typeof questionSpecial === "string" ) {
continue
}
const used = questionSpecial . args [ 0 ]
? . split ( ";" )
? . map ( ( a ) = > a . trim ( ) )
? . filter ( ( s ) = > s != "" )
const blacklisted = questionSpecial . args [ 1 ]
? . split ( ";" )
? . map ( ( a ) = > a . trim ( ) )
? . filter ( ( s ) = > s != "" )
if ( blacklisted ? . length > 0 && used ? . length > 0 ) {
2023-10-11 04:16:52 +02:00
context . err (
"The {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." +
2023-06-14 20:39:36 +02:00
"\n Whitelisted: " +
used . join ( ", " ) +
"\n Blacklisted: " +
blacklisted . join ( ", " )
2023-03-31 03:28:11 +02:00
)
}
for ( const usedLabel of used ) {
if ( ! allLabels . has ( usedLabel ) ) {
2023-10-11 04:16:52 +02:00
context . err (
"This layers specifies a special question element for label `" +
2023-06-14 20:39:36 +02:00
usedLabel +
"`, but this label doesn't exist.\n" +
" Available labels are " +
Array . from ( allLabels ) . join ( ", " )
2023-03-31 03:28:11 +02:00
)
}
seen . add ( usedLabel )
}
}
if ( noLabels . length == 0 ) {
/ * A t t h i s p o i n t , w e k n o w w h i c h q u e s t i o n l a b e l s a r e n o t y e t h a n d l e d a n d w h i c h a l r e a d y a r e h a n d l e d , a n d w e
* know there is no previous catch - all questions
* /
2023-09-17 13:45:46 +02:00
const question : QuestionableTagRenderingConfigJson = {
2023-03-31 03:28:11 +02:00
id : "leftover-questions" ,
render : {
"*" : ` {questions( , ${ Array . from ( seen ) . join ( ";" ) } )} ` ,
} ,
}
json . tagRenderings . push ( question )
}
2023-10-11 04:16:52 +02:00
return json
2023-03-31 03:28:11 +02:00
}
}
2023-04-14 02:42:57 +02:00
export class AddEditingElements extends DesugaringStep < LayerConfigJson > {
2023-11-30 00:39:55 +01:00
static addedElements : string [ ] = [
"minimap" ,
"just_created" ,
"split_button" ,
"move_button" ,
"delete_button" ,
"last_edit" ,
"favourite_state" ,
"all_tags" ,
2023-12-24 05:01:10 +01:00
"qr_code" ,
2023-11-30 00:39:55 +01:00
]
2023-04-15 02:28:24 +02:00
private readonly _desugaring : DesugaringContext
2023-06-01 02:52:21 +02:00
2023-04-15 02:28:24 +02:00
constructor ( desugaring : DesugaringContext ) {
2023-04-14 02:42:57 +02:00
super (
"Add some editing elements, such as the delete button or the move button if they are configured. These used to be handled by the feature info box, but this has been replaced by special visualisation elements" ,
[ ] ,
"AddEditingElements"
)
2023-04-15 02:28:24 +02:00
this . _desugaring = desugaring
2023-04-14 02:42:57 +02:00
}
2024-01-24 23:45:20 +01:00
convert ( json : LayerConfigJson , _ : ConversionContext ) : LayerConfigJson {
2023-07-15 18:04:30 +02:00
if ( this . _desugaring . tagRenderings === null ) {
2023-10-11 04:16:52 +02:00
return json
2023-07-15 18:04:30 +02:00
}
2023-10-31 11:49:14 +01:00
if ( json . source === "special" ) {
return json
}
if ( ! json . title && ! json . tagRenderings ) {
return json
}
json = { . . . json }
json . tagRenderings = [ . . . ( json . tagRenderings ? ? [ ] ) ]
2023-12-25 23:55:52 +01:00
const allIds = new Set < string > ( json . tagRenderings . map ( ( tr ) = > tr [ "id" ] ) )
2023-10-31 11:49:14 +01:00
const specialVisualisations = ValidationUtils . getAllSpecialVisualisations (
< any > json . tagRenderings
)
2024-06-17 04:27:08 +02:00
2023-10-31 11:49:14 +01:00
const usedSpecialFunctions = new Set (
specialVisualisations . map ( ( sv ) = >
typeof sv === "string" ? undefined : sv . func . funcName
)
)
2024-06-17 04:27:08 +02:00
/***** ADD TO TOP ****/
2023-04-14 02:42:57 +02:00
2023-04-20 17:42:07 +02:00
if (
2023-05-17 13:22:13 +02:00
this . _desugaring . tagRenderings . has ( "just_created" ) &&
2023-04-20 17:42:07 +02:00
! json . tagRenderings . some ( ( tr ) = > tr === "just_created" || tr [ "id" ] === "just_created" )
) {
json . tagRenderings . unshift ( this . _desugaring . tagRenderings . get ( "just_created" ) )
}
2024-06-17 04:27:08 +02:00
if ( ! allIds . has ( "nothing_known" ) ) {
const indexFirstQuestion = json . tagRenderings . findIndex ( tr = > tr [ "question" ] !== undefined )
json . tagRenderings . splice ( indexFirstQuestion ,
0 ,
this . _desugaring . tagRenderings . get ( "nothing_known" ) )
console . log ( "aDDING" , this . _desugaring . tagRenderings . get ( "nothing_known" ) )
}
/***** ADD TO BOTTOM ****/
if ( ! allIds . has ( "lod" ) ) {
json . tagRenderings . push ( this . _desugaring . tagRenderings . get ( "lod" ) )
}
if ( ! usedSpecialFunctions . has ( "minimap" ) ) {
json . tagRenderings . push ( this . _desugaring . tagRenderings . get ( "minimap" ) )
}
2023-10-31 11:49:14 +01:00
if ( json . allowSplit && ! usedSpecialFunctions . has ( "split_button" ) ) {
2023-04-14 02:42:57 +02:00
json . tagRenderings . push ( {
id : "split-button" ,
2023-06-14 20:39:36 +02:00
render : { "*" : "{split_button()}" } ,
2023-04-14 02:42:57 +02:00
} )
2023-04-20 01:52:23 +02:00
delete json . allowSplit
2023-04-14 02:42:57 +02:00
}
2023-10-31 11:49:14 +01:00
if ( json . allowMove && ! usedSpecialFunctions . has ( "move_button" ) ) {
2023-04-14 02:42:57 +02:00
json . tagRenderings . push ( {
id : "move-button" ,
2023-06-14 20:39:36 +02:00
render : { "*" : "{move_button()}" } ,
2023-04-14 02:42:57 +02:00
} )
}
2023-10-31 11:49:14 +01:00
if ( json . deletion && ! usedSpecialFunctions . has ( "delete_button" ) ) {
2023-04-14 02:42:57 +02:00
json . tagRenderings . push ( {
id : "delete-button" ,
2023-06-14 20:39:36 +02:00
render : { "*" : "{delete_button()}" } ,
2023-04-14 02:42:57 +02:00
} )
}
2023-11-22 19:39:19 +01:00
if ( ! usedSpecialFunctions . has ( "favourite_status" ) ) {
json . tagRenderings . push ( {
id : "favourite_status" ,
render : { "*" : "{favourite_status()}" } ,
} )
}
2024-06-17 04:27:08 +02:00
if ( ! allIds . has ( "share" ) ) {
json . tagRenderings . push ( this . _desugaring . tagRenderings . get ( "share" ) )
}
2023-11-22 19:39:19 +01:00
2023-12-25 23:55:52 +01:00
if ( ! allIds . has ( "qr_code" ) ) {
json . tagRenderings . push ( this . _desugaring . tagRenderings . get ( "qr_code" ) )
}
2024-06-17 04:27:08 +02:00
if (
json . source !== "special" &&
json . source !== "special:library" &&
json . tagRenderings &&
this . _desugaring . tagRenderings . has ( "last_edit" ) &&
! json . tagRenderings . some ( ( tr ) = > tr [ "id" ] === "last_edit" )
) {
json . tagRenderings . push ( this . _desugaring . tagRenderings . get ( "last_edit" ) )
2023-12-24 05:01:10 +01:00
}
2024-06-17 04:27:08 +02:00
2023-10-31 11:49:14 +01:00
if ( ! usedSpecialFunctions . has ( "all_tags" ) ) {
2023-09-17 13:45:46 +02:00
const trc : QuestionableTagRenderingConfigJson = {
2023-04-15 02:28:24 +02:00
id : "all-tags" ,
2023-06-14 20:39:36 +02:00
render : { "*" : "{all_tags()}" } ,
2023-04-21 18:09:14 +02:00
2023-04-15 02:28:24 +02:00
metacondition : {
or : [
"__featureSwitchIsDebugging=true" ,
2023-05-11 02:17:41 +02:00
"mapcomplete-show_tags=full" ,
2023-04-15 02:28:24 +02:00
"mapcomplete-show_debug=yes" ,
] ,
} ,
}
2023-04-20 01:52:23 +02:00
json . tagRenderings ? . push ( trc )
2023-04-15 02:28:24 +02:00
}
2023-10-11 04:16:52 +02:00
return json
2023-04-14 02:42:57 +02:00
}
}
2022-03-29 00:20:10 +02:00
/ * *
* Converts a 'special' translation into a regular translation which uses parameters
* /
export class RewriteSpecial extends DesugaringStep < TagRenderingConfigJson > {
constructor ( ) {
2022-07-11 09:14:26 +02:00
super (
"Converts a 'special' translation into a regular translation which uses parameters" ,
[ "special" ] ,
"RewriteSpecial"
2022-09-08 21:40:48 +02:00
)
2022-03-29 00:20:10 +02:00
}
2022-03-08 01:05:54 +01:00
2024-01-16 04:52:05 +01:00
private static escapeStr ( v : string ) : string {
2024-01-16 04:09:58 +01:00
return v
. replace ( /,/g , "&COMMA" )
. replace ( /\{/g , "&LBRACE" )
. replace ( /}/g , "&RBRACE" )
. replace ( /\(/g , "&LPARENS" )
. replace ( /\)/g , "&RPARENS" )
}
2022-03-29 00:20:10 +02:00
/ * *
* Does the heavy lifting and conversion
2022-07-11 09:14:26 +02:00
*
2022-03-29 00:20:10 +02:00
* // should not do anything if no 'special'-key is present
2023-10-12 16:55:26 +02:00
* RewriteSpecial . convertIfNeeded ( { "en" : "xyz" , "nl" : "abc" } , ConversionContext . test ( ) ) // => {"en": "xyz", "nl": "abc"}
2022-07-11 09:14:26 +02:00
*
2022-03-29 00:20:10 +02:00
* // should handle a simple special case
2023-10-12 16:55:26 +02:00
* RewriteSpecial . convertIfNeeded ( { "special" : { "type" : "image_carousel" } } , ConversionContext . test ( ) ) // => {'*': "{image_carousel()}"}
2022-07-11 09:14:26 +02:00
*
2024-06-17 04:27:08 +02:00
* // should add a class to the special element
* RewriteSpecial . convertIfNeeded ( { "special" : { "type" : "qr_code" } , class : "inline" } , ConversionContext . test ( ) ) // => {'*': "{qr_code():inline}"}
*
2022-03-29 00:20:10 +02:00
* // should handle special case with a parameter
2023-10-12 16:55:26 +02:00
* RewriteSpecial . convertIfNeeded ( { "special" : { "type" : "image_carousel" , "image_key" : "some_image_key" } } , ConversionContext . test ( ) ) // => {'*': "{image_carousel(some_image_key)}"}
2022-07-11 09:14:26 +02:00
*
2022-03-29 00:20:10 +02:00
* // should handle special case with a translated parameter
* const spec = { "special" : { "type" : "image_upload" , "label" : { "en" : "Add a picture to this object" , "nl" : "Voeg een afbeelding toe" } } }
2023-10-12 16:55:26 +02:00
* const r = RewriteSpecial . convertIfNeeded ( spec , ConversionContext . test ( ) )
2022-03-29 00:20:10 +02:00
* r // => {"en": "{image_upload(,Add a picture to this object)}", "nl": "{image_upload(,Voeg een afbeelding toe)}" }
2022-07-11 09:14:26 +02:00
*
2022-05-06 12:41:24 +02:00
* // should handle special case with a prefix and postfix
* const spec = { "special" : { "type" : "image_upload" } , before : { "en" : "PREFIX " } , after : { "en" : " POSTFIX" , nl : " Achtervoegsel" } }
2023-10-12 16:55:26 +02:00
* const r = RewriteSpecial . convertIfNeeded ( spec , ConversionContext . test ( ) )
2022-05-06 12:41:24 +02:00
* r // => {"en": "PREFIX {image_upload(,)} POSTFIX", "nl": "PREFIX {image_upload(,)} Achtervoegsel" }
2022-07-11 09:14:26 +02:00
*
2022-03-29 00:20:10 +02:00
* // should warn for unexpected keys
2023-10-12 16:55:26 +02:00
* const context = ConversionContext . test ( )
* RewriteSpecial . convertIfNeeded ( { "special" : { type : "image_carousel" } , "en" : "xyz" } , context ) // => {'*': "{image_carousel()}"}
2024-06-17 04:27:08 +02:00
* context . getAll ( "error" ) [ 0 ] . message // => "The only keys allowed next to a 'special'-block are 'before', 'after' and 'class'. Perhaps you meant to put 'en' into the special block?"
2022-07-11 09:14:26 +02:00
*
2022-03-29 00:20:10 +02:00
* // should give an error on unknown visualisations
2023-10-12 16:55:26 +02:00
* const context = ConversionContext . test ( )
* RewriteSpecial . convertIfNeeded ( { "special" : { type : "qsdf" } } , context ) // => undefined
* context . getAll ( "error" ) [ 0 ] . message . indexOf ( "Special visualisation 'qsdf' not found" ) >= 0 // => true
2022-07-11 09:14:26 +02:00
*
2022-03-29 00:20:10 +02:00
* // should give an error is 'type' is missing
2023-10-12 16:55:26 +02:00
* const context = ConversionContext . test ( )
* RewriteSpecial . convertIfNeeded ( { "special" : { } } , context ) // => undefined
* context . getAll ( "error" ) [ 0 ] . message // => "A 'special'-block should define 'type' to indicate which visualisation should be used"
2022-07-29 21:09:58 +02:00
*
*
2022-07-29 20:04:36 +02:00
* // an actual test
2022-07-29 21:09:58 +02:00
* const special = {
* "before" : {
2022-07-29 20:04:36 +02:00
* "en" : "<h3>Entrances</h3>This building has {_entrances_count} entrances:"
* } ,
2022-07-29 21:09:58 +02:00
* "after" : {
2022-07-29 20:04:36 +02:00
* "en" : "{_entrances_count_without_width_count} entrances don't have width information yet"
* } ,
2022-07-29 21:09:58 +02:00
* "special" : {
* "type" : "multi" ,
2022-07-29 20:04:36 +02:00
* "key" : "_entrance_properties_with_width" ,
* "tagrendering" : {
* "en" : "An <a href='#{id}'>entrance</a> of {canonical(width)}"
* }
* } }
2023-10-12 16:55:26 +02:00
* const context = ConversionContext . test ( )
2024-02-02 14:16:16 +01:00
* RewriteSpecial . convertIfNeeded ( special , context ) // => {"en": "<h3>Entrances</h3>This building has {_entrances_count} entrances:{multi(_entrance_properties_with_width,An <a href='#&LBRACEid&RBRACE'>entrance</a> of &LBRACEcanonical&LPARENSwidth&RPARENS&RBRACE,)}{_entrances_count_without_width_count} entrances don't have width information yet"}
2023-10-12 16:55:26 +02:00
* context . getAll ( "error" ) // => []
2024-01-16 04:09:58 +01:00
*
* // another actual test
* const special = {
* "special" : {
* "type" : "multi" ,
* "key" : "_nearby_bicycle_parkings:props" ,
2024-01-16 04:52:05 +01:00
* "tagrendering" : "<b>{id}</b> ({distance}m) {tagApply(a,b,c)}"
2024-01-16 04:09:58 +01:00
* } }
* const context = ConversionContext . test ( )
2024-02-02 14:16:16 +01:00
* RewriteSpecial . convertIfNeeded ( special , context ) // => {"*": "{multi(_nearby_bicycle_parkings:props,<b>&LBRACEid&RBRACE</b> &LPARENS&LBRACEdistance&RBRACEm&RPARENS &LBRACEtagApply&LPARENSa&COMMAb&COMMAc&RPARENS&RBRACE,)}"}
2024-01-16 04:09:58 +01:00
* context . getAll ( "error" ) // => []
2022-03-29 00:20:10 +02:00
* /
2022-07-11 09:14:26 +02:00
private static convertIfNeeded (
2023-10-11 04:16:52 +02:00
input :
| ( object & {
special : {
type : string
}
} )
| any ,
2023-10-12 16:55:26 +02:00
context : ConversionContext
2022-07-11 09:14:26 +02:00
) : any {
2022-03-29 00:20:10 +02:00
const special = input [ "special" ]
2022-07-11 09:14:26 +02:00
if ( special === undefined ) {
2022-03-29 00:20:10 +02:00
return input
}
2022-03-08 01:05:54 +01:00
2022-03-29 00:20:10 +02:00
const type = special [ "type" ]
2022-07-11 09:14:26 +02:00
if ( type === undefined ) {
2023-10-12 16:55:26 +02:00
context . err (
2022-03-29 00:20:10 +02:00
"A 'special'-block should define 'type' to indicate which visualisation should be used"
)
return undefined
}
2022-07-29 21:09:58 +02:00
2022-03-29 00:20:10 +02:00
const vis = SpecialVisualizations . specialVisualizations . find ( ( sp ) = > sp . funcName === type )
2022-07-11 09:14:26 +02:00
if ( vis === undefined ) {
2022-03-29 00:20:10 +02:00
const options = Utils . sortedByLevenshteinDistance (
type ,
SpecialVisualizations . specialVisualizations ,
( sp ) = > sp . funcName
)
2023-10-12 16:55:26 +02:00
context . err (
2022-03-29 00:20:10 +02:00
` Special visualisation ' ${ type } ' not found. Did you perhaps mean ${ options [ 0 ] . funcName } , ${ options [ 1 ] . funcName } or ${ options [ 2 ] . funcName } ? \ n \ tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md `
2022-09-08 21:40:48 +02:00
)
2022-03-29 00:20:10 +02:00
return undefined
}
2023-10-12 16:55:26 +02:00
Array . from ( Object . keys ( input ) )
2024-06-17 04:27:08 +02:00
. filter ( ( k ) = > k !== "special" && k !== "before" && k !== "after" && k !== "class" )
2023-10-12 16:55:26 +02:00
. map ( ( k ) = > {
2024-06-17 04:27:08 +02:00
return ` The only keys allowed next to a 'special'-block are 'before', 'after' and 'class'. Perhaps you meant to put ' ${ k } ' into the special block? `
2023-10-12 16:55:26 +02:00
} )
. forEach ( ( e ) = > context . err ( e ) )
2022-09-08 21:40:48 +02:00
2022-03-29 00:20:10 +02:00
const argNamesList = vis . args . map ( ( a ) = > a . name )
const argNames = new Set < string > ( argNamesList )
// Check for obsolete and misspelled arguments
2023-10-12 16:55:26 +02:00
Object . keys ( special )
. filter ( ( k ) = > ! argNames . has ( k ) )
. filter ( ( k ) = > k !== "type" && k !== "before" && k !== "after" )
. map ( ( wrongArg ) = > {
const byDistance = Utils . sortedByLevenshteinDistance (
wrongArg ,
argNamesList ,
( x ) = > x
)
return ` Unexpected argument in special block at ${ context } with name ' ${ wrongArg } '. Did you mean ${
byDistance [ 0 ]
} ? \ n \ tAll known arguments are $ { argNamesList . join ( ", " ) } `
} )
. forEach ( ( e ) = > context . err ( e ) )
2022-07-11 09:14:26 +02:00
2022-03-29 00:20:10 +02:00
// Check that all obligated arguments are present. They are obligated if they don't have a preset value
for ( const arg of vis . args ) {
if ( arg . required !== true ) {
continue
}
const param = special [ arg . name ]
2022-07-11 09:14:26 +02:00
if ( param === undefined ) {
2023-10-12 16:55:26 +02:00
context . err (
` Obligated parameter ' ${ arg . name } ' in special rendering of type ${
2023-04-07 02:13:57 +02:00
vis . funcName
} not found . \ n The full special rendering specification is : ' $ { JSON . stringify (
input
) } ' \ n $ { arg . name } : $ { arg . doc } `
2022-11-02 14:44:06 +01:00
)
2022-03-29 00:20:10 +02:00
}
}
2022-07-11 09:14:26 +02:00
2022-03-29 00:20:10 +02:00
const foundLanguages = new Set < string > ( )
const translatedArgs = argNamesList
. map ( ( nm ) = > special [ nm ] )
. filter ( ( v ) = > v !== undefined )
2024-01-16 04:09:58 +01:00
. filter ( ( v ) = > Translations . isProbablyATranslation ( v ) || v [ "*" ] !== undefined )
2022-03-29 00:20:10 +02:00
for ( const translatedArg of translatedArgs ) {
for ( const ln of Object . keys ( translatedArg ) ) {
foundLanguages . add ( ln )
2022-07-11 09:14:26 +02:00
}
2022-03-29 00:20:10 +02:00
}
2022-07-11 09:14:26 +02:00
2022-05-06 12:41:24 +02:00
const before = Translations . T ( input . before )
const after = Translations . T ( input . after )
2024-06-17 04:27:08 +02:00
const clss : string = input . class !== undefined ? ":" + input . class : ""
2022-05-06 12:41:24 +02:00
2022-07-11 09:14:26 +02:00
for ( const ln of Object . keys ( before ? . translations ? ? { } ) ) {
2022-05-06 12:41:24 +02:00
foundLanguages . add ( ln )
}
2022-07-11 09:14:26 +02:00
for ( const ln of Object . keys ( after ? . translations ? ? { } ) ) {
2022-05-06 12:41:24 +02:00
foundLanguages . add ( ln )
}
2022-07-11 09:14:26 +02:00
if ( foundLanguages . size === 0 ) {
2024-01-16 04:52:05 +01:00
const args = argNamesList
. map ( ( nm ) = > RewriteSpecial . escapeStr ( special [ nm ] ? ? "" ) )
. join ( "," )
2022-07-11 09:14:26 +02:00
return {
2024-06-17 04:27:08 +02:00
"*" : ` { ${ type } ( ${ args } ) ${ clss } } ` ,
2022-07-11 09:14:26 +02:00
}
2022-03-29 00:20:10 +02:00
}
2022-07-11 09:14:26 +02:00
2022-03-29 00:20:10 +02:00
const result = { }
const languages = Array . from ( foundLanguages )
languages . sort ( )
for ( const ln of languages ) {
const args = [ ]
for ( const argName of argNamesList ) {
2022-07-29 21:09:58 +02:00
let v = special [ argName ] ? ? ""
2022-07-11 09:14:26 +02:00
if ( Translations . isProbablyATranslation ( v ) ) {
2022-07-29 21:09:58 +02:00
v = new Translation ( v ) . textFor ( ln )
}
2022-09-08 21:40:48 +02:00
2022-07-29 21:09:58 +02:00
if ( typeof v === "string" ) {
2024-01-16 04:09:58 +01:00
args . push ( RewriteSpecial . escapeStr ( v ) )
2022-07-29 21:09:58 +02:00
} else if ( typeof v === "object" ) {
2022-07-28 09:16:19 +02:00
args . push ( JSON . stringify ( v ) )
2022-07-11 09:14:26 +02:00
} else {
2022-03-29 00:20:10 +02:00
args . push ( v )
}
}
2022-05-06 12:41:24 +02:00
const beforeText = before ? . textFor ( ln ) ? ? ""
const afterText = after ? . textFor ( ln ) ? ? ""
2024-06-17 04:27:08 +02:00
result [ ln ] = ` ${ beforeText } { ${ type } ( ${ args . map ( ( a ) = > a ) . join ( "," ) } ) ${ clss } } ${ afterText } `
2022-03-29 00:20:10 +02:00
}
return result
2022-03-08 01:05:54 +01:00
}
2022-03-29 00:20:10 +02:00
/ * *
* const tr = {
* render : { special : { type : "image_carousel" , image_key : "image" } } ,
* mappings : [
* {
* if : "other_image_key" ,
* then : { special : { type : "image_carousel" , image_key : "other_image_key" } }
* }
* ]
* }
2023-10-12 16:55:26 +02:00
* const result = new RewriteSpecial ( ) . convertStrict ( tr , ConversionContext . test ( ) )
2022-03-29 00:20:10 +02:00
* const expected = { render : { '*' : "{image_carousel(image)}" } , mappings : [ { if : "other_image_key" , then : { '*' : "{image_carousel(other_image_key)}" } } ] }
* result // => expected
2022-07-11 09:14:26 +02:00
*
2022-07-29 20:04:36 +02:00
* // Should put text before if specified
2022-05-06 12:41:24 +02:00
* const tr = {
* render : { special : { type : "image_carousel" , image_key : "image" } , before : { en : "Some introduction" } } ,
* }
2023-10-12 16:55:26 +02:00
* const result = new RewriteSpecial ( ) . convertStrict ( tr , ConversionContext . test ( ) )
2022-05-06 12:41:24 +02:00
* const expected = { render : { 'en' : "Some introduction{image_carousel(image)}" } }
* result // => expected
2022-07-29 21:09:58 +02:00
*
2022-07-29 20:04:36 +02:00
* // Should put text after if specified
* const tr = {
* render : { special : { type : "image_carousel" , image_key : "image" } , after : { en : "Some footer" } } ,
* }
2023-10-12 16:55:26 +02:00
* const result = new RewriteSpecial ( ) . convertStrict ( tr , ConversionContext . test ( ) )
2022-07-29 20:04:36 +02:00
* const expected = { render : { 'en' : "{image_carousel(image)}Some footer" } }
* result // => expected
2022-03-29 00:20:10 +02:00
* /
2023-10-11 04:16:52 +02:00
convert ( json : TagRenderingConfigJson , context : ConversionContext ) : TagRenderingConfigJson {
2022-03-29 00:20:10 +02:00
json = Utils . Clone ( json )
2023-06-21 22:42:26 +02:00
const paths : ConfigMeta [ ] = tagrenderingconfigmeta
2022-03-29 00:20:10 +02:00
for ( const path of paths ) {
2023-06-21 22:42:26 +02:00
if ( path . hints . typehint !== "rendered" ) {
2022-03-29 00:20:10 +02:00
continue
}
Utils . WalkPath ( path . path , json , ( leaf , travelled ) = >
2023-10-12 16:55:26 +02:00
RewriteSpecial . convertIfNeeded ( leaf , context . enter ( travelled ) )
2022-09-08 21:40:48 +02:00
)
2022-03-29 00:20:10 +02:00
}
2022-07-11 09:14:26 +02:00
2023-10-11 04:16:52 +02:00
return json
2022-03-08 01:05:54 +01:00
}
}
2023-10-06 23:56:50 +02:00
class ExpandIconBadges extends DesugaringStep < PointRenderingConfigJson > {
2023-02-03 03:57:30 +01:00
private _expand : ExpandTagRendering
constructor ( state : DesugaringContext , layer : LayerConfigJson ) {
super ( "Expands shorthand properties on iconBadges" , [ "iconBadges" ] , "ExpandIconBadges" )
this . _expand = new ExpandTagRendering ( state , layer )
}
2023-10-11 04:16:52 +02:00
convert ( json : PointRenderingConfigJson , context : ConversionContext ) : PointRenderingConfigJson {
2023-02-03 03:57:30 +01:00
if ( ! json [ "iconBadges" ] ) {
2023-10-11 04:16:52 +02:00
return json
2023-02-03 03:57:30 +01:00
}
2023-10-06 23:56:50 +02:00
const badgesJson = json . iconBadges
2023-02-03 03:57:30 +01:00
2023-10-11 04:16:52 +02:00
const iconBadges : {
if : TagConfigJson
2023-10-12 16:55:26 +02:00
then : string | MinimalTagRenderingConfigJson
2023-10-11 04:16:52 +02:00
} [ ] = [ ]
2023-02-03 03:57:30 +01:00
for ( let i = 0 ; i < badgesJson . length ; i ++ ) {
2023-10-11 04:16:52 +02:00
const iconBadge : {
if : TagConfigJson
2023-10-12 16:55:26 +02:00
then : string | MinimalTagRenderingConfigJson
2023-10-11 04:16:52 +02:00
} = badgesJson [ i ]
const expanded = this . _expand . convert (
2023-09-17 13:45:46 +02:00
< QuestionableTagRenderingConfigJson > iconBadge . then ,
2023-10-11 04:16:52 +02:00
context . enters ( "iconBadges" , i )
2023-02-03 03:57:30 +01:00
)
2023-10-11 04:16:52 +02:00
if ( expanded === undefined ) {
2023-02-03 03:57:30 +01:00
iconBadges . push ( iconBadge )
continue
}
iconBadges . push (
2023-10-11 04:16:52 +02:00
. . . expanded . map ( ( resolved ) = > ( {
2023-02-03 03:57:30 +01:00
if : iconBadge . if ,
2023-10-12 16:55:26 +02:00
then : < MinimalTagRenderingConfigJson > resolved ,
2023-02-03 03:57:30 +01:00
} ) )
)
}
2023-10-11 04:16:52 +02:00
return { . . . json , iconBadges }
2023-02-03 03:57:30 +01:00
}
}
2023-10-06 23:56:50 +02:00
class PreparePointRendering extends Fuse < PointRenderingConfigJson > {
2023-02-03 03:57:30 +01:00
constructor ( state : DesugaringContext , layer : LayerConfigJson ) {
super (
"Prepares point renderings by expanding 'icon' and 'iconBadges'" ,
new On (
2023-10-12 16:55:26 +02:00
"marker" ,
new Each (
new On (
"icon" ,
new FirstOf ( new ExpandTagRendering ( state , layer , { applyCondition : false } ) )
)
)
2023-02-03 03:57:30 +01:00
) ,
new ExpandIconBadges ( state , layer )
)
}
}
2023-06-01 02:52:21 +02:00
class SetFullNodeDatabase extends DesugaringStep < LayerConfigJson > {
constructor ( ) {
2023-06-14 20:39:36 +02:00
super (
"sets the fullNodeDatabase-bit if needed" ,
2023-06-01 02:52:21 +02:00
[ "fullNodeDatabase" ] ,
2023-06-14 20:39:36 +02:00
"SetFullNodeDatabase"
)
2023-06-01 02:52:21 +02:00
}
2023-10-11 04:16:52 +02:00
convert ( json : LayerConfigJson , context : ConversionContext ) : LayerConfigJson {
2023-06-14 20:39:36 +02:00
const needsSpecial =
json . tagRenderings ? . some ( ( tr ) = > {
if ( typeof tr === "string" ) {
return false
}
const specs = ValidationUtils . getSpecialVisualisations ( < TagRenderingConfigJson > tr )
return specs ? . some ( ( sp ) = > sp . needsNodeDatabase )
} ) ? ? false
2023-06-01 02:52:21 +02:00
if ( ! needsSpecial ) {
2023-10-11 04:16:52 +02:00
return json
2023-06-14 20:39:36 +02:00
}
2024-02-12 14:14:25 +01:00
context . debug ( "Layer " + json . id + " needs the fullNodeDatabase" )
2023-10-11 04:16:52 +02:00
return { . . . json , fullNodeDatabase : true }
2023-06-01 02:52:21 +02:00
}
}
2023-10-06 23:56:50 +02:00
class ExpandMarkerRenderings extends DesugaringStep < IconConfigJson > {
private readonly _layer : LayerConfigJson
private readonly _state : DesugaringContext
constructor ( state : DesugaringContext , layer : LayerConfigJson ) {
super (
"Expands tagRenderings in the icons, if needed" ,
[ "icon" , "color" ] ,
"ExpandMarkerRenderings"
)
this . _layer = layer
this . _state = state
}
2023-10-11 04:16:52 +02:00
convert ( json : IconConfigJson , context : ConversionContext ) : IconConfigJson {
2023-10-06 23:56:50 +02:00
const expander = new ExpandTagRendering ( this . _state , this . _layer )
const result : IconConfigJson = { icon : undefined , color : undefined }
if ( json . icon && json . icon [ "builtin" ] ) {
2023-10-12 16:55:26 +02:00
result . icon = < MinimalTagRenderingConfigJson > (
expander . convert ( < any > json . icon , context . enter ( "icon" ) ) [ 0 ]
)
2023-10-06 23:56:50 +02:00
} else {
result . icon = json . icon
}
if ( json . color && json . color [ "builtin" ] ) {
2023-10-12 16:55:26 +02:00
result . color = < MinimalTagRenderingConfigJson > (
expander . convert ( < any > json . color , context . enter ( "color" ) ) [ 0 ]
)
2023-10-06 23:56:50 +02:00
} else {
result . color = json . color
}
2023-10-11 04:16:52 +02:00
return result
2023-10-06 23:56:50 +02:00
}
}
2023-11-22 19:39:19 +01:00
class AddFavouriteBadges extends DesugaringStep < LayerConfigJson > {
constructor ( ) {
super (
"Adds the favourite heart to the title and the rendering badges" ,
[ ] ,
"AddFavouriteBadges"
)
}
2024-01-24 23:45:20 +01:00
convert ( json : LayerConfigJson , _ : ConversionContext ) : LayerConfigJson {
2023-11-30 00:39:55 +01:00
if ( json . source === "special" || json . source === "special:library" ) {
2023-11-22 19:39:19 +01:00
return json
}
const pr = json . pointRendering ? . [ 0 ]
if ( pr ) {
pr . iconBadges ? ? = [ ]
if ( ! pr . iconBadges . some ( ( ti ) = > ti . if === "_favourite=yes" ) ) {
pr . iconBadges . push ( { if : "_favourite=yes" , then : "circle:white;heart:red" } )
}
}
return json
}
}
2023-11-19 05:05:15 +01:00
export class AddRatingBadge extends DesugaringStep < LayerConfigJson > {
constructor ( ) {
super (
"Adds the 'rating'-element if a reviews-element is used in the tagRenderings" ,
[ "titleIcons" ] ,
"AddRatingBadge"
)
}
2024-01-24 23:45:20 +01:00
convert ( json : LayerConfigJson , _ : ConversionContext ) : LayerConfigJson {
2023-11-19 05:05:15 +01:00
if ( ! json . tagRenderings ) {
return json
}
2023-11-22 19:39:19 +01:00
if ( json . titleIcons . some ( ( ti ) = > ti === "icons.rating" || ti [ "id" ] === "rating" ) ) {
// already added
return json
}
2023-12-08 00:12:21 +01:00
if ( json . id === "favourite" ) {
// handled separately
return json
}
2023-11-19 05:05:15 +01:00
2023-11-19 13:03:46 +01:00
const specialVis : Exclude < RenderingSpecification , string > [ ] = <
Exclude < RenderingSpecification , string > [ ]
> ValidationUtils . getAllSpecialVisualisations ( < any > json . tagRenderings ) . filter (
( rs ) = > typeof rs !== "string"
2023-11-19 05:05:15 +01:00
)
2023-11-19 13:03:46 +01:00
const funcs = new Set < string > ( specialVis . map ( ( rs ) = > rs . func . funcName ) )
2023-11-19 05:05:15 +01:00
2023-11-19 13:03:46 +01:00
if ( funcs . has ( "list_reviews" ) ) {
; ( < ( string | TagRenderingConfigJson ) [ ] > json . titleIcons ) . push ( "icons.rating" )
2023-11-19 05:05:15 +01:00
}
return json
}
}
2023-12-08 00:12:21 +01:00
2023-11-29 17:05:45 +01:00
export class AutoTitleIcon extends DesugaringStep < LayerConfigJson > {
constructor ( ) {
super (
"The auto-icon creates a (non-clickable) title icon based on a tagRendering which has icons" ,
[ "titleIcons" ] ,
"AutoTitleIcon"
)
}
2024-01-02 20:19:43 +01:00
private createTitleIconsBasedOn (
tr : QuestionableTagRenderingConfigJson
) : TagRenderingConfigJson | undefined {
const mappings : { if : TagConfigJson ; then : string } [ ] = tr . mappings
? . filter ( ( m ) = > m . icon !== undefined )
. map ( ( m ) = > {
const path : string = typeof m . icon === "string" ? m.icon : m.icon.path
const img = ` <img class="m-1 h-6 w-6 low-interaction rounded" src=' ${ path } '/> `
return { if : m . if , then : img }
} )
if ( ! mappings || mappings . length === 0 ) {
return undefined
}
return < TagRenderingConfigJson > {
id : "title_icon_auto_" + tr . id ,
mappings ,
}
}
2023-11-29 17:05:45 +01:00
convert ( json : LayerConfigJson , context : ConversionContext ) : LayerConfigJson {
json = { . . . json }
json . titleIcons = [ . . . json . titleIcons ]
2024-01-02 20:19:43 +01:00
const allAutoIndex = json . titleIcons . indexOf ( < any > "auto:*" )
if ( allAutoIndex >= 0 ) {
const generated = Utils . NoNull (
json . tagRenderings . map ( ( tr ) = > {
if ( typeof tr === "string" ) {
return undefined
}
return this . createTitleIconsBasedOn ( < any > tr )
} )
)
json . titleIcons . splice ( allAutoIndex , 1 , . . . generated )
return json
}
2023-11-29 17:05:45 +01:00
for ( let i = 0 ; i < json . titleIcons . length ; i ++ ) {
const titleIcon = json . titleIcons [ i ]
if ( typeof titleIcon !== "string" ) {
continue
}
if ( ! titleIcon . startsWith ( "auto:" ) ) {
continue
}
const trId = titleIcon . substring ( "auto:" . length )
2023-12-03 04:44:59 +01:00
const tr = < QuestionableTagRenderingConfigJson > (
json . tagRenderings . find ( ( tr ) = > tr [ "id" ] === trId )
)
2023-11-29 17:05:45 +01:00
if ( tr === undefined ) {
2023-12-03 04:44:59 +01:00
context . enters ( "titleIcons" , i ) . err ( "TagRendering with id " + trId + " not found" )
2023-11-29 17:05:45 +01:00
continue
}
2024-01-02 20:19:43 +01:00
const generated = this . createTitleIconsBasedOn ( tr )
if ( ! generated ) {
2023-11-29 17:05:45 +01:00
context
. enters ( "titleIcons" , i )
2023-12-03 04:44:59 +01:00
. warn (
"TagRendering with id " +
trId +
" does not have any icons, not generating an icon for this"
)
2023-11-29 17:05:45 +01:00
continue
}
2024-01-02 20:19:43 +01:00
json . titleIcons [ i ] = generated
2023-11-29 17:05:45 +01:00
}
return json
}
}
2023-11-19 05:05:15 +01:00
2023-11-02 04:35:32 +01:00
export class PrepareLayer extends Fuse < LayerConfigJson > {
2024-06-16 16:06:26 +02:00
constructor (
state : DesugaringContext ,
options ? : { addTagRenderingsToContext? : false | boolean }
) {
2023-11-02 04:35:32 +01:00
super (
2022-01-21 01:57:16 +01:00
"Fully prepares and expands a layer for the LayerConfig." ,
2022-04-06 03:06:50 +02:00
new On ( "tagRenderings" , new Each ( new RewriteSpecial ( ) ) ) ,
new On ( "tagRenderings" , new Concat ( new ExpandRewrite ( ) ) . andThenF ( Utils . Flatten ) ) ,
2024-06-16 16:06:26 +02:00
new On (
"tagRenderings" ,
( layer ) = >
new Concat (
new ExpandTagRendering ( state , layer , {
addToContext : options?.addTagRenderingsToContext ? ? false ,
} )
)
) ,
2023-04-07 03:54:11 +02:00
new On ( "tagRenderings" , new Each ( new DetectInline ( ) ) ) ,
2023-04-14 02:42:57 +02:00
new AddQuestionBox ( ) ,
2023-04-15 02:28:24 +02:00
new AddEditingElements ( state ) ,
2023-06-01 02:52:21 +02:00
new SetFullNodeDatabase ( ) ,
2023-10-12 16:55:26 +02:00
new On <
( LineRenderingConfigJson | RewritableConfigJson < LineRenderingConfigJson > ) [ ] ,
LayerConfigJson
> ( "lineRendering" , new Each ( new ExpandRewrite ( ) ) . andThenF ( Utils . Flatten ) ) ,
2023-10-06 23:56:50 +02:00
new On < PointRenderingConfigJson [ ] , LayerConfigJson > (
"pointRendering" ,
( layer ) = >
new Each ( new On ( "marker" , new Each ( new ExpandMarkerRenderings ( state , layer ) ) ) )
) ,
new On < PointRenderingConfigJson [ ] , LayerConfigJson > (
"pointRendering" ,
2023-02-03 03:57:30 +01:00
( layer ) = > new Each ( new PreparePointRendering ( state , layer ) )
2022-08-18 14:39:40 +02:00
) ,
2023-02-03 03:57:30 +01:00
new SetDefault ( "titleIcons" , [ "icons.defaults" ] ) ,
2023-11-19 13:03:46 +01:00
new AddRatingBadge ( ) ,
2023-11-22 19:39:19 +01:00
new AddFavouriteBadges ( ) ,
2023-11-29 17:05:45 +01:00
new AutoTitleIcon ( ) ,
2023-03-09 13:34:03 +01:00
new On (
"titleIcons" ,
( layer ) = >
2023-06-01 14:40:54 +02:00
new Concat ( new ExpandTagRendering ( state , layer , { noHardcodedStrings : true } ) )
2023-03-09 13:34:03 +01:00
) ,
2023-03-09 14:45:36 +01:00
new ExpandFilter ( state )
2022-01-21 01:57:16 +01:00
)
}
}