2024-08-11 12:03:24 +02:00
import { DesugaringStep , Each , On } from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { ConversionContext } from "./ConversionContext"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import LayerConfig from "../LayerConfig"
import Constants from "../../Constants"
import { Utils } from "../../../Utils"
import DeleteConfig from "../DeleteConfig"
import { And } from "../../../Logic/Tags/And"
import { DoesImageExist , ValidateFilter , ValidatePointRendering } from "./Validation"
import { ValidateTagRenderings } from "./ValidateTagRenderings"
2024-08-16 02:09:54 +02:00
import { TagsFilterClosed } from "../../../Logic/Tags/TagTypes"
2024-08-11 12:03:24 +02:00
export class PrevalidateLayer extends DesugaringStep < LayerConfigJson > {
private readonly _isBuiltin : boolean
private readonly _doesImageExist : DoesImageExist
/ * *
* The paths where this layer is originally saved . Triggers some extra checks
* /
private readonly _path : string
private readonly _studioValidations : boolean
private readonly _validatePointRendering = new ValidatePointRendering ( )
constructor (
path : string ,
isBuiltin : boolean ,
doesImageExist : DoesImageExist ,
2024-08-23 13:13:41 +02:00
studioValidations : boolean
2024-08-11 12:03:24 +02:00
) {
super ( "Runs various checks against common mistakes for a layer" , [ ] , "PrevalidateLayer" )
this . _path = path
this . _isBuiltin = isBuiltin
this . _doesImageExist = doesImageExist
this . _studioValidations = studioValidations
}
convert ( json : LayerConfigJson , context : ConversionContext ) : LayerConfigJson {
if ( json . id === undefined ) {
context . enter ( "id" ) . err ( ` Not a valid layer: id is undefined ` )
} else {
if ( json . id ? . toLowerCase ( ) !== json . id ) {
context . enter ( "id" ) . err ( ` The id of a layer should be lowercase: ${ json . id } ` )
}
const layerRegex = /[a-zA-Z][a-zA-Z_0-9]+/
if ( json . id . match ( layerRegex ) === null ) {
context . enter ( "id" ) . err ( "Invalid ID. A layer ID should match " + layerRegex . source )
}
}
if ( json . source === undefined ) {
2024-08-16 02:09:54 +02:00
if ( json . presets ? . length < 1 ) {
context
. enter ( "source" )
. err (
2024-08-23 13:13:41 +02:00
"No source section is defined; please define one as data is not loaded otherwise"
2024-08-16 02:09:54 +02:00
)
}
2024-08-11 12:03:24 +02:00
} else {
if ( json . source === "special" || json . source === "special:library" ) {
} else if ( json . source && json . source [ "osmTags" ] === undefined ) {
context
. enters ( "source" , "osmTags" )
. err (
2024-08-23 13:13:41 +02:00
"No osmTags defined in the source section - these should always be present, even for geojson layer"
2024-08-11 12:03:24 +02:00
)
} else {
const osmTags = TagUtils . Tag ( json . source [ "osmTags" ] , context + "source.osmTags" )
if ( osmTags . isNegative ( ) ) {
context
. enters ( "source" , "osmTags" )
. err (
2024-11-05 13:52:23 +01:00
"The tags that will be used to load data from OpenStreetMap are all negative - this means that they all match something that _doesn't_ have a certain tag. For example, `key=` means anything without `key`. Did you perhaps mean to use `key~*`, meaning anything _with_ this key set? The tags are:\n\t" +
2024-08-23 13:13:41 +02:00
osmTags . asHumanString ( false , false , { } )
2024-08-11 12:03:24 +02:00
)
}
}
if ( json . source [ "geoJsonSource" ] !== undefined ) {
context
. enters ( "source" , "geoJsonSource" )
. err ( "Use 'geoJson' instead of 'geoJsonSource'" )
}
if ( json . source [ "geojson" ] !== undefined ) {
context
. enters ( "source" , "geojson" )
. err ( "Use 'geoJson' instead of 'geojson' (the J is a capital letter)" )
}
}
2024-10-19 14:44:55 +02:00
if ( json [ "doCount" ] !== undefined ) {
2024-09-19 17:29:55 +02:00
context . err ( "Detected 'doCount'. did you mean: isCounted ?" )
}
2024-08-11 12:03:24 +02:00
if (
json . syncSelection !== undefined &&
LayerConfig . syncSelectionAllowed . indexOf ( json . syncSelection ) < 0
) {
context
. enter ( "syncSelection" )
. err (
"Invalid sync-selection: must be one of " +
2024-08-23 13:13:41 +02:00
LayerConfig . syncSelectionAllowed . map ( ( v ) = > ` ' ${ v } ' ` ) . join ( ", " ) +
" but got '" +
json . syncSelection +
"'"
2024-08-11 12:03:24 +02:00
)
}
if ( json [ "pointRenderings" ] ? . length > 0 ) {
context
. enter ( "pointRenderings" )
. err ( "Detected a 'pointRenderingS', it is written singular" )
}
if (
! ( json . pointRendering ? . length > 0 ) &&
json . pointRendering !== null &&
json . source !== "special" &&
json . source !== "special:library"
) {
context . enter ( "pointRendering" ) . err ( "There are no pointRenderings at all..." )
}
json . pointRendering ? . forEach ( ( pr , i ) = >
2024-08-23 13:13:41 +02:00
this . _validatePointRendering . convert ( pr , context . enters ( "pointeRendering" , i ) )
2024-08-11 12:03:24 +02:00
)
if ( json [ "mapRendering" ] ) {
context . enter ( "mapRendering" ) . err ( "This layer has a legacy 'mapRendering'" )
}
if ( json . presets ? . length > 0 ) {
if ( ! ( json . pointRendering ? . length > 0 ) ) {
context . enter ( "presets" ) . warn ( "A preset is defined, but there is no pointRendering" )
}
}
if ( json . source === "special" ) {
2025-02-07 01:37:36 +01:00
if ( ! Constants . isPriviliged ( json ) ) {
2024-08-11 12:03:24 +02:00
context . err (
"Layer " +
2024-08-23 13:13:41 +02:00
json . id +
" uses 'special' as source.osmTags. However, this layer is not a privileged layer"
2024-08-11 12:03:24 +02:00
)
}
}
2024-10-19 14:44:55 +02:00
if (
this . _isBuiltin &&
json . allowMove === undefined &&
json . source [ "geoJson" ] === undefined
) {
2025-02-07 01:37:36 +01:00
if ( ! Constants . isPriviliged ( json ) ) {
2024-09-02 12:48:15 +02:00
context . err ( "Layer " + json . id + " does not have an explicit 'allowMove'" )
2024-09-02 10:29:01 +02:00
}
}
2024-08-11 12:03:24 +02:00
if ( context . hasErrors ( ) ) {
return undefined
}
if ( json . tagRenderings !== undefined && json . tagRenderings . length > 0 ) {
new On ( "tagRenderings" , new Each ( new ValidateTagRenderings ( json ) ) )
if ( json . title === undefined && json . source !== "special:library" ) {
context
. enter ( "title" )
. err (
2024-08-23 13:13:41 +02:00
"This layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error."
2024-08-11 12:03:24 +02:00
)
}
if ( json . title === null ) {
context . info (
2024-08-23 13:13:41 +02:00
"Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set."
2024-08-11 12:03:24 +02:00
)
}
{
// Check for multiple, identical builtin questions - usability for studio users
const duplicates = Utils . Duplicates (
2024-08-23 13:13:41 +02:00
< string [ ] > json . tagRenderings . filter ( ( tr ) = > typeof tr === "string" )
2024-08-11 12:03:24 +02:00
)
for ( let i = 0 ; i < json . tagRenderings . length ; i ++ ) {
const tagRendering = json . tagRenderings [ i ]
if ( typeof tagRendering === "string" && duplicates . indexOf ( tagRendering ) > 0 ) {
context
. enters ( "tagRenderings" , i )
. err ( ` This builtin question is used multiple times ( ${ tagRendering } ) ` )
}
}
}
}
if ( json [ "builtin" ] !== undefined ) {
context . err ( "This layer hasn't been expanded: " + json )
return null
}
if ( json . minzoom > Constants . minZoomLevelToAddNewPoint ) {
const c = context . enter ( "minzoom" )
const msg = ` Minzoom is ${ json . minzoom } , this should be at most ${ Constants . minZoomLevelToAddNewPoint } as a preset is set. Why? Selecting the pin for a new item will zoom in to level before adding the point. Having a greater minzoom will hide the points, resulting in possible duplicates `
if ( json . presets ? . length > 0 ) {
c . err ( msg )
} else {
c . warn ( msg )
}
}
{
// duplicate ids in tagrenderings check
const duplicates = Utils . NoNull (
2024-08-23 13:13:41 +02:00
Utils . Duplicates ( Utils . NoNull ( ( json . tagRenderings ? ? [ ] ) . map ( ( tr ) = > tr [ "id" ] ) ) )
2024-08-11 12:03:24 +02:00
)
if ( duplicates . length > 0 ) {
// It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list
context
. enter ( "tagRenderings" )
. err (
"Some tagrenderings have a duplicate id: " +
2024-08-23 13:13:41 +02:00
duplicates . join ( ", " ) +
"\n" +
JSON . stringify (
json . tagRenderings . filter ( ( tr ) = > duplicates . indexOf ( tr [ "id" ] ) >= 0 )
)
2024-08-11 12:03:24 +02:00
)
}
}
if ( json . deletion !== undefined && json . deletion instanceof DeleteConfig ) {
if ( json . deletion . softDeletionTags === undefined ) {
context
. enter ( "deletion" )
. warn ( "No soft-deletion tags in deletion block for layer " + json . id )
}
}
try {
} catch ( e ) {
context . err ( "Could not validate layer due to: " + e + e . stack )
}
if ( this . _studioValidations ) {
if ( ! json . description ) {
context . enter ( "description" ) . err ( "A description is required" )
}
if ( ! json . name ) {
context . enter ( "name" ) . err ( "A name is required" )
}
}
if ( this . _isBuiltin ) {
// Some checks for legacy elements
if ( json [ "overpassTags" ] !== undefined ) {
context . err (
"Layer " +
2024-08-23 13:13:41 +02:00
json . id +
'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": <tags>}\' instead of "overpassTags": <tags> (note: this isn\'t your fault, the custom theme generator still spits out the old format)'
2024-08-11 12:03:24 +02:00
)
}
const forbiddenTopLevel = [
"icon" ,
"wayHandling" ,
"roamingRenderings" ,
"roamingRendering" ,
"label" ,
"width" ,
"color" ,
"colour" ,
"iconOverlays" ,
]
for ( const forbiddenKey of forbiddenTopLevel ) {
if ( json [ forbiddenKey ] !== undefined )
context . err ( "Layer " + json . id + " still has a forbidden key " + forbiddenKey )
}
if ( json [ "hideUnderlayingFeaturesMinPercentage" ] !== undefined ) {
context . err (
2024-08-23 13:13:41 +02:00
"Layer " + json . id + " contains an old 'hideUnderlayingFeaturesMinPercentage'"
2024-08-11 12:03:24 +02:00
)
}
if (
json . isShown !== undefined &&
( json . isShown [ "render" ] !== undefined || json . isShown [ "mappings" ] !== undefined )
) {
context . warn ( "Has a tagRendering as `isShown`" )
}
}
if ( this . _isBuiltin ) {
// Check location of layer file
const expected : string = ` assets/layers/ ${ json . id } / ${ json . id } .json `
if ( this . _path != undefined && this . _path . indexOf ( expected ) < 0 ) {
context . err (
"Layer is in an incorrect place. The path is " +
2024-08-23 13:13:41 +02:00
this . _path +
", but expected " +
expected
2024-08-11 12:03:24 +02:00
)
}
}
if ( this . _isBuiltin ) {
// Check for correct IDs
if ( json . tagRenderings ? . some ( ( tr ) = > tr [ "id" ] === "" ) ) {
const emptyIndexes : number [ ] = [ ]
for ( let i = 0 ; i < json . tagRenderings . length ; i ++ ) {
const tagRendering = json . tagRenderings [ i ]
if ( tagRendering [ "id" ] === "" ) {
emptyIndexes . push ( i )
}
}
context
. enter ( [ "tagRenderings" , . . . emptyIndexes ] )
. err (
` Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${ emptyIndexes . join (
2024-08-23 13:13:41 +02:00
","
) } ] ) `
2024-08-11 12:03:24 +02:00
)
}
const duplicateIds = Utils . Duplicates (
2024-08-23 13:13:41 +02:00
( json . tagRenderings ? ? [ ] ) ? . map ( ( f ) = > f [ "id" ] ) . filter ( ( id ) = > id !== "questions" )
2024-08-11 12:03:24 +02:00
)
if ( duplicateIds . length > 0 && ! Utils . runningFromConsole ) {
context
. enter ( "tagRenderings" )
. err ( ` Some tagRenderings have a duplicate id: ${ duplicateIds } ` )
}
if ( json . description === undefined ) {
if ( typeof json . source === null ) {
context . err ( "A priviliged layer must have a description" )
} else {
context . warn ( "A builtin layer should have a description" )
}
}
}
if ( json . filter ) {
new On ( "filter" , new Each ( new ValidateFilter ( ) ) ) . convert ( json , context )
}
if ( json . tagRenderings !== undefined ) {
new On (
"tagRenderings" ,
2024-08-23 13:13:41 +02:00
new Each ( new ValidateTagRenderings ( json , this . _doesImageExist ) )
2024-08-11 12:03:24 +02:00
) . convert ( json , context )
}
if ( json . pointRendering !== null && json . pointRendering !== undefined ) {
if ( ! Array . isArray ( json . pointRendering ) ) {
throw (
"pointRendering in " +
json . id +
" is not iterable, it is: " +
typeof json . pointRendering
)
}
for ( let i = 0 ; i < json . pointRendering . length ; i ++ ) {
const pointRendering = json . pointRendering [ i ]
if ( pointRendering . marker === undefined ) {
continue
}
for ( const icon of pointRendering ? . marker ) {
const indexM = pointRendering ? . marker . indexOf ( icon )
if ( ! icon . icon ) {
continue
}
if ( icon . icon [ "condition" ] ) {
context
. enters ( "pointRendering" , i , "marker" , indexM , "icon" , "condition" )
. err (
2024-08-23 13:13:41 +02:00
"Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead."
2024-08-11 12:03:24 +02:00
)
}
}
}
}
if ( json . presets !== undefined ) {
if ( typeof json . source === "string" ) {
context . enter ( "presets" ) . err ( "A special layer cannot have presets" )
}
2024-08-16 02:09:54 +02:00
let baseTags : TagsFilterClosed
if ( json . source ) {
// Check that a preset will be picked up by the layer itself
baseTags = TagUtils . Tag ( json . source [ "osmTags" ] )
}
2024-08-11 12:03:24 +02:00
for ( let i = 0 ; i < json . presets . length ; i ++ ) {
const preset = json . presets [ i ]
if ( ! preset ) {
context . enters ( "presets" , i ) . err ( "This preset is undefined" )
continue
}
if ( ! preset . tags ) {
context . enters ( "presets" , i , "tags" ) . err ( "No tags defined for this preset" )
continue
}
if ( ! preset . tags ) {
context . enters ( "presets" , i , "title" ) . err ( "No title defined for this preset" )
}
const tags = new And ( preset . tags . map ( ( t ) = > TagUtils . Tag ( t ) ) )
const properties = { }
for ( const tag of tags . asChange ( { id : "node/-1" } ) ) {
properties [ tag . k ] = tag . v
}
2024-08-23 13:13:41 +02:00
if ( baseTags ) {
2024-08-16 02:09:54 +02:00
const doMatch = baseTags . matchesProperties ( properties )
if ( ! doMatch ) {
context
. enters ( "presets" , i , "tags" )
. err (
"This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
2024-08-23 13:13:41 +02:00
tags . asHumanString ( false , false , { } ) +
"\n The required tags are: " +
baseTags . asHumanString ( false , false , { } )
2024-08-16 02:09:54 +02:00
)
}
2024-08-11 12:03:24 +02:00
}
}
}
return json
}
}