2025-06-04 00:21:28 +02:00
import {
Concat ,
DesugaringContext ,
DesugaringStep ,
Each ,
FirstOf ,
Fuse ,
On ,
SetDefault ,
} from "./Conversion"
2023-06-01 14:40:54 +02:00
import { LayerConfigJson } from "../Json/LayerConfigJson"
2025-06-04 00:21:28 +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"
2025-01-09 20:39:21 +01:00
import FilterConfigJson from "../Json/FilterConfigJson"
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"
2024-08-16 02:09:54 +02:00
import { TagUtils } from "../../../Logic/Tags/TagUtils"
2025-01-09 20:39:21 +01:00
import { ExpandFilter , PruneFilters } from "./ExpandFilter"
2025-01-11 01:18:56 +01:00
import { ExpandTagRendering } from "./ExpandTagRendering"
2022-09-14 16:29:41 +02:00
2024-09-15 02:22:31 +02:00
class AddFiltersFromTagRenderings extends DesugaringStep < LayerConfigJson > {
constructor ( ) {
2024-10-19 14:44:55 +02:00
super (
2025-05-16 15:48:55 +02:00
"AddFiltersFromTagRenderings" ,
2025-06-04 00:21:28 +02:00
'Inspects all the tagRenderings. If some tagRenderings have the `filter` attribute set, introduce those filters. This step might introduce shorthand filter names, thus \'ExpandFilter\' should be run afterwards. Can be disabled with "#filter":"no-auto"'
2024-10-19 14:44:55 +02:00
)
2024-09-15 02:22:31 +02:00
}
convert ( json : LayerConfigJson , context : ConversionContext ) : LayerConfigJson {
const noAutoFilters = json [ "#filter" ] === "no-auto"
2024-10-19 14:44:55 +02:00
if ( noAutoFilters ) {
2024-09-15 02:22:31 +02:00
return json
}
2024-10-19 14:44:55 +02:00
if ( json . filter ? . [ "sameAs" ] ) {
2024-09-15 02:22:31 +02:00
return json
}
2024-10-19 14:44:55 +02:00
const filters : ( FilterConfigJson | string ) [ ] = [ . . . ( < any > json . filter ? ? [ ] ) ]
2024-09-15 02:22:31 +02:00
function filterExists ( filterName : string ) : boolean {
return filters . some ( ( existing ) = > {
const id : string = existing [ "id" ] ? ? existing
return (
filterName === id ||
( filterName . startsWith ( "filters." ) && filterName . endsWith ( "." + id ) )
)
} )
}
for ( let i = 0 ; i < json . tagRenderings ? . length ; i ++ ) {
const tagRendering = < TagRenderingConfigJson > json . tagRenderings [ i ]
if ( ! tagRendering ? . filter ) {
continue
}
if ( tagRendering . filter === true ) {
if ( filterExists ( tagRendering [ "id" ] ) ) {
continue
}
2024-10-19 14:44:55 +02:00
filters . push (
ExpandFilter . buildFilterFromTagRendering (
tagRendering ,
context . enters ( "tagRenderings" , i , "filter" )
)
)
2024-09-15 02:22:31 +02:00
continue
}
for ( const filterName of tagRendering . filter ? ? [ ] ) {
if ( typeof filterName !== "string" ) {
context . enters ( "tagRenderings" , i , "filter" ) . err ( "Not a string: " + filterName )
}
if ( filterExists ( filterName ) ) {
// This filter has already been added
continue
}
if ( ! filterName ) {
context . err ( "Got undefined as filter expansion in " + tagRendering [ "id" ] )
continue
}
filters . push ( filterName )
}
}
2024-10-19 14:44:55 +02:00
if ( filters . length === 0 ) {
2024-09-15 02:22:31 +02:00
return json
}
return { . . . json , filter : filters }
}
}
2022-09-08 21:40:48 +02:00
2023-04-07 03:54:11 +02:00
class DetectInline extends DesugaringStep < QuestionableTagRenderingConfigJson > {
constructor ( ) {
super (
2025-05-16 15:48:55 +02:00
"DetectInline" ,
2025-06-04 00:21:28 +02:00
"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"
2023-04-07 03:54:11 +02:00
)
}
convert (
json : QuestionableTagRenderingConfigJson ,
2024-10-19 14:44:55 +02:00
context : ConversionContext
2023-10-11 04:16:52 +02:00
) : 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 " +
2025-01-28 15:42:34 +01: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
}
2024-09-02 15:01:03 +02:00
2024-10-19 14:44:55 +02:00
if ( json . render === undefined ) {
2024-10-10 23:28:05 +02:00
context . err ( "No 'render' defined" )
return json
}
2024-10-19 14:44:55 +02:00
if (
! Object . values ( json ? . render ) ? . some ( ( render ) = > render !== "{" + json . freeform . key + "}" )
) {
2024-09-02 15:01:03 +02:00
// We only render the current value, without anything more. Not worth inlining
return json
}
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 ( ) {
2025-06-18 21:40:01 +02:00
super (
"AddQuestionBox" ,
"Adds a 'questions'-object if no question element is added yet. Will ignore all elements which were previously asked for (and questions labeled with 'hidden')"
)
2023-03-31 03:28:11 +02:00
}
2024-04-13 01:16:53 +02:00
/ * *
2025-06-06 20:14:57 +02:00
* // If a general questionbox is added, should not add a new
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()}" } }]
2025-06-13 01:14:24 +02:00
*
*
* // If a partial questionbox is added, should not add "hidden" to the blacklist
* const action = new AddQuestionBox ( )
* const tagRenderings = [ { id : "questions-some-category" , render : { "*" : "{questions(whitelist,blacklist,)}" } } , { id : "questions-black" , render : { "*" : "{questions(blacklist)}" } } ]
* const conv = action . convert ( { tagRenderings } , ConversionContext . construct ( [ "test" ] , [ ] ) )
* conv . tagRenderings // => [{id:"questions-some-category", render: {"*": "{questions(whitelist,blacklist,)}" } },{id:"questions-black", render: {"*": "{questions(blacklist)}" } }, {id:"leftover-questions",labels: ["ignore-docs","added_by_default"], render: {"*": "{questions( ,hidden;blacklist;whitelist)}" } }]
2024-04-13 01:16:53 +02:00
* /
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 (
2024-10-19 14:44:55 +02:00
< QuestionableTagRenderingConfigJson [ ] > json . tagRenderings
2024-04-13 02:40:21 +02:00
) . 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 (
2024-10-19 14:44:55 +02:00
( sp ) = > sp . args . length === 0 || sp . args [ 0 ] . trim ( ) === ""
2023-03-31 03:28:11 +02:00
)
if ( noLabels . length > 1 ) {
2023-10-11 04:16:52 +02:00
context . err (
2025-06-18 21:40:01 +02:00
"Multiple 'questions'-visualisations found which would show _all_ questions. Don't do this - questions will be shown twice. Did you perhaps import all questions from another layer?"
2023-03-31 03:28:11 +02:00
)
}
2025-06-06 18:15:50 +02:00
/ * *
* We want to construct a questionbox that shows all leftover questions .
* For this , we need to determine what those leftover questions _are_ in the first place .
*
* So , we gather the labels of the layer and compare that to the labels used by previous question boxes
* /
2023-03-31 03:28:11 +02:00
// ALl labels that are used in this layer
const allLabels = new Set (
2025-06-18 21:40:01 +02:00
json . tagRenderings . flatMap (
( tr ) = > ( < QuestionableTagRenderingConfigJson > tr ) . labels ? ? [ ]
)
2023-03-31 03:28:11 +02:00
)
2025-06-06 18:15:50 +02:00
/ * *
* The essence of all questionboxes : what is whitelisted , what is blacklisted ?
* /
2025-06-18 21:40:01 +02:00
const questionBoxes : { blacklist : string [ ] ; whitelist : string [ ] } [ ] = [ ]
2023-03-31 03:28:11 +02:00
for ( const questionSpecial of questionSpecials ) {
if ( typeof questionSpecial === "string" ) {
2025-06-06 18:15:50 +02:00
// Probably a header or something
2023-03-31 03:28:11 +02:00
continue
}
2025-06-06 18:15:50 +02:00
const whitelist = questionSpecial . args [ 0 ]
2023-03-31 03:28:11 +02:00
? . split ( ";" )
? . map ( ( a ) = > a . trim ( ) )
? . filter ( ( s ) = > s != "" )
2025-06-06 18:15:50 +02:00
const blacklist = questionSpecial . args [ 1 ]
2023-03-31 03:28:11 +02:00
? . split ( ";" )
? . map ( ( a ) = > a . trim ( ) )
? . filter ( ( s ) = > s != "" )
2025-06-06 18:15:50 +02:00
for ( const usedLabel of whitelist ) {
2023-03-31 03:28:11 +02:00
if ( ! allLabels . has ( usedLabel ) ) {
2023-10-11 04:16:52 +02:00
context . err (
"This layers specifies a special question element for label `" +
2025-01-28 15:42:34 +01:00
usedLabel +
"`, but this label doesn't exist.\n" +
" Available labels are " +
Array . from ( allLabels ) . join ( ", " )
2023-03-31 03:28:11 +02:00
)
}
}
2025-06-06 18:15:50 +02:00
questionBoxes . push ( { blacklist , whitelist } )
2023-03-31 03:28:11 +02:00
}
2025-06-06 20:14:57 +02:00
if ( noLabels . length == 1 ) {
2025-06-06 18:15:50 +02:00
// We already have a questionbox handling _all_ questions
return json
}
const usedLabels : Set < string > = new Set ( )
2025-06-06 21:40:22 +02:00
usedLabels . add ( "hidden" ) // hidden is a bit a special one. As we don't show it, we consider it 'consumed' too
2025-06-06 18:15:50 +02:00
for ( const { blacklist , whitelist } of questionBoxes ) {
if ( whitelist . length > 0 && blacklist . length == 0 ) {
// All questions from "whitelist" are guaranteed to be used here
2025-06-18 21:40:01 +02:00
whitelist . forEach ( ( label ) = > usedLabels . add ( label ) )
2023-03-31 03:28:11 +02:00
}
}
2025-06-06 18:15:50 +02:00
/ * * W e s h o u l d s t i l l c h e c k t h e w e i r d q u e s t i o n b o x e s t h a t h a v e b o t h a w h i t e l i s t _ a n d _ a b l a c k l i s t .
* Can we say that the whitelisted items are fully consumed ?
* /
let needsEvaluation = true
2025-06-18 21:40:01 +02:00
let toEvaluate = questionBoxes . filter (
( q ) = > q . whitelist . length > 0 && q . blacklist . length > 0
)
2025-06-06 18:15:50 +02:00
while ( needsEvaluation && toEvaluate . length > 0 ) {
needsEvaluation = false
const toReEvaluate = [ ]
for ( const { blacklist , whitelist } of toEvaluate ) {
2025-06-18 21:40:01 +02:00
const blacklistRest = blacklist . filter ( ( label ) = > ! usedLabels . has ( label ) )
2025-06-06 18:15:50 +02:00
if ( blacklistRest . length == 0 ) {
// All items from the blacklist have been handled by a different questionbox
// We can safely say that all whitelisted items are consumed
if ( whitelist . length == 0 ) {
// Even better: this questionbox will show all leftover questions
return json
}
2025-06-18 21:40:01 +02:00
whitelist . forEach ( ( label ) = > {
2025-06-06 18:15:50 +02:00
usedLabels . add ( label )
} )
needsEvaluation = true
} else {
// Hmm, maybe in a next iteration?
toReEvaluate . push ( { blacklist , whitelist } )
}
}
toEvaluate = toReEvaluate
}
if ( toEvaluate . length > 0 ) {
// If we end up here, we have a questionbox with a whitelist _and_ a blacklist.
// We cannot unambiguously create a leftover-questions box for this
context . err (
"Could not calculate a non-ambiguous leftover questions block. A {questions()}-special rendering is found which has both a whitelist and a blacklist; where the blacklist was not fully consumed by other tagRenderings\n\t" +
2025-06-18 21:40:01 +02:00
JSON . stringify ( toEvaluate ) +
"\n\tConsumed labels are: " +
Array . from ( usedLabels ) . join ( ", " )
2025-06-06 18:15:50 +02:00
)
}
/ * 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
* /
2025-06-06 21:40:22 +02:00
2025-06-06 18:15:50 +02:00
const question : QuestionableTagRenderingConfigJson = {
id : "leftover-questions" ,
labels : [ "ignore-docs" , "added_by_default" ] ,
render : {
"*" : ` {questions( , ${ Array . from ( usedLabels ) . 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 > {
2024-06-18 03:33:11 +02:00
private readonly _desugaring : DesugaringContext
2024-06-20 04:21:29 +02:00
private readonly _addedByDefaultAtTop : QuestionableTagRenderingConfigJson [ ]
2024-06-18 03:33:11 +02:00
private readonly _addedByDefault : QuestionableTagRenderingConfigJson [ ]
2024-07-16 19:31:00 +02:00
private readonly builtinQuestions : QuestionableTagRenderingConfigJson [ ]
2024-08-16 02:09:54 +02:00
2023-04-15 02:28:24 +02:00
constructor ( desugaring : DesugaringContext ) {
2023-04-14 02:42:57 +02:00
super (
2025-05-16 15:48:55 +02:00
"AddEditingElements" ,
"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"
2023-04-14 02:42:57 +02:00
)
2023-04-15 02:28:24 +02:00
this . _desugaring = desugaring
2024-07-16 19:31:00 +02:00
this . builtinQuestions = Array . from ( this . _desugaring . tagRenderings ? . values ( ) ? ? [ ] )
2024-06-18 03:33:11 +02:00
2024-07-16 19:31:00 +02:00
this . _addedByDefaultAtTop = this . getAddedByDefaultIds ( "added_by_default_top" )
this . _addedByDefault = this . getAddedByDefaultIds ( "added_by_default" )
}
2024-06-18 03:33:11 +02:00
2024-07-16 19:31:00 +02:00
public getAddedByDefaultIds ( key : string ) : QuestionableTagRenderingConfigJson [ ] {
const addByDefault = this . builtinQuestions . filter ( ( tr ) = > tr . labels ? . indexOf ( key ) >= 0 )
const ids = new Set ( addByDefault . map ( ( tr ) = > tr . id ) )
const idsInOrder = this . _desugaring . tagRenderingOrder ? . filter ( ( id ) = > ids . has ( id ) ) ? ? [ ]
return Utils . NoNull ( idsInOrder . map ( ( id ) = > this . _desugaring . tagRenderings . get ( id ) ) )
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 (
2024-10-19 14:44:55 +02:00
< any > json . tagRenderings
2023-10-31 11:49:14 +01:00
)
2024-06-17 04:27:08 +02:00
2023-10-31 11:49:14 +01:00
const usedSpecialFunctions = new Set (
specialVisualisations . map ( ( sv ) = >
2024-10-19 14:44:55 +02:00
typeof sv === "string" ? undefined : sv . func . funcName
)
2023-10-31 11:49:14 +01:00
)
2024-06-17 04:27:08 +02:00
/***** ADD TO TOP ****/
2023-04-14 02:42:57 +02:00
2024-06-20 04:21:29 +02:00
json . tagRenderings . unshift ( . . . this . _addedByDefaultAtTop . filter ( ( tr ) = > ! allIds . has ( tr . id ) ) )
2024-06-17 04:27:08 +02:00
/***** ADD TO BOTTOM ****/
if ( ! usedSpecialFunctions . has ( "minimap" ) ) {
json . tagRenderings . push ( this . _desugaring . tagRenderings . get ( "minimap" ) )
}
2024-06-20 04:21:29 +02:00
if (
usedSpecialFunctions . has ( "image_upload" ) &&
! usedSpecialFunctions . has ( "nearby_images" )
) {
2024-06-18 03:33:11 +02:00
json . tagRenderings . push ( this . _desugaring . tagRenderings . get ( "nearby_images" ) )
}
2024-06-17 04:27:08 +02:00
2023-10-31 11:49:14 +01:00
if ( json . allowSplit && ! usedSpecialFunctions . has ( "split_button" ) ) {
2024-06-18 03:33:11 +02:00
json . tagRenderings . push ( this . _desugaring . tagRenderings . get ( "split_button" ) )
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" ,
2025-01-28 15:42:34 +01: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" ,
2025-01-28 15:42:34 +01:00
render : { "*" : "{delete_button()}" } ,
2023-11-22 19:39:19 +01:00
} )
}
2023-12-24 05:01:10 +01:00
2024-06-20 04:21:29 +02:00
json . tagRenderings . push ( . . . this . _addedByDefault . filter ( ( tr ) = > ! allIds . has ( tr . id ) ) )
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()}" } ,
2024-07-21 10:52:51 +02:00
labels : [ "ignore_docs" ] ,
2023-04-15 02:28:24 +02:00
metacondition : {
or : [
"__featureSwitchIsDebugging=true" ,
2023-05-11 02:17:41 +02:00
"mapcomplete-show_tags=full" ,
2025-01-28 15:42:34 +01:00
"mapcomplete-show_debug=yes" ,
] ,
} ,
2023-04-15 02:28:24 +02:00
}
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 (
2025-05-16 15:48:55 +02:00
"RewriteSpecial" ,
2025-06-04 00:21:28 +02:00
"Converts a 'special' translation into a regular translation which uses parameters"
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-06-18 19:40:50 +02:00
private static escapeStr ( v : string , context : ConversionContext ) : string {
2024-06-20 04:21:29 +02:00
if ( typeof v !== "string" ) {
context . err ( "Detected a non-string value where one expected a string: " + v )
return RewriteSpecial . escapeStr ( "" + v , context )
2024-06-18 19:40:50 +02:00
}
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
2025-05-30 00:29:03 +02:00
* RewriteSpecial . convertIfNeeded ( { "special" : { "type" : "qr_code" } , class : "inline" } , ConversionContext . test ( ) ) // => {'*': "{qr_code(,):inline}"}
2024-06-17 04:27:08 +02:00
*
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 ( ) )
2024-10-13 12:32:54 +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 ( ) )
2024-10-13 12:32:54 +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 & {
2025-01-28 15:42:34 +01:00
special : {
type : string
}
} )
2023-10-11 04:16:52 +02:00
| any ,
2024-10-19 14:44:55 +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 (
2024-10-19 14:44:55 +02:00
"A 'special'-block should define 'type' to indicate which visualisation should be used"
2022-03-29 00:20:10 +02:00
)
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 ,
2024-10-19 14:44:55 +02:00
( sp ) = > sp . funcName
2022-03-29 00:20:10 +02:00
)
2023-10-12 16:55:26 +02:00
context . err (
2025-04-07 19:10:11 +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://source.mapcomplete.org/MapComplete/MapComplete/src/branch/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 ,
2024-10-19 14:44:55 +02:00
( x ) = > x
2023-10-12 16:55:26 +02:00
)
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 (
2024-10-19 14:44:55 +02:00
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-18 03:33:11 +02:00
const clss : string = input . class !== undefined ? ":" + input . class : ""
2024-06-17 04:27:08 +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
2024-06-18 19:40:50 +02:00
. map ( ( nm ) = > RewriteSpecial . escapeStr ( special [ nm ] ? ? "" , context ) )
2024-01-16 04:52:05 +01:00
. join ( "," )
2022-07-11 09:14:26 +02:00
return {
2025-01-28 15:42:34 +01: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-06-18 19:40:50 +02:00
args . push ( RewriteSpecial . escapeStr ( v , context ) )
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-20 04:21:29 +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 ) = >
2024-10-19 14:44:55 +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 ) {
2025-05-16 15:48:55 +02:00
super ( "ExpandIconBadges" , "Expands shorthand properties on iconBadges" )
2023-02-03 03:57:30 +01:00
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 ++ ) {
2025-01-28 15:42:34 +01:00
const iconBadge :
| string
| {
if : TagConfigJson
then : string | MinimalTagRenderingConfigJson
} = badgesJson [ i ]
2025-01-18 02:31:15 +01:00
if ( typeof iconBadge === "string" ) {
const expanded : QuestionableTagRenderingConfigJson [ ] = this . _expand . convert (
iconBadge ,
context . enters ( "iconBadges" , i )
)
for ( const tr of expanded ) {
const condition = tr . condition
for ( const trElement of tr . mappings ) {
const showIf = TagUtils . optimzeJson ( {
2025-01-28 15:42:34 +01:00
and : Utils.NoNull ( [
condition ,
2025-01-18 02:31:15 +01:00
{
2025-01-28 15:42:34 +01:00
or : Utils.NoNull ( [ trElement . alsoShowIf , trElement . if ] ) ,
} ,
] ) ,
2025-01-18 02:31:15 +01:00
} )
if ( showIf === true ) {
2025-01-28 15:42:34 +01:00
context . warn (
"Dropping iconBadge that would be _always_ shown: " +
( trElement . icon ? ? trElement . then )
)
2025-01-18 02:31:15 +01:00
continue
}
if ( showIf === false ) {
continue
}
iconBadges . push ( {
if : showIf ,
2025-01-28 15:42:34 +01:00
then : trElement.icon ? ? trElement . then ,
2025-01-18 02:31:15 +01:00
} )
}
}
continue
}
const expanded : QuestionableTagRenderingConfigJson [ ] = this . _expand . convert (
2023-09-17 13:45:46 +02:00
< QuestionableTagRenderingConfigJson > iconBadge . then ,
2024-10-19 14:44:55 +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 ,
2025-01-28 15:42:34 +01:00
then : < MinimalTagRenderingConfigJson > resolved ,
2024-10-19 14:44:55 +02:00
} ) )
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 (
2025-01-10 14:09:54 +01:00
"Prepares point renderings by expanding 'icon' and 'iconBadges'." +
2025-01-28 15:42:34 +01:00
" A tagRendering from the host tagRenderings will be substituted in" ,
2023-02-03 03:57:30 +01:00
new On (
2023-10-12 16:55:26 +02:00
"marker" ,
new Each (
new On (
"icon" ,
2024-10-19 14:44:55 +02:00
new FirstOf ( new ExpandTagRendering ( state , layer , { applyCondition : false } ) )
)
)
2023-02-03 03:57:30 +01:00
) ,
2024-10-19 14:44:55 +02:00
new ExpandIconBadges ( state , layer )
2023-02-03 03:57:30 +01:00
)
}
}
2023-06-01 02:52:21 +02:00
class SetFullNodeDatabase extends DesugaringStep < LayerConfigJson > {
constructor ( ) {
2025-06-04 00:21:28 +02:00
super ( "SetFullNodeDatabase" , "sets the fullNodeDatabase-bit if needed" )
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 ) {
2025-06-04 00:21:28 +02:00
super ( "ExpandMarkerRenderings" , "Expands tagRenderings in the icons, if needed" )
2023-10-06 23:56:50 +02:00
this . _layer = layer
this . _state = state
}
2023-10-11 04:16:52 +02:00
convert ( json : IconConfigJson , context : ConversionContext ) : IconConfigJson {
2025-01-18 00:30:06 +01:00
const expander = new ExpandTagRendering ( this . _state , this . _layer , { applyCondition : false } )
2023-10-06 23:56:50 +02:00
const result : IconConfigJson = { icon : undefined , color : undefined }
if ( json . icon && json . icon [ "builtin" ] ) {
2025-01-18 00:30:06 +01:00
result . icon =
< MinimalTagRenderingConfigJson > (
expander . convert ( < any > json . icon , context . enter ( "icon" ) ) [ 0 ]
) ? ? json . icon
2023-10-06 23:56:50 +02:00
} else {
result . icon = json . icon
}
if ( json . color && json . color [ "builtin" ] ) {
2025-01-18 00:30:06 +01:00
result . color =
< MinimalTagRenderingConfigJson > (
expander . convert ( < any > json . color , context . enter ( "color" ) ) [ 0 ]
) ? ? json . color
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 (
2025-05-16 15:48:55 +02:00
"AddFavouriteBadges" ,
2025-06-04 00:21:28 +02:00
"Adds the favourite heart to the title and the rendering badges"
2023-11-22 19:39:19 +01:00
)
}
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 (
2025-05-16 15:48:55 +02:00
"AddRatingBadge" ,
2025-06-04 00:21:28 +02:00
"Adds the 'rating'-element if a reviews-element is used in the tagRenderings"
2023-11-19 05:05:15 +01:00
)
}
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 > [ ]
2025-01-28 15:42:34 +01:00
> ValidationUtils . getAllSpecialVisualisations ( < any > json . tagRenderings ) . filter (
2024-10-19 14:44:55 +02:00
( 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" ) ) {
2025-06-19 21:55:28 +02:00
; ( < ( 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 (
2025-05-16 15:48:55 +02:00
"AutoTitleIcon" ,
2025-06-04 00:21:28 +02:00
"The auto-icon creates a (non-clickable) title icon based on a tagRendering which has icons"
2023-11-29 17:05:45 +01:00
)
}
2024-01-02 20:19:43 +01:00
private createTitleIconsBasedOn (
2024-10-19 14:44:55 +02:00
tr : QuestionableTagRenderingConfigJson
2024-01-02 20:19:43 +01:00
) : 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 ,
2025-01-28 15:42:34 +01:00
mappings ,
2024-01-02 20:19:43 +01:00
}
}
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 )
2024-10-19 14:44:55 +02:00
} )
2024-01-02 20:19:43 +01:00
)
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 " +
2025-01-28 15:42:34 +01:00
trId +
" does not have any icons, not generating an icon for this"
2023-12-03 04:44:59 +01:00
)
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
2025-05-16 15:48:55 +02:00
class MoveUnitConfigs extends DesugaringStep < LayerConfigJson > {
constructor ( ) {
2025-06-04 00:21:28 +02:00
super (
"MoveUnitConfigs" ,
"Looks into all the tagRenderings for a 'unitConfig', moves them to the layer level"
)
2025-05-16 15:48:55 +02:00
}
convert ( json : LayerConfigJson , context : ConversionContext ) : LayerConfigJson {
json = { . . . json , units : [ . . . ( json . units ? ? [ ] ) ] }
for ( const tr of json . tagRenderings ? ? [ ] ) {
2025-06-04 00:21:28 +02:00
const qtr = < QuestionableTagRenderingConfigJson > tr
2025-05-16 15:48:55 +02:00
const unitConfig = qtr ? . freeform ? . unit
if ( ! unitConfig ) {
continue
}
json . units . push ( {
2025-06-04 00:21:28 +02:00
[ qtr . freeform . key ] : unitConfig ,
2025-05-16 15:48:55 +02:00
} )
2025-05-20 00:34:39 +02:00
// Note: we do not delete the config - this way, if the tagRendering is imported in another layer, the unit comes along
2025-05-16 15:48:55 +02:00
}
return json
}
}
2024-08-16 02:09:54 +02:00
class DeriveSource extends DesugaringStep < LayerConfigJson > {
constructor ( ) {
2024-08-23 13:13:41 +02:00
super (
2025-05-16 15:48:55 +02:00
"DeriveSource" ,
2025-06-04 00:21:28 +02:00
"If no source is given, automatically derives the osmTags by 'or'-ing all the preset tags"
2024-08-23 13:13:41 +02:00
)
2024-08-16 02:09:54 +02:00
}
public convert ( json : LayerConfigJson , context : ConversionContext ) : LayerConfigJson {
if ( json . source ) {
return json
}
if ( ! json . presets ) {
2024-08-23 13:13:41 +02:00
context . err (
2024-10-19 14:44:55 +02:00
"No source tags given. Trying to derive the source-tags based on the presets, but no presets are given"
2024-08-23 13:13:41 +02:00
)
2024-08-16 02:09:54 +02:00
return json
}
json = { . . . json }
2024-08-23 13:13:41 +02:00
const raw = { or : json.presets.map ( ( pr ) = > ( { and : pr.tags } ) ) }
2024-08-16 02:09:54 +02:00
const osmTags = TagUtils . optimzeJson ( raw )
if ( osmTags === false ) {
context . err ( "The preset-tags optimize to 'false' " + JSON . stringify ( raw ) )
return json
}
if ( osmTags === true ) {
context . err ( "The preset-tags optimize to 'true' " + JSON . stringify ( raw ) )
return json
}
2025-07-03 17:32:22 +02:00
json . source = { osmTags : { and : [ osmTags ] } }
2024-08-16 02:09:54 +02:00
return json
}
}
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 ,
2024-10-19 14:44:55 +02:00
options ? : { addTagRenderingsToContext? : false | boolean }
2024-06-16 16:06:26 +02:00
) {
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." ,
2024-08-16 02:09:54 +02:00
new DeriveSource ( ) ,
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 , {
2025-01-28 15:42:34 +01:00
addToContext : options?.addTagRenderingsToContext ? ? false ,
2024-10-19 14:44:55 +02:00
} )
)
2024-06-16 16:06:26 +02:00
) ,
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 ) = >
2024-10-19 14:44:55 +02:00
new Each ( new On ( "marker" , new Each ( new ExpandMarkerRenderings ( state , layer ) ) ) )
2023-10-06 23:56:50 +02:00
) ,
new On < PointRenderingConfigJson [ ] , LayerConfigJson > (
"pointRendering" ,
2024-10-19 14:44:55 +02: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 ) = >
2024-10-19 14:44:55 +02:00
new Concat ( new ExpandTagRendering ( state , layer , { noHardcodedStrings : true } ) )
2023-03-09 13:34:03 +01:00
) ,
2024-09-15 02:22:31 +02:00
new AddFiltersFromTagRenderings ( ) ,
2025-01-09 20:39:21 +01:00
new ExpandFilter ( state ) ,
2025-05-20 00:34:39 +02:00
new MoveUnitConfigs ( ) ,
2025-01-09 20:39:21 +01:00
new PruneFilters ( )
2022-01-21 01:57:16 +01:00
)
}
2025-01-11 02:39:08 +01:00
convert ( json : LayerConfigJson , context : ConversionContext ) : LayerConfigJson {
2025-01-18 00:30:06 +01:00
if ( json === undefined || json === null ) {
2025-01-11 02:39:08 +01:00
throw "Error: prepareLayer got null"
}
2025-05-03 23:48:35 +02:00
if (
json . source ? . [ "osmTags" ] !== undefined &&
json . source ? . [ "osmTags" ] ? . [ "and" ] === undefined
) {
2025-04-21 23:53:10 +02:00
json = { . . . json }
json . source = < any > { . . . ( < object > json . source ) }
2025-05-03 23:48:35 +02:00
json . source [ "osmTags" ] = { and : [ json . source [ "osmTags" ] ] }
2025-04-21 23:53:10 +02:00
}
2025-01-11 02:39:08 +01:00
return super . convert ( json , context )
}
2022-01-21 01:57:16 +01:00
}