2023-11-30 00:39:55 +01:00
import { Bypass , Conversion , DesugaringStep , Each , Fuse , On } from "./Conversion"
2023-09-21 15:29:34 +02:00
import { LayerConfigJson } from "../Json/LayerConfigJson"
import LayerConfig from "../LayerConfig"
import { Utils } from "../../../Utils"
import Constants from "../../Constants"
import { Translation } from "../../../UI/i18n/Translation"
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
import LayoutConfig from "../LayoutConfig"
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import { ExtractImages } from "./FixImages"
import { And } from "../../../Logic/Tags/And"
import Translations from "../../../UI/i18n/Translations"
import FilterConfigJson from "../Json/FilterConfigJson"
import DeleteConfig from "../DeleteConfig"
2024-02-23 11:44:56 +01:00
import {
MappingConfigJson ,
QuestionableTagRenderingConfigJson ,
} from "../Json/QuestionableTagRenderingConfigJson"
2023-09-21 15:29:34 +02:00
import Validators from "../../../UI/InputElement/Validators"
import TagRenderingConfig from "../TagRenderingConfig"
import { parse as parse_html } from "node-html-parser"
2023-09-24 00:25:10 +02:00
import PresetConfig from "../PresetConfig"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
2023-11-02 04:35:32 +01:00
import { Translatable } from "../Json/Translatable"
import { ConversionContext } from "./ConversionContext"
2024-01-03 18:24:00 +01:00
import { AvailableRasterLayers } from "../../RasterLayers"
2024-01-12 23:19:31 +01:00
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
2024-05-13 17:21:40 +02:00
import NameSuggestionIndex from "../../../Logic/Web/NameSuggestionIndex"
2024-07-09 00:37:23 +02:00
import { Tag } from "../../../Logic/Tags/Tag"
2022-02-04 00:44:09 +01:00
2023-12-02 03:19:50 +01:00
class ValidateLanguageCompleteness extends DesugaringStep < LayoutConfig > {
2023-09-21 15:29:34 +02:00
private readonly _languages : string [ ]
2022-02-04 00:44:09 +01:00
constructor ( . . . languages : string [ ] ) {
2022-02-14 02:26:03 +01:00
super (
"Checks that the given object is fully translated in the specified languages" ,
[ ] ,
2024-08-02 19:06:14 +02:00
"ValidateLanguageCompleteness" ,
2023-09-21 15:29:34 +02:00
)
this . _languages = languages ? ? [ "en" ]
2022-02-04 00:44:09 +01:00
}
2023-12-02 03:19:50 +01:00
convert ( obj : LayoutConfig , context : ConversionContext ) : LayoutConfig {
const origLayers = obj . layers
obj . layers = [ . . . obj . layers ] . filter ( ( l ) = > l [ "id" ] !== "favourite" )
2023-09-21 15:29:34 +02:00
const translations = Translation . ExtractAllTranslationsFrom ( obj )
2022-04-06 03:06:50 +02:00
for ( const neededLanguage of this . _languages ) {
2022-02-04 00:44:09 +01:00
translations
. filter (
( t ) = >
t . tr . translations [ neededLanguage ] === undefined &&
2024-08-02 19:06:14 +02:00
t . tr . translations [ "*" ] === undefined ,
2022-09-08 21:40:48 +02:00
)
2022-02-04 00:44:09 +01:00
. forEach ( ( missing ) = > {
2023-10-11 04:16:52 +02:00
context
. enter ( missing . context . split ( "." ) )
. err (
` The theme ${ obj . id } should be translation-complete for ` +
2024-08-02 19:06:14 +02:00
neededLanguage +
", but it lacks a translation for " +
missing . context +
".\n\tThe known translation is " +
missing . tr . textFor ( "en" ) ,
2023-10-11 04:16:52 +02:00
)
2023-09-21 15:29:34 +02:00
} )
2022-02-04 00:44:09 +01:00
}
2023-12-02 03:19:50 +01:00
obj . layers = origLayers
2023-10-11 04:16:52 +02:00
return obj
2022-02-04 00:44:09 +01:00
}
}
2022-07-06 12:57:23 +02:00
export class DoesImageExist extends DesugaringStep < string > {
2023-09-21 15:29:34 +02:00
private readonly _knownImagePaths : Set < string >
private readonly _ignore? : Set < string >
private readonly doesPathExist : ( path : string ) = > boolean = undefined
2022-09-08 21:40:48 +02:00
2022-07-06 12:57:23 +02:00
constructor (
knownImagePaths : Set < string > ,
2023-02-03 03:57:30 +01:00
checkExistsSync : ( path : string ) = > boolean = undefined ,
2024-08-02 19:06:14 +02:00
ignore? : Set < string > ,
2022-07-06 12:57:23 +02:00
) {
2023-09-21 15:29:34 +02:00
super ( "Checks if an image exists" , [ ] , "DoesImageExist" )
this . _ignore = ignore
this . _knownImagePaths = knownImagePaths
this . doesPathExist = checkExistsSync
2022-07-06 11:14:19 +02:00
}
2023-10-11 04:16:52 +02:00
convert ( image : string , context : ConversionContext ) : string {
2023-02-03 03:57:30 +01:00
if ( this . _ignore ? . has ( image ) ) {
2023-10-11 04:16:52 +02:00
return image
2023-02-03 03:57:30 +01:00
}
2022-07-06 11:14:19 +02:00
if ( image . indexOf ( "{" ) >= 0 ) {
2023-10-30 16:32:43 +01:00
context . debug ( "Ignoring image with { in the path: " + image )
2023-10-11 04:16:52 +02:00
return image
2022-07-06 11:14:19 +02:00
}
if ( image === "assets/SocialImage.png" ) {
2023-10-11 04:16:52 +02:00
return image
2022-07-06 11:14:19 +02:00
}
if ( image . match ( /[a-z]*/ ) ) {
2023-11-19 04:38:34 +01:00
if ( Constants . defaultPinIcons . indexOf ( image ) >= 0 ) {
2022-07-06 11:14:19 +02:00
// This is a builtin img, e.g. 'checkmark' or 'crosshair'
2023-10-11 04:16:52 +02:00
return image
2022-07-06 11:14:19 +02:00
}
}
2022-09-08 21:40:48 +02:00
2023-04-02 02:59:20 +02:00
if ( image . startsWith ( "<" ) && image . endsWith ( ">" ) ) {
// This is probably HTML, you're on your own here
2023-10-11 04:16:52 +02:00
return image
2023-04-02 02:59:20 +02:00
}
2022-07-08 03:14:55 +02:00
if ( ! this . _knownImagePaths . has ( image ) ) {
2022-07-06 12:57:23 +02:00
if ( this . doesPathExist === undefined ) {
2023-10-11 04:16:52 +02:00
context . err (
2024-08-02 19:06:14 +02:00
` Image with path ${ image } not found or not attributed; it is used in ${ context } ` ,
2023-09-21 15:29:34 +02:00
)
2022-07-06 12:57:23 +02:00
} else if ( ! this . doesPathExist ( image ) ) {
2023-10-11 04:16:52 +02:00
context . err (
2024-08-02 19:06:14 +02:00
` Image with path ${ image } does not exist. \ n Check for typo's and missing directories in the path. ` ,
2023-09-21 15:29:34 +02:00
)
2022-07-06 12:57:23 +02:00
} else {
2023-10-11 04:16:52 +02:00
context . err (
2024-08-02 19:06:14 +02:00
` Image with path ${ image } is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info ` ,
2023-09-21 15:29:34 +02:00
)
2022-07-06 11:14:19 +02:00
}
}
2023-10-11 04:16:52 +02:00
return image
2022-07-06 11:14:19 +02:00
}
}
2023-10-30 13:45:44 +01:00
export class ValidateTheme extends DesugaringStep < LayoutConfigJson > {
2024-01-03 18:24:00 +01:00
private static readonly _availableLayers = AvailableRasterLayers . allIds ( )
2022-02-04 00:44:09 +01:00
/ * *
* The paths where this layer is originally saved . Triggers some extra checks
* @private
* /
2023-09-21 15:29:34 +02:00
private readonly _path? : string
private readonly _isBuiltin : boolean
2023-02-03 03:57:30 +01:00
//private readonly _sharedTagRenderings: Map<string, any>
2023-09-21 15:29:34 +02:00
private readonly _validateImage : DesugaringStep < string >
private readonly _extractImages : ExtractImages = undefined
2022-09-08 21:40:48 +02:00
2022-07-06 12:57:23 +02:00
constructor (
doesImageExist : DoesImageExist ,
path : string ,
isBuiltin : boolean ,
2024-08-02 19:06:14 +02:00
sharedTagRenderings? : Set < string > ,
2022-07-06 12:57:23 +02:00
) {
2023-09-21 15:29:34 +02:00
super ( "Doesn't change anything, but emits warnings and errors" , [ ] , "ValidateTheme" )
this . _validateImage = doesImageExist
this . _path = path
this . _isBuiltin = isBuiltin
2023-02-03 03:57:30 +01:00
if ( sharedTagRenderings ) {
2023-09-21 15:29:34 +02:00
this . _extractImages = new ExtractImages ( this . _isBuiltin , sharedTagRenderings )
2023-02-03 03:57:30 +01:00
}
2022-02-04 00:44:09 +01:00
}
2023-10-11 04:16:52 +02:00
convert ( json : LayoutConfigJson , context : ConversionContext ) : LayoutConfigJson {
2023-09-21 15:29:34 +02:00
const theme = new LayoutConfig ( json , this . _isBuiltin )
2022-02-04 00:44:09 +01:00
{
// Legacy format checks
if ( this . _isBuiltin ) {
if ( json [ "units" ] !== undefined ) {
2023-10-11 04:16:52 +02:00
context . err (
2022-02-04 00:44:09 +01:00
"The theme " +
2024-08-02 19:06:14 +02:00
json . id +
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) " ,
2023-09-21 15:29:34 +02:00
)
2022-02-04 00:44:09 +01:00
}
if ( json [ "roamingRenderings" ] !== undefined ) {
2023-10-11 04:16:52 +02:00
context . err (
2022-02-04 00:44:09 +01:00
"Theme " +
2024-08-02 19:06:14 +02:00
json . id +
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead" ,
2023-09-21 15:29:34 +02:00
)
2022-02-04 00:44:09 +01:00
}
}
}
2023-10-30 13:45:44 +01:00
if ( ! json . title ) {
context . enter ( "title" ) . err ( ` The theme ${ json . id } does not have a title defined. ` )
}
2024-01-23 22:03:22 +01:00
if ( ! json . icon ) {
2024-01-13 01:51:19 +01:00
context . enter ( "icon" ) . err ( "A theme should have an icon" )
}
2023-02-03 03:57:30 +01:00
if ( this . _isBuiltin && this . _extractImages !== undefined ) {
2022-02-10 23:16:14 +01:00
// Check images: are they local, are the licenses there, is the theme icon square, ...
2023-10-11 04:16:52 +02:00
const images = this . _extractImages . convert ( json , context . inOperation ( "ValidateTheme" ) )
2023-09-21 15:29:34 +02:00
const remoteImages = images . filter ( ( img ) = > img . path . indexOf ( "http" ) == 0 )
2022-02-09 22:37:21 +01:00
for ( const remoteImage of remoteImages ) {
2023-10-11 04:16:52 +02:00
context . err (
2022-02-09 22:37:21 +01:00
"Found a remote image: " +
2024-08-02 19:06:14 +02:00
remoteImage . path +
" in theme " +
json . id +
", please download it." ,
2023-09-21 15:29:34 +02:00
)
2022-02-09 22:37:21 +01:00
}
for ( const image of images ) {
2023-10-11 04:16:52 +02:00
this . _validateImage . convert ( image . path , context . enters ( image . context ) )
2022-02-09 22:37:21 +01:00
}
}
2022-02-17 23:54:14 +01:00
2022-02-04 00:44:09 +01:00
try {
2022-09-24 03:33:09 +02:00
if ( this . _isBuiltin ) {
if ( theme . id !== theme . id . toLowerCase ( ) ) {
2023-10-11 04:16:52 +02:00
context . err ( "Theme ids should be in lowercase, but it is " + theme . id )
2022-09-24 03:33:09 +02:00
}
2022-02-04 00:44:09 +01:00
2022-09-24 03:33:09 +02:00
const filename = this . _path . substring (
this . _path . lastIndexOf ( "/" ) + 1 ,
2024-08-02 19:06:14 +02:00
this . _path . length - 5 ,
2023-09-21 15:29:34 +02:00
)
2022-09-24 03:33:09 +02:00
if ( theme . id !== filename ) {
2023-10-11 04:16:52 +02:00
context . err (
2022-09-24 03:33:09 +02:00
"Theme ids should be the same as the name.json, but we got id: " +
2024-08-02 19:06:14 +02:00
theme . id +
" and filename " +
filename +
" (" +
this . _path +
")" ,
2023-09-21 15:29:34 +02:00
)
2022-09-24 03:33:09 +02:00
}
2023-10-11 04:16:52 +02:00
this . _validateImage . convert ( theme . icon , context . enter ( "icon" ) )
2022-02-04 00:44:09 +01:00
}
2023-09-24 00:25:10 +02:00
const dups = Utils . Duplicates ( json . layers . map ( ( layer ) = > layer [ "id" ] ) )
2022-02-04 00:44:09 +01:00
if ( dups . length > 0 ) {
2023-10-11 04:16:52 +02:00
context . err (
2024-08-02 19:06:14 +02:00
` The theme ${ json . id } defines multiple layers with id ${ dups . join ( ", " ) } ` ,
2023-09-21 15:29:34 +02:00
)
2022-02-04 00:44:09 +01:00
}
if ( json [ "mustHaveLanguage" ] !== undefined ) {
2023-10-11 04:16:52 +02:00
new ValidateLanguageCompleteness ( . . . json [ "mustHaveLanguage" ] ) . convert (
theme ,
2024-08-02 19:06:14 +02:00
context ,
2023-10-11 04:16:52 +02:00
)
2022-02-04 00:44:09 +01:00
}
2022-10-04 13:50:24 +02:00
if ( ! json . hideFromOverview && theme . id !== "personal" && this . _isBuiltin ) {
2022-06-23 12:31:47 +02:00
// The first key in the the title-field must be english, otherwise the title in the loading page will be the different language
2023-09-21 15:29:34 +02:00
const targetLanguage = theme . title . SupportedLanguages ( ) [ 0 ]
2022-07-06 12:57:23 +02:00
if ( targetLanguage !== "en" ) {
2023-10-11 04:16:52 +02:00
context . err (
2024-08-02 19:06:14 +02:00
` TargetLanguage is not 'en' for public theme ${ theme . id } , it is ${ targetLanguage } . Move 'en' up in the title of the theme and set it as the first key ` ,
2023-09-21 15:29:34 +02:00
)
2022-06-23 12:31:47 +02:00
}
2022-07-06 12:57:23 +02:00
2022-02-16 22:18:58 +01:00
// Official, public themes must have a full english translation
2023-10-11 04:16:52 +02:00
new ValidateLanguageCompleteness ( "en" ) . convert ( theme , context )
2022-02-16 02:23:50 +01:00
}
2022-02-04 00:44:09 +01:00
} catch ( e ) {
2024-01-13 01:51:19 +01:00
console . error ( e )
context . err ( "Could not validate the theme due to: " + e )
2022-02-04 00:44:09 +01:00
}
2023-09-24 00:25:10 +02:00
if ( theme . id !== "personal" ) {
2023-10-11 04:16:52 +02:00
new DetectDuplicatePresets ( ) . convert ( theme , context )
2023-09-24 00:25:10 +02:00
}
2023-10-30 13:45:44 +01:00
if ( ! theme . title ) {
context . enter ( "title" ) . err ( "A theme must have a title" )
}
if ( ! theme . description ) {
context . enter ( "description" ) . err ( "A theme must have a description" )
}
if ( theme . overpassUrl && typeof theme . overpassUrl === "string" ) {
context
. enter ( "overpassUrl" )
. err ( "The overpassURL is a string, use a list of strings instead. Wrap it with [ ]" )
}
2024-01-03 18:24:00 +01:00
if ( json . defaultBackgroundId ) {
const backgroundId = json . defaultBackgroundId
const isCategory =
backgroundId === "photo" || backgroundId === "map" || backgroundId === "osmbasedmap"
if ( ! isCategory && ! ValidateTheme . _availableLayers . has ( backgroundId ) ) {
2024-03-25 03:45:24 +01:00
const options = Array . from ( ValidateTheme . _availableLayers )
2024-04-13 02:40:21 +02:00
const nearby = Utils . sortedByLevenshteinDistance ( backgroundId , options , ( t ) = > t )
2024-01-03 18:24:00 +01:00
context
. enter ( "defaultBackgroundId" )
2024-04-13 02:40:21 +02:00
. err (
` This layer ID is not known: ${ backgroundId } . Perhaps you meant one of ${ nearby
. slice ( 0 , 5 )
2024-08-02 19:06:14 +02:00
. join ( ", " ) } ` ,
2024-04-13 02:40:21 +02:00
)
2024-01-03 18:24:00 +01:00
}
}
2024-01-23 22:03:22 +01:00
for ( let i = 0 ; i < theme . layers . length ; i ++ ) {
const layer = theme . layers [ i ]
if ( ! layer . id . match ( "[a-z][a-z0-9_]*" ) ) {
2024-02-15 17:48:26 +01:00
context
. enters ( "layers" , i , "id" )
. err ( "Invalid ID:" + layer . id + "should match [a-z][a-z0-9_]*" )
2024-01-23 22:03:22 +01:00
}
}
2023-10-11 04:16:52 +02:00
return json
2022-02-04 00:44:09 +01:00
}
}
export class ValidateThemeAndLayers extends Fuse < LayoutConfigJson > {
2022-07-06 12:57:23 +02:00
constructor (
doesImageExist : DoesImageExist ,
path : string ,
isBuiltin : boolean ,
2024-08-02 19:06:14 +02:00
sharedTagRenderings? : Set < string > ,
2022-07-06 12:57:23 +02:00
) {
2022-02-04 00:44:09 +01:00
super (
"Validates a theme and the contained layers" ,
2022-07-06 12:57:23 +02:00
new ValidateTheme ( doesImageExist , path , isBuiltin , sharedTagRenderings ) ,
2023-10-12 16:55:26 +02:00
new On (
"layers" ,
new Each (
2023-11-30 00:39:55 +01:00
new Bypass (
( layer ) = > Constants . added_by_default . indexOf ( < any > layer . id ) < 0 ,
2024-08-02 19:06:14 +02:00
new ValidateLayerConfig ( undefined , isBuiltin , doesImageExist , false , true ) ,
) ,
) ,
) ,
2023-09-21 15:29:34 +02:00
)
2022-02-04 00:44:09 +01:00
}
}
2022-02-10 23:16:14 +01:00
class OverrideShadowingCheck extends DesugaringStep < LayoutConfigJson > {
2022-02-04 00:44:09 +01:00
constructor ( ) {
2022-02-17 23:54:14 +01:00
super (
"Checks that an 'overrideAll' does not override a single override" ,
[ ] ,
2024-08-02 19:06:14 +02:00
"OverrideShadowingCheck" ,
2023-09-21 15:29:34 +02:00
)
2022-02-04 00:44:09 +01:00
}
2023-10-11 04:16:52 +02:00
convert ( json : LayoutConfigJson , context : ConversionContext ) : LayoutConfigJson {
2023-09-21 15:29:34 +02:00
const overrideAll = json . overrideAll
2022-02-10 23:16:14 +01:00
if ( overrideAll === undefined ) {
2023-10-11 04:16:52 +02:00
return json
2022-02-04 00:44:09 +01:00
}
2022-02-10 23:16:14 +01:00
2023-09-21 15:29:34 +02:00
const withOverride = json . layers . filter ( ( l ) = > l [ "override" ] !== undefined )
2022-02-04 00:44:09 +01:00
for ( const layer of withOverride ) {
for ( const key in overrideAll ) {
2022-07-19 09:46:06 +02:00
if ( key . endsWith ( "+" ) || key . startsWith ( "+" ) ) {
// This key will _add_ to the list, not overwrite it - so no warning is needed
2023-09-21 15:29:34 +02:00
continue
2022-07-19 09:46:06 +02:00
}
2022-02-10 23:16:14 +01:00
if (
layer [ "override" ] [ key ] !== undefined ||
layer [ "override" ] [ "=" + key ] !== undefined
) {
const w =
"The override of layer " +
JSON . stringify ( layer [ "builtin" ] ) +
" has a shadowed property: " +
key +
2023-09-21 15:29:34 +02:00
" is overriden by overrideAll of the theme"
2023-10-11 04:16:52 +02:00
context . err ( w )
2022-02-10 23:16:14 +01:00
}
2022-02-04 00:44:09 +01:00
}
}
2022-02-10 23:16:14 +01:00
2023-10-11 04:16:52 +02:00
return json
2022-02-04 00:44:09 +01:00
}
}
2022-06-20 01:42:30 +02:00
class MiscThemeChecks extends DesugaringStep < LayoutConfigJson > {
2022-02-19 17:39:16 +01:00
constructor ( ) {
2023-09-21 15:29:34 +02:00
super ( "Miscelleanous checks on the theme" , [ ] , "MiscThemesChecks" )
2022-02-19 17:39:16 +01:00
}
2022-06-20 01:42:30 +02:00
2023-10-11 04:16:52 +02:00
convert ( json : LayoutConfigJson , context : ConversionContext ) : LayoutConfigJson {
2022-06-20 01:42:30 +02:00
if ( json . id !== "personal" && ( json . layers === undefined || json . layers . length === 0 ) ) {
2023-10-11 04:16:52 +02:00
context . err ( "The theme " + json . id + " has no 'layers' defined" )
2022-04-22 03:17:40 +02:00
}
2024-08-02 19:06:14 +02:00
if ( ! Array . isArray ( json . layers ) ) {
context . enter ( "layers" ) . err ( "The 'layers'-field should be an array, but it is not. Did you pase a layer identifier and forget to add the '[' and ']'?" )
}
2022-06-20 01:42:30 +02:00
if ( json . socialImage === "" ) {
2023-10-11 04:16:52 +02:00
context . warn ( "Social image for theme " + json . id + " is the emtpy string" )
2023-09-21 15:29:34 +02:00
}
2024-02-22 01:39:42 +01:00
if ( json [ "clustering" ] ) {
context . warn ( "Obsolete field `clustering` is still around" )
}
2024-08-10 15:36:47 +02:00
if ( json . layers === undefined ) {
context . err ( "This theme has no layers defined" )
} else {
2024-01-23 22:03:22 +01:00
for ( let i = 0 ; i < json . layers . length ; i ++ ) {
const l = json . layers [ i ]
if ( l [ "override" ] ? . [ "source" ] === undefined ) {
continue
}
if ( l [ "override" ] ? . [ "source" ] ? . [ "geoJson" ] ) {
continue // We don't care about external data as we won't cache it anyway
}
if ( l [ "override" ] [ "id" ] !== undefined ) {
continue
}
2024-02-15 17:48:26 +01:00
context
. enters ( "layers" , i )
. err ( "A layer which changes the source-tags must also change the ID" )
2024-01-23 22:03:22 +01:00
}
}
2024-02-20 02:56:23 +01:00
if ( json [ "overideAll" ] ) {
context
. enter ( "overideAll" )
. err (
2024-08-02 19:06:14 +02:00
"'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them." ,
2024-02-20 02:56:23 +01:00
)
}
2023-10-11 04:16:52 +02:00
return json
2022-02-19 17:39:16 +01:00
}
}
2022-02-10 23:16:14 +01:00
export class PrevalidateTheme extends Fuse < LayoutConfigJson > {
2022-02-04 00:44:09 +01:00
constructor ( ) {
super (
"Various consistency checks on the raw JSON" ,
2022-04-22 03:17:40 +02:00
new MiscThemeChecks ( ) ,
2024-08-02 19:06:14 +02:00
new OverrideShadowingCheck ( ) ,
2023-09-21 15:29:34 +02:00
)
2022-02-04 00:44:09 +01:00
}
}
2023-07-28 14:11:00 +02:00
export class DetectConflictingAddExtraTags extends DesugaringStep < TagRenderingConfigJson > {
constructor ( ) {
2023-09-01 16:06:22 +02:00
super (
2023-11-09 15:42:15 +01:00
"The `if`-part in a mapping might set some keys. Those keys are not allowed to be set in the `addExtraTags`, as this might result in conflicting values" ,
2023-09-01 16:06:22 +02:00
[ ] ,
2024-08-02 19:06:14 +02:00
"DetectConflictingAddExtraTags" ,
2023-09-21 15:29:34 +02:00
)
2023-07-28 14:11:00 +02:00
}
2023-10-11 04:16:52 +02:00
convert ( json : TagRenderingConfigJson , context : ConversionContext ) : TagRenderingConfigJson {
2023-07-28 14:11:00 +02:00
if ( ! ( json . mappings ? . length > 0 ) ) {
2023-10-11 04:16:52 +02:00
return json
2023-07-28 14:11:00 +02:00
}
2023-11-02 04:35:32 +01:00
try {
2024-04-08 01:58:07 +02:00
const tagRendering = new TagRenderingConfig ( json , context . path . join ( "." ) )
2023-07-28 14:11:00 +02:00
2023-11-02 04:35:32 +01:00
for ( let i = 0 ; i < tagRendering . mappings . length ; i ++ ) {
const mapping = tagRendering . mappings [ i ]
if ( ! mapping . addExtraTags ) {
continue
}
const keysInMapping = new Set ( mapping . if . usedKeys ( ) )
const keysInAddExtraTags = mapping . addExtraTags . map ( ( t ) = > t . key )
const duplicateKeys = keysInAddExtraTags . filter ( ( k ) = > keysInMapping . has ( k ) )
if ( duplicateKeys . length > 0 ) {
context
. enters ( "mappings" , i )
. err (
"AddExtraTags overrides a key that is set in the `if`-clause of this mapping. Selecting this answer might thus first set one value (needed to match as answer) and then override it with a different value, resulting in an unsaveable question. The offending `addExtraTags` is " +
2024-08-02 19:06:14 +02:00
duplicateKeys . join ( ", " ) ,
2023-11-02 04:35:32 +01:00
)
}
2023-07-28 14:11:00 +02:00
}
2023-11-02 04:35:32 +01:00
return json
} catch ( e ) {
2024-01-13 01:51:19 +01:00
context . err ( "Could not check for conflicting extra tags due to: " + e )
2023-11-02 04:35:32 +01:00
return undefined
}
2023-07-28 14:11:00 +02:00
}
}
2023-11-09 15:42:15 +01:00
export class DetectNonErasedKeysInMappings extends DesugaringStep < QuestionableTagRenderingConfigJson > {
constructor ( ) {
super (
"A tagRendering might set a freeform key (e.g. `name` and have an option that _should_ erase this name, e.g. `noname=yes`). Under normal circumstances, every mapping/freeform should affect all touched keys" ,
[ ] ,
2024-08-02 19:06:14 +02:00
"DetectNonErasedKeysInMappings" ,
2023-11-09 15:42:15 +01:00
)
}
convert (
json : QuestionableTagRenderingConfigJson ,
2024-08-02 19:06:14 +02:00
context : ConversionContext ,
2023-11-09 15:42:15 +01:00
) : QuestionableTagRenderingConfigJson {
if ( json . multiAnswer ) {
// No need to check this here, this has its own validation
return json
}
if ( ! json . question ) {
// No need to check the writable tags, as this cannot write
return json
}
2024-01-03 14:57:26 +01:00
2023-11-09 15:42:15 +01:00
function addAll ( keys : { forEach : ( f : ( s : string ) = > void ) = > void } , addTo : Set < string > ) {
keys ? . forEach ( ( k ) = > addTo . add ( k ) )
}
const freeformKeys : Set < string > = new Set ( )
if ( json . freeform ) {
freeformKeys . add ( json . freeform . key )
for ( const tag of json . freeform . addExtraTags ? ? [ ] ) {
const tagParsed = TagUtils . Tag ( tag )
addAll ( tagParsed . usedKeys ( ) , freeformKeys )
}
}
const mappingKeys : Set < string > [ ] = [ ]
for ( const mapping of json . mappings ? ? [ ] ) {
if ( mapping . hideInAnswer === true ) {
mappingKeys . push ( undefined )
continue
}
const thisMappingKeys : Set < string > = new Set < string > ( )
addAll ( TagUtils . Tag ( mapping . if ) . usedKeys ( ) , thisMappingKeys )
for ( const tag of mapping . addExtraTags ? ? [ ] ) {
addAll ( TagUtils . Tag ( tag ) . usedKeys ( ) , thisMappingKeys )
}
mappingKeys . push ( thisMappingKeys )
}
const neededKeys = new Set < string > ( )
addAll ( freeformKeys , neededKeys )
for ( const mappingKey of mappingKeys ) {
addAll ( mappingKey , neededKeys )
}
neededKeys . delete ( "fixme" ) // fixme gets a free pass
if ( json . freeform ) {
for ( const neededKey of neededKeys ) {
if ( ! freeformKeys . has ( neededKey ) ) {
context
. enters ( "freeform" )
. warn (
"The freeform block does not modify the key `" +
2024-08-02 19:06:14 +02:00
neededKey +
"` which is set in a mapping. Use `addExtraTags` to overwrite it" ,
2023-11-09 15:42:15 +01:00
)
}
}
}
for ( let i = 0 ; i < json . mappings ? . length ; i ++ ) {
const mapping = json . mappings [ i ]
if ( mapping . hideInAnswer === true ) {
continue
}
const keys = mappingKeys [ i ]
for ( const neededKey of neededKeys ) {
if ( ! keys . has ( neededKey ) ) {
context
. enters ( "mappings" , i )
. warn (
"This mapping does not modify the key `" +
2024-08-02 19:06:14 +02:00
neededKey +
"` which is set in a mapping or by the freeform block. Use `addExtraTags` to overwrite it" ,
2023-11-09 15:42:15 +01:00
)
}
}
}
return json
}
}
2024-05-07 17:24:16 +02:00
export class DetectMappingsShadowedByCondition extends DesugaringStep < TagRenderingConfigJson > {
2024-05-08 14:06:28 +02:00
private readonly _forceError : boolean
2024-05-07 17:24:16 +02:00
2024-05-08 14:06:28 +02:00
constructor ( forceError : boolean = false ) {
2024-06-16 16:06:26 +02:00
super (
"Checks that, if the tagrendering has a condition, that a mapping is not contradictory to it, i.e. that there are no dead mappings" ,
[ ] ,
2024-08-02 19:06:14 +02:00
"DetectMappingsShadowedByCondition" ,
2024-06-16 16:06:26 +02:00
)
2024-05-08 14:06:28 +02:00
this . _forceError = forceError
2024-05-07 17:24:16 +02:00
}
/ * *
*
2024-05-08 14:06:28 +02:00
* const validator = new DetectMappingsShadowedByCondition ( true )
2024-05-07 17:24:16 +02:00
* const ctx = ConversionContext . construct ( [ ] , [ "test" ] )
* validator . convert ( {
* condition : "count>0" ,
* mappings : [
* {
* if : "count=0" ,
* then : {
* en : "No count"
* }
* }
* ]
* } , ctx )
* ctx . hasErrors ( ) // => true
* /
convert ( json : TagRenderingConfigJson , context : ConversionContext ) : TagRenderingConfigJson {
2024-06-16 16:06:26 +02:00
if ( ! json . condition && ! json . metacondition ) {
2024-05-07 17:24:16 +02:00
return json
}
2024-06-16 16:06:26 +02:00
if ( ! json . mappings || json . mappings ? . length == 0 ) {
2024-05-07 17:24:16 +02:00
return json
}
let conditionJson = json . condition ? ? json . metacondition
2024-06-16 16:06:26 +02:00
if ( json . condition !== undefined && json . metacondition !== undefined ) {
conditionJson = { and : [ json . condition , json . metacondition ] }
2024-05-07 17:24:16 +02:00
}
const condition = TagUtils . Tag ( conditionJson , context . path . join ( "." ) )
2024-06-16 16:06:26 +02:00
for ( let i = 0 ; i < json . mappings . length ; i ++ ) {
2024-05-07 17:24:16 +02:00
const mapping = json . mappings [ i ]
const tagIf = TagUtils . Tag ( mapping . if , context . path . join ( "." ) )
const optimized = new And ( [ tagIf , condition ] ) . optimize ( )
2024-06-16 16:06:26 +02:00
if ( optimized === false ) {
const msg =
"Detected a conflicting mapping and condition. The mapping requires tags " +
tagIf . asHumanString ( ) +
", yet this can never happen because the set condition requires " +
condition . asHumanString ( )
2024-05-08 14:06:28 +02:00
const ctx = context . enters ( "mappings" , i )
if ( this . _forceError ) {
ctx . err ( msg )
} else {
ctx . warn ( msg )
}
2024-05-07 17:24:16 +02:00
}
}
return undefined
}
}
2023-02-08 01:14:21 +01:00
export class DetectShadowedMappings extends DesugaringStep < TagRenderingConfigJson > {
2023-09-21 15:29:34 +02:00
private readonly _calculatedTagNames : string [ ]
2022-06-20 01:42:30 +02:00
2022-03-17 23:04:00 +01:00
constructor ( layerConfig? : LayerConfigJson ) {
2023-09-21 15:29:34 +02:00
super ( "Checks that the mappings don't shadow each other" , [ ] , "DetectShadowedMappings" )
this . _calculatedTagNames = DetectShadowedMappings . extractCalculatedTagNames ( layerConfig )
2022-03-17 23:04:00 +01:00
}
/ * *
2022-06-20 01:42:30 +02:00
*
2022-03-17 23:04:00 +01:00
* DetectShadowedMappings . extractCalculatedTagNames ( { calculatedTags : [ "_abc:=js()" ] } ) // => ["_abc"]
* DetectShadowedMappings . extractCalculatedTagNames ( { calculatedTags : [ "_abc=js()" ] } ) // => ["_abc"]
* /
2022-06-20 01:42:30 +02:00
private static extractCalculatedTagNames (
2024-08-02 19:06:14 +02:00
layerConfig? : LayerConfigJson | { calculatedTags : string [ ] } ,
2022-06-20 01:42:30 +02:00
) {
2022-03-17 23:04:00 +01:00
return (
layerConfig ? . calculatedTags ? . map ( ( ct ) = > {
2022-06-20 01:42:30 +02:00
if ( ct . indexOf ( ":=" ) >= 0 ) {
2023-09-21 15:29:34 +02:00
return ct . split ( ":=" ) [ 0 ]
2022-03-17 23:04:00 +01:00
}
2023-09-21 15:29:34 +02:00
return ct . split ( "=" ) [ 0 ]
2022-03-17 23:04:00 +01:00
} ) ? ? [ ]
2023-09-21 15:29:34 +02:00
)
2022-02-04 00:44:09 +01:00
}
2022-02-10 23:16:14 +01:00
2022-03-23 19:48:06 +01:00
/ * *
2022-06-20 01:42:30 +02:00
*
2022-03-23 19:48:06 +01:00
* // should detect a simple shadowed mapping
* const tr = { mappings : [
* {
* if : { or : [ "key=value" , "x=y" ] } ,
* then : "Case A"
* } ,
* {
* if : "key=value" ,
* then : "Shadowed"
* }
* ]
* }
2023-10-12 16:55:26 +02:00
* const context = ConversionContext . test ( )
* const r = new DetectShadowedMappings ( ) . convert ( tr , context ) ;
* context . getAll ( "error" ) . length // => 1
* context . getAll ( "error" ) [ 0 ] . message . indexOf ( "The mapping key=value is fully matched by a previous mapping (namely 0)" ) >= 0 // => true
2022-03-23 19:48:06 +01:00
*
* const tr = { mappings : [
* {
* if : { or : [ "key=value" , "x=y" ] } ,
* then : "Case A"
* } ,
* {
* if : { and : [ "key=value" , "x=y" ] } ,
* then : "Shadowed"
* }
* ]
* }
2023-10-12 16:55:26 +02:00
* const context = ConversionContext . test ( )
* const r = new DetectShadowedMappings ( ) . convert ( tr , context ) ;
* context . getAll ( "error" ) . length // => 1
2023-11-13 13:45:22 +01:00
* context . getAll ( "error" ) [ 0 ] . message . indexOf ( "The mapping key=value & x=y is fully matched by a previous mapping (namely 0)" ) >= 0 // => true
2022-03-23 19:48:06 +01:00
* /
2023-10-11 04:16:52 +02:00
convert ( json : TagRenderingConfigJson , context : ConversionContext ) : TagRenderingConfigJson {
2022-02-10 23:16:14 +01:00
if ( json . mappings === undefined || json . mappings . length === 0 ) {
2023-10-11 04:16:52 +02:00
return json
2022-02-04 00:44:09 +01:00
}
2023-09-21 15:29:34 +02:00
const defaultProperties = { }
2022-03-17 23:04:00 +01:00
for ( const calculatedTagName of this . _calculatedTagNames ) {
2022-06-20 01:42:30 +02:00
defaultProperties [ calculatedTagName ] =
2023-09-21 15:29:34 +02:00
"some_calculated_tag_value_for_" + calculatedTagName
2022-03-17 23:04:00 +01:00
}
2022-06-20 01:42:30 +02:00
const parsedConditions = json . mappings . map ( ( m , i ) = > {
2023-11-02 04:35:32 +01:00
const c = context . enters ( "mappings" , i )
const ifTags = TagUtils . Tag ( m . if , c . enter ( "if" ) )
2023-09-21 15:29:34 +02:00
const hideInAnswer = m [ "hideInAnswer" ]
2023-02-08 01:14:21 +01:00
if ( hideInAnswer !== undefined && hideInAnswer !== false && hideInAnswer !== true ) {
2024-04-08 01:58:07 +02:00
const conditionTags = TagUtils . Tag ( hideInAnswer )
2022-02-20 01:39:12 +01:00
// Merge the condition too!
2023-09-21 15:29:34 +02:00
return new And ( [ conditionTags , ifTags ] )
2022-02-20 01:39:12 +01:00
}
2023-09-21 15:29:34 +02:00
return ifTags
} )
2022-02-10 23:16:14 +01:00
for ( let i = 0 ; i < json . mappings . length ; i ++ ) {
2023-11-02 04:35:32 +01:00
if ( ! parsedConditions [ i ] ? . isUsableAsAnswer ( ) ) {
2022-02-20 01:39:12 +01:00
// There is no straightforward way to convert this mapping.if into a properties-object, so we simply skip this one
// Yes, it might be shadowed, but running this check is to difficult right now
2023-09-21 15:29:34 +02:00
continue
2022-02-04 00:44:09 +01:00
}
2023-09-21 15:29:34 +02:00
const keyValues = parsedConditions [ i ] . asChange ( defaultProperties )
const properties = { }
2023-09-01 16:06:22 +02:00
keyValues . forEach ( ( { k , v } ) = > {
2023-09-21 15:29:34 +02:00
properties [ k ] = v
} )
2022-02-10 23:16:14 +01:00
for ( let j = 0 ; j < i ; j ++ ) {
2023-09-21 15:29:34 +02:00
const doesMatch = parsedConditions [ j ] . matchesProperties ( properties )
2022-07-06 12:57:23 +02:00
if (
doesMatch &&
2023-02-08 01:14:21 +01:00
json . mappings [ j ] [ "hideInAnswer" ] === true &&
json . mappings [ i ] [ "hideInAnswer" ] !== true
2022-07-06 12:57:23 +02:00
) {
2023-10-11 04:16:52 +02:00
context . warn (
2024-08-02 19:06:14 +02:00
` Mapping ${ i } is shadowed by mapping ${ j } . However, mapping ${ j } has 'hideInAnswer' set, which will result in a different rendering in question-mode. ` ,
2023-09-21 15:29:34 +02:00
)
2022-06-28 01:04:45 +02:00
} else if ( doesMatch ) {
2022-02-04 00:44:09 +01:00
// The current mapping is shadowed!
2023-10-11 04:16:52 +02:00
context . err ( ` Mapping ${ i } is shadowed by mapping ${ j } and will thus never be shown:
2022-02-20 01:39:12 +01:00
The mapping $ { parsedConditions [ i ] . asHumanString (
2024-08-02 19:06:14 +02:00
false ,
false ,
{ } ,
) } is fully matched by a previous mapping ( namely $ { j } ) , which matches :
2022-02-04 00:44:09 +01:00
$ { parsedConditions [ j ] . asHumanString ( false , false , { } ) } .
2022-09-11 01:49:07 +02:00
2022-03-17 22:03:41 +01:00
To fix this problem , you can try to :
- Move the shadowed mapping up
2022-06-28 01:04:45 +02:00
- Do you want to use a different text in 'question mode' ? Add 'hideInAnswer=true' to the first mapping
2022-03-17 22:03:41 +01:00
- Use "addExtraTags" : [ "key=value" , . . . ] in order to avoid a different rendering
( e . g . [ { "if" : "fee=no" , "then" : "Free to use" , "hideInAnswer" : true } ,
{ "if" : { "and" : [ "fee=no" , "charge=" ] } , "then" : "Free to use" } ]
can be replaced by
[ { "if" : "fee=no" , "then" : "Free to use" , "addExtraTags" : [ "charge=" ] } ]
2023-09-21 15:29:34 +02:00
` )
2022-02-04 00:44:09 +01:00
}
}
}
2022-02-10 23:16:14 +01:00
2023-10-11 04:16:52 +02:00
return json
2022-02-04 00:44:09 +01:00
}
}
2022-02-17 23:54:14 +01:00
export class DetectMappingsWithImages extends DesugaringStep < TagRenderingConfigJson > {
2023-09-21 15:29:34 +02:00
private readonly _doesImageExist : DoesImageExist
2022-07-06 12:57:23 +02:00
constructor ( doesImageExist : DoesImageExist ) {
2022-02-17 23:54:14 +01:00
super (
"Checks that 'then'clauses in mappings don't have images, but use 'icon' instead" ,
[ ] ,
2024-08-02 19:06:14 +02:00
"DetectMappingsWithImages" ,
2023-09-21 15:29:34 +02:00
)
this . _doesImageExist = doesImageExist
2022-02-17 23:54:14 +01:00
}
2022-03-23 19:48:06 +01:00
/ * *
2023-10-12 16:55:26 +02:00
* const context = ConversionContext . test ( )
2022-07-06 14:00:39 +02:00
* const r = new DetectMappingsWithImages ( new DoesImageExist ( new Set < string > ( ) ) ) . convert ( {
2022-03-23 19:48:06 +01:00
* "mappings" : [
* {
* "if" : "bicycle_parking=stands" ,
* "then" : {
* "en" : "Staple racks <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>" ,
* "nl" : "Nietjes <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>" ,
* "fr" : "Arceaux <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>" ,
* "gl" : "De roda (Stands) <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>" ,
* "de" : "Fahrradbügel <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>" ,
* "hu" : "Korlát <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>" ,
* "it" : "Archetti <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>" ,
* "zh_Hant" : "單車架 <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>"
* }
* } ]
2023-10-12 16:55:26 +02:00
* } , context ) ;
* context . hasErrors ( ) // => true
* context . getAll ( "error" ) . some ( msg = > msg . message . indexOf ( "./assets/layers/bike_parking/staple.svg" ) >= 0 ) // => true
2022-03-23 19:48:06 +01:00
* /
2023-10-11 04:16:52 +02:00
convert ( json : TagRenderingConfigJson , context : ConversionContext ) : TagRenderingConfigJson {
2022-02-17 23:54:14 +01:00
if ( json . mappings === undefined || json . mappings . length === 0 ) {
2023-10-11 04:16:52 +02:00
return json
2022-02-17 23:54:14 +01:00
}
2023-09-21 15:29:34 +02:00
const ignoreToken = "ignore-image-in-then"
2022-02-17 23:54:14 +01:00
for ( let i = 0 ; i < json . mappings . length ; i ++ ) {
2023-09-21 15:29:34 +02:00
const mapping = json . mappings [ i ]
const ignore = mapping [ "#" ] ? . indexOf ( ignoreToken ) >= 0
const images = Utils . Dedup ( Translations . T ( mapping . then ) ? . ExtractImages ( ) ? ? [ ] )
2023-10-11 04:16:52 +02:00
const ctx = context . enters ( "mappings" , i )
2022-02-17 23:54:14 +01:00
if ( images . length > 0 ) {
2022-06-20 01:42:30 +02:00
if ( ! ignore ) {
2023-10-11 04:16:52 +02:00
ctx . err (
` A mapping has an image in the 'then'-clause. Remove the image there and use \` "icon": <your-image> \` instead. The images found are ${ images . join (
2024-08-02 19:06:14 +02:00
", " ,
) } . ( This check can be turned of by adding "#" : "${ignoreToken}" in the mapping , but this is discouraged ` ,
2023-09-21 15:29:34 +02:00
)
2022-06-20 01:42:30 +02:00
} else {
2023-10-11 04:16:52 +02:00
ctx . info (
` Ignored image ${ images . join (
2024-08-02 19:06:14 +02:00
", " ,
) } in 'then' - clause of a mapping as this check has been disabled ` ,
2023-09-21 15:29:34 +02:00
)
2022-06-20 01:42:30 +02:00
for ( const image of images ) {
2023-10-11 04:16:52 +02:00
this . _doesImageExist . convert ( image , ctx )
2022-06-20 01:42:30 +02:00
}
2022-02-19 17:57:34 +01:00
}
2022-06-20 01:42:30 +02:00
} else if ( ignore ) {
2023-10-11 04:16:52 +02:00
ctx . warn ( ` Unused ' ${ ignoreToken } ' - please remove this ` )
2022-02-17 23:54:14 +01:00
}
}
2023-10-11 04:16:52 +02:00
return json
2023-09-02 02:04:59 +02:00
}
}
class ValidatePossibleLinks extends DesugaringStep < string | Record < string , string > > {
constructor ( ) {
2023-09-21 15:29:34 +02:00
super (
"Given a possible set of translations, validates that <a href=... target='_blank'> does have `rel='noopener'` set" ,
[ ] ,
2024-08-02 19:06:14 +02:00
"ValidatePossibleLinks" ,
2023-09-21 15:29:34 +02:00
)
2023-09-02 02:04:59 +02:00
}
public isTabnabbingProne ( str : string ) : boolean {
2023-09-21 15:29:34 +02:00
const p = parse_html ( str )
const links = Array . from ( p . getElementsByTagName ( "a" ) )
2023-09-02 02:04:59 +02:00
if ( links . length == 0 ) {
2023-09-21 15:29:34 +02:00
return false
2022-02-17 23:54:14 +01:00
}
2023-09-02 02:04:59 +02:00
for ( const link of Array . from ( links ) ) {
if ( link . getAttribute ( "target" ) !== "_blank" ) {
2023-09-21 15:29:34 +02:00
continue
2023-09-02 02:04:59 +02:00
}
2023-09-21 15:29:34 +02:00
const rel = new Set < string > ( link . getAttribute ( "rel" ) ? . split ( " " ) ? ? [ ] )
2023-09-02 02:04:59 +02:00
if ( rel . has ( "noopener" ) ) {
2023-09-21 15:29:34 +02:00
continue
2023-09-02 02:04:59 +02:00
}
2023-09-21 15:29:34 +02:00
const source = link . getAttribute ( "href" )
2023-09-02 02:04:59 +02:00
if ( source . startsWith ( "http" ) ) {
// No variable part - we assume the link is safe
2023-09-21 15:29:34 +02:00
continue
2023-09-02 02:04:59 +02:00
}
2023-09-21 15:29:34 +02:00
return true
2023-09-02 02:04:59 +02:00
}
2023-09-21 15:29:34 +02:00
return false
2023-09-02 02:04:59 +02:00
}
2023-09-21 15:29:34 +02:00
convert (
json : string | Record < string , string > ,
2024-08-02 19:06:14 +02:00
context : ConversionContext ,
2023-10-11 04:16:52 +02:00
) : string | Record < string , string > {
2023-09-02 02:04:59 +02:00
if ( typeof json === "string" ) {
if ( this . isTabnabbingProne ( json ) ) {
2023-10-11 04:16:52 +02:00
context . err (
"The string " +
2024-08-02 19:06:14 +02:00
json +
" has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping" ,
2023-09-21 15:29:34 +02:00
)
2023-09-02 02:04:59 +02:00
}
} else {
for ( const k in json ) {
if ( this . isTabnabbingProne ( json [ k ] ) ) {
2023-10-11 04:16:52 +02:00
context . err (
2024-08-02 19:06:14 +02:00
` The translation for ${ k } ' ${ json [ k ] } ' has a link targeting \` _blank \` , but it doesn't have \` rel='noopener' \` set. This gives rise to reverse tabnapping ` ,
2023-09-21 15:29:34 +02:00
)
2023-09-02 02:04:59 +02:00
}
}
}
2023-10-11 04:16:52 +02:00
return json
2022-02-17 23:54:14 +01:00
}
}
2023-11-02 04:35:32 +01:00
class CheckTranslation extends DesugaringStep < Translatable > {
public static readonly allowUndefined : CheckTranslation = new CheckTranslation ( true )
public static readonly noUndefined : CheckTranslation = new CheckTranslation ( )
private readonly _allowUndefined : boolean
constructor ( allowUndefined : boolean = false ) {
super (
"Checks that a translation is valid and internally consistent" ,
[ "*" ] ,
2024-08-02 19:06:14 +02:00
"CheckTranslation" ,
2023-11-02 04:35:32 +01:00
)
this . _allowUndefined = allowUndefined
}
convert ( json : Translatable , context : ConversionContext ) : Translatable {
if ( json === undefined || json === null ) {
if ( ! this . _allowUndefined ) {
context . err ( "Expected a translation, but got " + json )
}
return json
}
if ( typeof json === "string" ) {
return json
}
const keys = Object . keys ( json )
if ( keys . length === 0 ) {
context . err ( "No actual values are given in this translation, it is completely empty" )
return json
}
const en = json [ "en" ]
if ( ! en && json [ "*" ] === undefined ) {
const msg = "Received a translation without english version"
context . warn ( msg )
}
for ( const key of keys ) {
const lng = json [ key ]
if ( lng === "" ) {
2024-04-01 02:29:37 +02:00
context . enter ( lng ) . err ( "Got an empty string in translation for language " + key )
2023-11-02 04:35:32 +01:00
}
2023-03-24 19:21:15 +01:00
2023-11-02 04:35:32 +01:00
// TODO validate that all subparts are here
}
return json
}
}
class MiscTagRenderingChecks extends DesugaringStep < TagRenderingConfigJson > {
2024-05-23 04:42:26 +02:00
private readonly _layerConfig : LayerConfigJson
2024-08-02 19:06:14 +02:00
2024-05-23 04:42:26 +02:00
constructor ( layerConfig? : LayerConfigJson ) {
2023-09-21 15:29:34 +02:00
super ( "Miscellaneous checks on the tagrendering" , [ "special" ] , "MiscTagRenderingChecks" )
2024-05-23 04:42:26 +02:00
this . _layerConfig = layerConfig
2022-10-29 03:02:42 +02:00
}
2022-11-02 14:44:06 +01:00
convert (
2023-03-08 02:01:52 +01:00
json : TagRenderingConfigJson | QuestionableTagRenderingConfigJson ,
2024-08-02 19:06:14 +02:00
context : ConversionContext ,
2023-10-11 04:16:52 +02:00
) : TagRenderingConfigJson {
2022-11-02 14:44:06 +01:00
if ( json [ "special" ] !== undefined ) {
2023-10-11 04:16:52 +02:00
context . err (
2024-08-02 19:06:14 +02:00
"Detected `special` on the top level. Did you mean `{\"render\":{ \"special\": ... }}`" ,
2023-09-21 15:29:34 +02:00
)
2022-10-29 03:02:42 +02:00
}
2023-11-02 04:35:32 +01:00
2023-11-19 18:08:57 +01:00
if ( Object . keys ( json ) . length === 1 && typeof json [ "render" ] === "string" ) {
context . warn (
2024-08-02 19:06:14 +02:00
` use the content directly instead of {render: ${ JSON . stringify ( json [ "render" ] ) } } ` ,
2023-11-19 18:08:57 +01:00
)
}
2023-11-02 04:35:32 +01:00
{
for ( const key of [ "question" , "questionHint" , "render" ] ) {
CheckTranslation . allowUndefined . convert ( json [ key ] , context . enter ( key ) )
}
for ( let i = 0 ; i < json . mappings ? . length ? ? 0 ; i ++ ) {
2024-01-19 17:31:35 +01:00
const mapping : MappingConfigJson = json . mappings [ i ]
2023-11-02 04:35:32 +01:00
CheckTranslation . noUndefined . convert (
mapping . then ,
2024-08-02 19:06:14 +02:00
context . enters ( "mappings" , i , "then" ) ,
2023-11-02 04:35:32 +01:00
)
if ( ! mapping . if ) {
2024-01-19 17:31:35 +01:00
console . log (
"Checking mappings" ,
i ,
"if" ,
mapping . if ,
context . path . join ( "." ) ,
2024-08-02 19:06:14 +02:00
mapping . then ,
2024-01-19 17:31:35 +01:00
)
context . enters ( "mappings" , i , "if" ) . err ( "No `if` is defined" )
}
if ( mapping . addExtraTags ) {
for ( let j = 0 ; j < mapping . addExtraTags . length ; j ++ ) {
if ( ! mapping . addExtraTags [ j ] ) {
context
. enters ( "mappings" , i , "addExtraTags" , j )
. err (
2024-08-02 19:06:14 +02:00
"Detected a 'null' or 'undefined' value. Either specify a tag or delete this item" ,
2024-01-19 17:31:35 +01:00
)
}
}
2023-11-02 04:35:32 +01:00
}
2023-11-03 02:04:42 +01:00
const en = mapping ? . then ? . [ "en" ]
2023-11-05 12:05:00 +01:00
if ( en && this . detectYesOrNo ( en ) ) {
console . log ( "Found a match with yes or no: " , { en } )
2023-11-03 02:04:42 +01:00
context
. enters ( "mappings" , i , "then" )
. warn (
2024-08-02 19:06:14 +02:00
"A mapping should not start with 'yes' or 'no'. If the attribute is known, it will only show 'yes' or 'no' <i>without</i> the question, resulting in a weird phrasing in the information box" ,
2023-11-03 02:04:42 +01:00
)
}
2023-11-02 04:35:32 +01:00
}
}
2023-03-31 03:28:11 +02:00
if ( json [ "group" ] ) {
2024-08-02 19:06:14 +02:00
context . err ( "Groups are deprecated, use `\"label\": [\"" + json [ "group" ] + "\"]` instead" )
2023-03-29 18:54:00 +02:00
}
2023-09-02 02:04:59 +02:00
2023-11-02 04:35:32 +01:00
if ( json [ "question" ] && json . freeform ? . key === undefined && json . mappings === undefined ) {
context . err (
2024-08-02 19:06:14 +02:00
"A question is defined, but no mappings nor freeform (key) are. Add at least one of them" ,
2023-11-02 04:35:32 +01:00
)
}
if ( json [ "question" ] && ! json . freeform && ( json . mappings ? . length ? ? 0 ) == 1 ) {
context . err ( "A question is defined, but there is only one option to choose from." )
}
if ( json [ "questionHint" ] && ! json [ "question" ] ) {
context
. enter ( "questionHint" )
. err (
2024-08-02 19:06:14 +02:00
"A questionHint is defined, but no question is given. As such, the questionHint will never be shown" ,
2023-11-02 04:35:32 +01:00
)
}
2024-02-15 17:48:26 +01:00
if ( json . icon ? . [ "size" ] ) {
context
. enters ( "icon" , "size" )
. err (
2024-08-02 19:06:14 +02:00
"size is not a valid attribute. Did you mean 'class'? Class can be one of `small`, `medium` or `large`" ,
2023-11-02 04:35:32 +01:00
)
}
2023-10-17 16:06:58 +02:00
if ( json . freeform ) {
if ( json . render === undefined ) {
2023-10-24 22:01:10 +02:00
context
. enter ( "render" )
. err (
"This tagRendering allows to set a value to key " +
2024-08-02 19:06:14 +02:00
json . freeform . key +
", but does not define a `render`. Please, add a value here which contains `{" +
json . freeform . key +
"}`" ,
2023-10-24 22:01:10 +02:00
)
2023-10-17 16:06:58 +02:00
} else {
const render = new Translation ( < any > json . render )
for ( const ln in render . translations ) {
if ( ln . startsWith ( "_" ) ) {
continue
}
const txt : string = render . translations [ ln ]
if ( txt === "" ) {
2023-10-24 22:01:10 +02:00
context . enter ( "render" ) . err ( " Rendering for language " + ln + " is empty" )
2023-10-17 16:06:58 +02:00
}
if (
txt . indexOf ( "{" + json . freeform . key + "}" ) >= 0 ||
2023-10-24 22:01:10 +02:00
txt . indexOf ( "&LBRACE" + json . freeform . key + "&RBRACE" ) >= 0
2023-10-17 16:06:58 +02:00
) {
continue
}
if ( txt . indexOf ( "{" + json . freeform . key + ":" ) >= 0 ) {
continue
}
if (
json . freeform [ "type" ] === "opening_hours" &&
txt . indexOf ( "{opening_hours_table(" ) >= 0
) {
continue
}
const keyFirstArg = [ "canonical" , "fediverse_link" , "translated" ]
if (
keyFirstArg . some (
2024-08-02 19:06:14 +02:00
( funcName ) = > txt . indexOf ( ` { ${ funcName } ( ${ json . freeform . key } ` ) >= 0 ,
2023-10-17 16:06:58 +02:00
)
) {
continue
}
if (
json . freeform [ "type" ] === "wikidata" &&
txt . indexOf ( "{wikipedia(" + json . freeform . key ) >= 0
) {
continue
}
if ( json . freeform . key === "wikidata" && txt . indexOf ( "{wikipedia()" ) >= 0 ) {
continue
}
if (
json . freeform [ "type" ] === "wikidata" &&
txt . indexOf ( ` {wikidata_label( ${ json . freeform . key } ) ` ) >= 0
) {
continue
}
2024-04-13 02:40:21 +02:00
if ( json . freeform . key . indexOf ( "wikidata" ) >= 0 ) {
2024-02-28 02:13:36 +01:00
context
. enter ( "render" )
. err (
2024-08-02 19:06:14 +02:00
` The rendering for language ${ ln } does not contain \` { ${ json . freeform . key } } \` . Did you perhaps forget to set "freeform.type: 'wikidata'"? ` ,
2024-02-28 02:13:36 +01:00
)
2024-07-26 18:13:38 +02:00
continue
}
2024-08-09 16:55:08 +02:00
if (
txt . indexOf ( json . freeform . key ) >= 0 &&
txt . indexOf ( "{" + json . freeform . key + "}" ) < 0
) {
2024-07-26 18:13:38 +02:00
context
. enter ( "render" )
. err (
2024-08-02 19:06:14 +02:00
` The rendering for language ${ ln } does not contain \` { ${ json . freeform . key } } \` . However, it does contain ${ json . freeform . key } without braces. Did you forget the braces? \ n \ tThe current text is ${ txt } ` ,
2024-07-26 18:13:38 +02:00
)
continue
2024-02-28 02:13:36 +01:00
}
2024-07-26 18:13:38 +02:00
2023-10-24 22:01:10 +02:00
context
. enter ( "render" )
. err (
2024-08-02 19:06:14 +02:00
` The rendering for language ${ ln } does not contain \` { ${ json . freeform . key } } \` . This is a bug, as this rendering should show exactly this freeform key! \ n \ tThe current text is ${ txt } ` ,
2023-10-24 22:01:10 +02:00
)
2023-10-17 16:06:58 +02:00
}
}
2024-06-16 16:06:26 +02:00
if (
this . _layerConfig ? . source ? . osmTags &&
NameSuggestionIndex . supportedTypes ( ) . indexOf ( json . freeform . key ) >= 0
) {
const tags = TagUtils . TagD ( this . _layerConfig ? . source ? . osmTags ) ? . usedTags ( )
2024-05-23 04:42:26 +02:00
const suggestions = NameSuggestionIndex . getSuggestionsFor ( json . freeform . key , tags )
2024-06-16 16:06:26 +02:00
if ( suggestions === undefined ) {
context
. enters ( "freeform" , "type" )
. err (
"No entry found in the 'Name Suggestion Index'. None of the 'osmSource'-tags match an entry in the NSI.\n\tOsmSource-tags are " +
2024-08-02 19:06:14 +02:00
tags . map ( ( t ) = > new Tag ( t . key , t . value ) . asHumanString ( ) ) . join ( " ; " ) ,
2024-06-16 16:06:26 +02:00
)
2024-05-13 17:21:40 +02:00
}
2024-06-16 16:06:26 +02:00
} else if ( json . freeform . type === "nsi" ) {
context
. enters ( "freeform" , "type" )
. warn (
2024-08-02 19:06:14 +02:00
"No need to explicitly set type to 'NSI', autodetected based on freeform type" ,
2024-06-16 16:06:26 +02:00
)
2024-05-13 17:21:40 +02:00
}
2023-10-17 16:06:58 +02:00
}
if ( json . render && json [ "question" ] && json . freeform === undefined ) {
context . err (
` Detected a tagrendering which takes input without freeform key in ${ context } ; the question is ${ new Translation (
2024-08-02 19:06:14 +02:00
json [ "question" ] ,
) . textFor ( "en" ) } ` ,
2023-10-17 16:06:58 +02:00
)
}
2023-09-21 15:29:34 +02:00
const freeformType = json [ "freeform" ] ? . [ "type" ]
2023-03-24 19:21:15 +01:00
if ( freeformType ) {
2023-03-30 04:51:56 +02:00
if ( Validators . availableTypes . indexOf ( freeformType ) < 0 ) {
2023-10-11 04:16:52 +02:00
context
. enters ( "freeform" , "type" )
. err (
"Unknown type: " +
2024-08-02 19:06:14 +02:00
freeformType +
"; try one of " +
Validators . availableTypes . join ( ", " ) ,
2023-10-11 04:16:52 +02:00
)
2023-03-24 19:21:15 +01:00
}
}
2023-11-02 04:35:32 +01:00
2024-01-19 17:31:35 +01:00
if ( context . hasErrors ( ) ) {
return undefined
}
2023-10-11 04:16:52 +02:00
return json
2022-10-29 03:02:42 +02:00
}
2023-11-05 12:05:00 +01:00
/ * *
* const obj = new MiscTagRenderingChecks ( )
* obj . detectYesOrNo ( "Yes, this place has" ) // => true
* obj . detectYesOrNo ( "Yes" ) // => true
* obj . detectYesOrNo ( "No, this place does not have..." ) // => true
* obj . detectYesOrNo ( "This place does not have..." ) // => false
* /
private detectYesOrNo ( en : string ) : boolean {
return en . toLowerCase ( ) . match ( /^(yes|no)([,:;.?]|$)/ ) !== null
}
2022-10-29 03:02:42 +02:00
}
2022-02-17 23:54:14 +01:00
export class ValidateTagRenderings extends Fuse < TagRenderingConfigJson > {
2023-11-02 04:35:32 +01:00
constructor ( layerConfig? : LayerConfigJson , doesImageExist? : DoesImageExist ) {
2022-02-17 23:54:14 +01:00
super (
"Various validation on tagRenderingConfigs" ,
2024-05-23 04:42:26 +02:00
new MiscTagRenderingChecks ( layerConfig ) ,
2022-06-20 01:42:30 +02:00
new DetectShadowedMappings ( layerConfig ) ,
2024-05-07 17:24:16 +02:00
new DetectMappingsShadowedByCondition ( ) ,
2023-07-28 14:11:00 +02:00
new DetectConflictingAddExtraTags ( ) ,
2023-11-30 00:39:55 +01:00
// TODO enable new DetectNonErasedKeysInMappings(),
2023-02-08 01:14:21 +01:00
new DetectMappingsWithImages ( doesImageExist ) ,
2023-09-21 15:29:34 +02:00
new On ( "render" , new ValidatePossibleLinks ( ) ) ,
new On ( "question" , new ValidatePossibleLinks ( ) ) ,
new On ( "questionHint" , new ValidatePossibleLinks ( ) ) ,
new On ( "mappings" , new Each ( new On ( "then" , new ValidatePossibleLinks ( ) ) ) ) ,
2024-08-02 19:06:14 +02:00
new MiscTagRenderingChecks ( layerConfig ) ,
2023-09-21 15:29:34 +02:00
)
2022-02-17 23:54:14 +01:00
}
}
2023-11-02 04:35:32 +01:00
export class PrevalidateLayer extends DesugaringStep < LayerConfigJson > {
private readonly _isBuiltin : boolean
private readonly _doesImageExist : DoesImageExist
2022-02-04 00:44:09 +01:00
/ * *
* The paths where this layer is originally saved . Triggers some extra checks
* /
2023-11-02 04:35:32 +01:00
private readonly _path : string
2023-10-24 22:01:10 +02:00
private readonly _studioValidations : boolean
2024-01-12 23:19:31 +01:00
private readonly _validatePointRendering = new ValidatePointRendering ( )
2022-02-04 00:44:09 +01:00
2024-01-22 03:42:00 +01:00
constructor (
path : string ,
isBuiltin : boolean ,
doesImageExist : DoesImageExist ,
2024-08-02 19:06:14 +02:00
studioValidations : boolean ,
2024-01-22 03:42:00 +01:00
) {
2023-11-02 04:35:32 +01:00
super ( "Runs various checks against common mistakes for a layer" , [ ] , "PrevalidateLayer" )
2023-09-21 15:29:34 +02:00
this . _path = path
this . _isBuiltin = isBuiltin
this . _doesImageExist = doesImageExist
2023-10-20 19:04:55 +02:00
this . _studioValidations = studioValidations
2022-02-04 00:44:09 +01:00
}
2023-11-02 04:35:32 +01:00
convert ( json : LayerConfigJson , context : ConversionContext ) : LayerConfigJson {
2023-10-13 18:46:56 +02:00
if ( json . id === undefined ) {
2023-10-24 22:01:10 +02:00
context . enter ( "id" ) . err ( ` Not a valid layer: id is undefined ` )
2023-11-02 04:35:32 +01:00
} else {
if ( json . id ? . toLowerCase ( ) !== json . id ) {
context . enter ( "id" ) . err ( ` The id of a layer should be lowercase: ${ json . id } ` )
}
2024-01-23 22:03:22 +01:00
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 )
2023-11-02 04:35:32 +01:00
}
2023-10-13 18:46:56 +02:00
}
if ( json . source === undefined ) {
2023-11-02 04:35:32 +01:00
context
. enter ( "source" )
. err (
2024-08-02 19:06:14 +02:00
"No source section is defined; please define one as data is not loaded otherwise" ,
2023-11-02 04:35:32 +01:00
)
2023-10-13 18:46:56 +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-02 19:06:14 +02:00
"No osmTags defined in the source section - these should always be present, even for geojson layer" ,
2023-10-13 18:46:56 +02:00
)
} else {
const osmTags = TagUtils . Tag ( json . source [ "osmTags" ] , context + "source.osmTags" )
if ( osmTags . isNegative ( ) ) {
context
. enters ( "source" , "osmTags" )
. err (
"The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" +
2024-08-02 19:06:14 +02:00
osmTags . asHumanString ( false , false , { } ) ,
2023-10-13 18:46:56 +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)" )
}
}
if (
json . syncSelection !== undefined &&
LayerConfig . syncSelectionAllowed . indexOf ( json . syncSelection ) < 0
) {
context
. enter ( "syncSelection" )
. err (
"Invalid sync-selection: must be one of " +
2024-08-02 19:06:14 +02:00
LayerConfig . syncSelectionAllowed . map ( ( v ) = > ` ' ${ v } ' ` ) . join ( ", " ) +
" but got '" +
json . syncSelection +
"'" ,
2023-10-13 18:46:56 +02:00
)
}
2023-11-13 04:33:25 +01:00
if ( json [ "pointRenderings" ] ? . length > 0 ) {
context
. enter ( "pointRenderings" )
. err ( "Detected a 'pointRenderingS', it is written singular" )
}
2023-10-13 18:46:56 +02:00
2023-11-13 13:45:22 +01:00
if (
! ( json . pointRendering ? . length > 0 ) &&
json . pointRendering !== null &&
json . source !== "special" &&
json . source !== "special:library"
) {
2023-11-12 15:30:20 +01:00
context . enter ( "pointRendering" ) . err ( "There are no pointRenderings at all..." )
2023-11-12 13:03:07 +01:00
}
2023-11-13 04:33:25 +01:00
2024-01-19 17:31:35 +01:00
json . pointRendering ? . forEach ( ( pr , i ) = >
2024-08-02 19:06:14 +02:00
this . _validatePointRendering . convert ( pr , context . enters ( "pointeRendering" , i ) ) ,
2024-01-19 17:31:35 +01:00
)
2024-01-12 23:19:31 +01:00
2023-11-13 04:33:25 +01:00
if ( json [ "mapRendering" ] ) {
context . enter ( "mapRendering" ) . err ( "This layer has a legacy 'mapRendering'" )
}
2023-11-11 14:52:01 +01:00
if ( json . presets ? . length > 0 ) {
if ( ! ( json . pointRendering ? . length > 0 ) ) {
context . enter ( "presets" ) . warn ( "A preset is defined, but there is no pointRendering" )
}
}
2023-03-29 18:54:00 +02:00
if ( json . source === "special" ) {
if ( ! Constants . priviliged_layers . find ( ( x ) = > x == json . id ) ) {
2023-10-11 04:16:52 +02:00
context . err (
"Layer " +
2024-08-02 19:06:14 +02:00
json . id +
" uses 'special' as source.osmTags. However, this layer is not a priviliged layer" ,
2023-09-21 15:29:34 +02:00
)
2023-03-29 18:54:00 +02:00
}
}
2023-11-02 04:35:32 +01:00
if ( context . hasErrors ( ) ) {
return undefined
}
2022-07-20 22:57:39 +02:00
if ( json . tagRenderings !== undefined && json . tagRenderings . length > 0 ) {
2024-01-19 17:31:35 +01:00
new On ( "tagRenderings" , new Each ( new ValidateTagRenderings ( json ) ) )
2023-07-15 18:04:30 +02:00
if ( json . title === undefined && json . source !== "special:library" ) {
2023-11-14 17:35:12 +01:00
context
. enter ( "title" )
. err (
2024-08-02 19:06:14 +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." ,
2023-11-14 17:35:12 +01:00
)
2022-07-20 22:57:39 +02:00
}
if ( json . title === null ) {
2023-10-11 04:16:52 +02:00
context . info (
2024-08-02 19:06:14 +02:00
"Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set." ,
2023-09-21 15:29:34 +02:00
)
2022-07-20 22:57:39 +02:00
}
2023-10-24 22:01:10 +02:00
{
// Check for multiple, identical builtin questions - usability for studio users
const duplicates = Utils . Duplicates (
2024-08-02 19:06:14 +02:00
< string [ ] > json . tagRenderings . filter ( ( tr ) = > typeof tr === "string" ) ,
2023-10-24 22:01:10 +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 } ) ` )
}
}
}
2022-07-19 13:30:26 +02:00
}
2022-02-04 00:44:09 +01:00
if ( json [ "builtin" ] !== undefined ) {
2023-10-11 04:16:52 +02:00
context . err ( "This layer hasn't been expanded: " + json )
return null
2022-02-04 00:44:09 +01:00
}
2022-09-08 21:40:48 +02:00
2023-04-02 02:59:20 +02:00
if ( json . minzoom > Constants . minZoomLevelToAddNewPoint ) {
2023-10-11 04:16:52 +02:00
const c = context . enter ( "minzoom" )
2023-10-11 17:30:06 +02:00
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 )
}
2022-08-24 01:29:11 +02:00
}
2022-04-08 22:12:43 +02:00
{
// duplicate ids in tagrenderings check
2023-11-05 12:05:00 +01:00
const duplicates = Utils . NoNull (
2024-08-02 19:06:14 +02:00
Utils . Duplicates ( Utils . NoNull ( ( json . tagRenderings ? ? [ ] ) . map ( ( tr ) = > tr [ "id" ] ) ) ) ,
2023-09-21 15:29:34 +02:00
)
2022-06-20 01:42:30 +02:00
if ( duplicates . length > 0 ) {
2023-11-05 12:05:00 +01:00
// 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
2023-10-11 04:16:52 +02:00
context
. enter ( "tagRenderings" )
2024-04-13 02:40:21 +02:00
. err (
"Some tagrenderings have a duplicate id: " +
2024-08-02 19:06:14 +02:00
duplicates . join ( ", " ) +
"\n" +
JSON . stringify (
json . tagRenderings . filter ( ( tr ) = > duplicates . indexOf ( tr [ "id" ] ) >= 0 ) ,
) ,
2024-04-13 02:40:21 +02:00
)
2022-04-08 22:12:43 +02:00
}
}
2022-06-20 01:42:30 +02:00
2022-10-27 01:50:41 +02:00
if ( json . deletion !== undefined && json . deletion instanceof DeleteConfig ) {
if ( json . deletion . softDeletionTags === undefined ) {
2023-10-11 04:16:52 +02:00
context
. enter ( "deletion" )
. warn ( "No soft-deletion tags in deletion block for layer " + json . id )
2022-09-24 03:33:09 +02:00
}
}
2022-02-04 00:44:09 +01:00
try {
2023-11-02 04:35:32 +01:00
} catch ( e ) {
context . err ( "Could not validate layer due to: " + e + e . stack )
}
2022-09-08 21:40:48 +02:00
2023-11-02 04:35:32 +01:00
if ( this . _studioValidations ) {
if ( ! json . description ) {
context . enter ( "description" ) . err ( "A description is required" )
2022-02-04 00:44:09 +01:00
}
2023-11-02 04:35:32 +01:00
if ( ! json . name ) {
context . enter ( "name" ) . err ( "A name is required" )
2022-02-04 00:44:09 +01:00
}
2023-11-02 04:35:32 +01:00
}
if ( this . _isBuiltin ) {
// Some checks for legacy elements
2022-02-04 00:44:09 +01:00
2023-11-02 04:35:32 +01:00
if ( json [ "overpassTags" ] !== undefined ) {
context . err (
"Layer " +
2024-08-02 19:06:14 +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)" ,
2023-09-21 15:29:34 +02:00
)
2023-11-02 04:35:32 +01: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-02 19:06:14 +02:00
"Layer " + json . id + " contains an old 'hideUnderlayingFeaturesMinPercentage'" ,
2023-11-02 04:35:32 +01:00
)
}
2022-02-04 00:44:09 +01:00
2023-11-02 04:35:32 +01: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-02 19:06:14 +02:00
this . _path +
", but expected " +
expected ,
2023-11-02 04:35:32 +01: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 )
2022-02-04 00:44:09 +01:00
}
}
2023-11-02 04:35:32 +01:00
context
. enter ( [ "tagRenderings" , . . . emptyIndexes ] )
. err (
` Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${ emptyIndexes . join (
2024-08-02 19:06:14 +02:00
"," ,
) } ] ) ` ,
2023-11-02 04:35:32 +01:00
)
2022-02-04 00:44:09 +01:00
}
2022-09-11 01:49:07 +02:00
2023-11-02 04:35:32 +01:00
const duplicateIds = Utils . Duplicates (
2024-08-02 19:06:14 +02:00
( json . tagRenderings ? ? [ ] ) ? . map ( ( f ) = > f [ "id" ] ) . filter ( ( id ) = > id !== "questions" ) ,
2023-11-02 04:35:32 +01:00
)
if ( duplicateIds . length > 0 && ! Utils . runningFromConsole ) {
context
. enter ( "tagRenderings" )
. err ( ` Some tagRenderings have a duplicate id: ${ duplicateIds } ` )
2022-02-04 00:44:09 +01:00
}
2022-02-10 23:16:14 +01:00
2023-11-02 04:35:32 +01:00
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" )
2023-10-30 14:32:31 +01:00
}
2022-08-18 14:39:40 +02:00
}
2023-11-02 04:35:32 +01:00
}
if ( json . filter ) {
new On ( "filter" , new Each ( new ValidateFilter ( ) ) ) . convert ( json , context )
}
2022-08-18 14:39:40 +02:00
2023-11-02 04:35:32 +01:00
if ( json . tagRenderings !== undefined ) {
new On (
"tagRenderings" ,
2024-08-02 19:06:14 +02:00
new Each ( new ValidateTagRenderings ( json , this . _doesImageExist ) ) ,
2023-11-02 04:35:32 +01: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
2023-03-25 02:48:24 +01:00
}
2023-11-02 04:35:32 +01:00
for ( const icon of pointRendering ? . marker ) {
const indexM = pointRendering ? . marker . indexOf ( icon )
if ( ! icon . icon ) {
continue
2022-02-14 15:40:38 +01:00
}
2023-11-02 04:35:32 +01:00
if ( icon . icon [ "condition" ] ) {
2023-10-11 04:16:52 +02:00
context
2023-11-02 04:35:32 +01:00
. enters ( "pointRendering" , i , "marker" , indexM , "icon" , "condition" )
2023-10-11 04:16:52 +02:00
. err (
2024-08-02 19:06:14 +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." ,
2023-10-11 04:16:52 +02:00
)
2022-02-14 15:40:38 +01:00
}
}
}
2022-02-04 00:44:09 +01:00
}
2022-02-10 23:16:14 +01:00
2023-11-02 04:35:32 +01:00
if ( json . presets !== undefined ) {
if ( typeof json . source === "string" ) {
2023-11-03 02:04:42 +01:00
context . enter ( "presets" ) . err ( "A special layer cannot have presets" )
2023-10-20 19:04:55 +02:00
}
2023-11-02 04:35:32 +01:00
// Check that a preset will be picked up by the layer itself
const baseTags = TagUtils . Tag ( json . source [ "osmTags" ] )
for ( let i = 0 ; i < json . presets . length ; i ++ ) {
const preset = json . presets [ i ]
2023-11-09 15:42:15 +01:00
if ( ! preset ) {
context . enters ( "presets" , i ) . err ( "This preset is undefined" )
continue
}
2023-11-03 02:04:42 +01:00
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 ) ) )
2023-11-02 04:35:32 +01:00
const properties = { }
2023-11-03 02:04:42 +01:00
for ( const tag of tags . asChange ( { id : "node/-1" } ) ) {
2023-11-02 04:35:32 +01:00
properties [ tag . k ] = tag . v
}
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-02 19:06:14 +02:00
tags . asHumanString ( false , false , { } ) +
"\n The required tags are: " +
baseTags . asHumanString ( false , false , { } ) ,
2023-11-02 04:35:32 +01:00
)
}
}
}
return json
}
}
2023-11-30 00:39:55 +01:00
export class ValidateLayerConfig extends DesugaringStep < LayerConfigJson > {
private readonly validator : ValidateLayer
2024-01-03 14:57:26 +01:00
2023-11-30 00:39:55 +01:00
constructor (
path : string ,
isBuiltin : boolean ,
doesImageExist : DoesImageExist ,
studioValidations : boolean = false ,
2024-08-02 19:06:14 +02:00
skipDefaultLayers : boolean = false ,
2023-11-30 00:39:55 +01:00
) {
super ( "Thin wrapper around 'ValidateLayer" , [ ] , "ValidateLayerConfig" )
this . validator = new ValidateLayer (
path ,
isBuiltin ,
doesImageExist ,
studioValidations ,
2024-08-02 19:06:14 +02:00
skipDefaultLayers ,
2023-11-30 00:39:55 +01:00
)
}
convert ( json : LayerConfigJson , context : ConversionContext ) : LayerConfigJson {
2023-12-02 03:19:50 +01:00
const prepared = this . validator . convert ( json , context )
if ( ! prepared ) {
context . err ( "Preparing layer failed" )
return undefined
}
return prepared ? . raw
2023-11-30 00:39:55 +01:00
}
}
2024-01-03 14:57:26 +01:00
2024-01-12 23:19:31 +01:00
class ValidatePointRendering extends DesugaringStep < PointRenderingConfigJson > {
constructor ( ) {
super ( "Various checks for pointRenderings" , [ ] , "ValidatePOintRendering" )
}
convert ( json : PointRenderingConfigJson , context : ConversionContext ) : PointRenderingConfigJson {
if ( json . marker === undefined && json . label === undefined ) {
context . err ( ` A point rendering should define at least an marker or a label ` )
}
if ( json [ "markers" ] ) {
2024-01-19 17:31:35 +01:00
context
. enter ( "markers" )
. err (
2024-08-02 19:06:14 +02:00
` Detected a field 'markerS' in pointRendering. It is written as a singular case ` ,
2024-01-19 17:31:35 +01:00
)
2024-01-12 23:19:31 +01:00
}
if ( json . marker && ! Array . isArray ( json . marker ) ) {
2024-01-19 17:31:35 +01:00
context . enter ( "marker" ) . err ( "The marker in a pointRendering should be an array" )
2024-01-12 23:19:31 +01:00
}
if ( json . location . length == 0 ) {
2024-01-19 17:31:35 +01:00
context
. enter ( "location" )
. err (
2024-08-02 19:06:14 +02:00
"A pointRendering should have at least one 'location' to defined where it should be rendered. " ,
2024-01-19 17:31:35 +01:00
)
2024-01-12 23:19:31 +01:00
}
return json
}
}
2024-01-23 22:03:22 +01:00
2023-11-02 04:35:32 +01:00
export class ValidateLayer extends Conversion <
LayerConfigJson ,
{ parsed : LayerConfig ; raw : LayerConfigJson }
> {
private readonly _skipDefaultLayers : boolean
private readonly _prevalidation : PrevalidateLayer
2024-01-23 22:03:22 +01:00
2023-11-02 04:35:32 +01:00
constructor (
path : string ,
isBuiltin : boolean ,
doesImageExist : DoesImageExist ,
studioValidations : boolean = false ,
2024-08-02 19:06:14 +02:00
skipDefaultLayers : boolean = false ,
2023-11-02 04:35:32 +01:00
) {
super ( "Doesn't change anything, but emits warnings and errors" , [ ] , "ValidateLayer" )
this . _prevalidation = new PrevalidateLayer (
path ,
isBuiltin ,
doesImageExist ,
2024-08-02 19:06:14 +02:00
studioValidations ,
2023-11-02 04:35:32 +01:00
)
this . _skipDefaultLayers = skipDefaultLayers
}
convert (
json : LayerConfigJson ,
2024-08-02 19:06:14 +02:00
context : ConversionContext ,
2023-11-02 04:35:32 +01:00
) : { parsed : LayerConfig ; raw : LayerConfigJson } {
context = context . inOperation ( this . name )
if ( typeof json === "string" ) {
context . err (
2024-08-02 19:06:14 +02:00
` Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed ` ,
2023-11-02 04:35:32 +01:00
)
return undefined
}
if ( this . _skipDefaultLayers && Constants . added_by_default . indexOf ( < any > json . id ) >= 0 ) {
return { parsed : undefined , raw : json }
}
this . _prevalidation . convert ( json , context . inOperation ( this . _prevalidation . name ) )
if ( context . hasErrors ( ) ) {
return undefined
}
let layerConfig : LayerConfig
try {
layerConfig = new LayerConfig ( json , "validation" , true )
} catch ( e ) {
context . err ( "Could not parse layer due to:" + e )
return undefined
}
2023-11-11 14:52:01 +01:00
2023-11-02 04:35:32 +01:00
for ( let i = 0 ; i < ( layerConfig . calculatedTags ? ? [ ] ) . length ; i ++ ) {
const [ _ , code , __ ] = layerConfig . calculatedTags [ i ]
try {
new Function ( "feat" , "return " + code + ";" )
} catch ( e ) {
context
. enters ( "calculatedTags" , i )
. err (
2024-08-02 19:06:14 +02:00
` Invalid function definition: the custom javascript is invalid: ${ e } . The offending javascript code is: \ n ${ code } ` ,
2023-11-02 04:35:32 +01:00
)
2023-10-20 19:04:55 +02:00
}
}
2023-12-08 18:27:50 +01:00
for ( let i = 0 ; i < layerConfig . titleIcons . length ; i ++ ) {
const titleIcon = layerConfig . titleIcons [ i ]
2023-12-19 22:08:00 +01:00
if ( < any > titleIcon . render === "icons.defaults" ) {
2023-12-08 18:27:50 +01:00
context . enters ( "titleIcons" , i ) . err ( "Detected a literal 'icons.defaults'" )
}
2023-12-19 22:08:00 +01:00
if ( < any > titleIcon . render === "icons.rating" ) {
2023-12-08 18:27:50 +01:00
context . enters ( "titleIcons" , i ) . err ( "Detected a literal 'icons.rating'" )
}
}
2024-01-03 18:24:00 +01:00
for ( let i = 0 ; i < json . presets ? . length ; i ++ ) {
2024-01-03 14:57:26 +01:00
const preset = json . presets [ i ]
if (
preset . snapToLayer === undefined &&
preset . maxSnapDistance !== undefined &&
preset . maxSnapDistance !== null
) {
context
. enters ( "presets" , i , "maxSnapDistance" )
. err ( "A maxSnapDistance is given, but there is no layer given to snap to" )
}
}
2024-02-23 11:44:56 +01:00
if ( json [ "doCount" ] ) {
context . enters ( "doCount" ) . err ( "Use `isCounted` instead of `doCount`" )
}
2024-02-26 15:08:07 +01:00
if ( json . source ) {
const src = json . source
if ( src [ "isOsmCache" ] !== undefined ) {
context . enters ( "source" ) . err ( "isOsmCache is deprecated" )
}
if ( src [ "maxCacheAge" ] !== undefined ) {
context
. enters ( "source" )
. err ( "maxCacheAge is deprecated; it is " + src [ "maxCacheAge" ] )
}
}
2024-06-16 16:06:26 +02:00
if ( json . allowMove ? . [ "enableAccuraccy" ] !== undefined ) {
context
. enters ( "allowMove" , "enableAccuracy" )
. err (
2024-08-02 19:06:14 +02:00
"`enableAccuracy` is written with two C in the first occurrence and only one in the last" ,
2024-06-16 16:06:26 +02:00
)
2024-04-28 11:07:51 +02:00
}
2023-10-12 16:55:26 +02:00
return { raw : json , parsed : layerConfig }
2022-02-04 00:44:09 +01:00
}
}
2022-09-24 03:33:09 +02:00
2023-03-24 19:21:15 +01:00
export class ValidateFilter extends DesugaringStep < FilterConfigJson > {
constructor ( ) {
2023-09-21 15:29:34 +02:00
super ( "Detect common errors in the filters" , [ ] , "ValidateFilter" )
2023-03-24 19:21:15 +01:00
}
2023-10-11 04:16:52 +02:00
convert ( filter : FilterConfigJson , context : ConversionContext ) : FilterConfigJson {
2023-07-28 14:11:00 +02:00
if ( typeof filter === "string" ) {
// Calling another filter, we skip
2023-10-11 04:16:52 +02:00
return filter
2023-07-28 14:11:00 +02:00
}
2024-07-21 10:52:51 +02:00
if ( filter === undefined ) {
2024-07-18 15:35:56 +02:00
context . err ( "Trying to validate a filter, but this filter is undefined" )
return undefined
}
2023-03-24 19:21:15 +01:00
for ( const option of filter . options ) {
2023-07-28 14:11:00 +02:00
for ( let i = 0 ; i < option . fields ? . length ? ? 0 ; i ++ ) {
2023-09-21 15:29:34 +02:00
const field = option . fields [ i ]
const type = field . type ? ? "string"
2023-03-30 04:51:56 +02:00
if ( Validators . availableTypes . find ( ( t ) = > t === type ) === undefined ) {
2023-10-11 04:16:52 +02:00
context
. enters ( "fields" , i )
. err (
` Invalid filter: ${ type } is not a valid textfield type. \ n \ tTry one of ${ Array . from (
2024-08-02 19:06:14 +02:00
Validators . availableTypes ,
) . join ( "," ) } ` ,
2023-10-11 04:16:52 +02:00
)
2023-03-24 19:21:15 +01:00
}
}
}
2023-10-11 04:16:52 +02:00
return filter
2023-03-24 19:21:15 +01:00
}
}
2022-10-27 01:50:41 +02:00
export class DetectDuplicateFilters extends DesugaringStep < {
layers : LayerConfigJson [ ]
themes : LayoutConfigJson [ ]
} > {
2022-09-24 03:33:09 +02:00
constructor ( ) {
2022-10-27 01:50:41 +02:00
super (
"Tries to detect layers where a shared filter can be used (or where similar filters occur)" ,
[ ] ,
2024-08-02 19:06:14 +02:00
"DetectDuplicateFilters" ,
2023-09-21 15:29:34 +02:00
)
2022-09-24 03:33:09 +02:00
}
2022-10-27 01:50:41 +02:00
convert (
json : { layers : LayerConfigJson [ ] ; themes : LayoutConfigJson [ ] } ,
2024-08-02 19:06:14 +02:00
context : ConversionContext ,
2023-10-11 04:16:52 +02:00
) : { layers : LayerConfigJson [ ] ; themes : LayoutConfigJson [ ] } {
2023-09-21 15:29:34 +02:00
const { layers , themes } = json
2022-10-27 01:50:41 +02:00
const perOsmTag = new Map <
string ,
{
layer : LayerConfigJson
layout : LayoutConfigJson | undefined
filter : FilterConfigJson
} [ ]
2023-09-21 15:29:34 +02:00
> ( )
2022-09-24 03:33:09 +02:00
for ( const layer of layers ) {
2023-09-21 15:29:34 +02:00
this . addLayerFilters ( layer , perOsmTag )
2022-09-24 03:33:09 +02:00
}
for ( const theme of themes ) {
2022-10-27 01:50:41 +02:00
if ( theme . id === "personal" ) {
2023-09-21 15:29:34 +02:00
continue
2022-09-24 03:33:09 +02:00
}
for ( const layer of theme . layers ) {
2022-10-27 01:50:41 +02:00
if ( typeof layer === "string" ) {
2023-09-21 15:29:34 +02:00
continue
2022-09-24 03:33:09 +02:00
}
2022-10-27 01:50:41 +02:00
if ( layer [ "builtin" ] !== undefined ) {
2023-09-21 15:29:34 +02:00
continue
2022-09-24 03:33:09 +02:00
}
2023-09-21 15:29:34 +02:00
this . addLayerFilters ( < LayerConfigJson > layer , perOsmTag , theme )
2022-09-24 03:33:09 +02:00
}
}
// At this point, we have gathered all filters per tag - time to find duplicates
perOsmTag . forEach ( ( value , key ) = > {
2022-10-27 01:50:41 +02:00
if ( value . length <= 1 ) {
2022-09-24 03:33:09 +02:00
// Seen this key just once, it is unique
2023-09-21 15:29:34 +02:00
return
2022-09-24 03:33:09 +02:00
}
2023-09-21 15:29:34 +02:00
let msg = "Possible duplicate filter: " + key
2023-09-01 16:06:22 +02:00
for ( const { filter , layer , layout } of value ) {
2023-09-21 15:29:34 +02:00
let id = ""
2022-10-27 01:50:41 +02:00
if ( layout !== undefined ) {
2023-09-21 15:29:34 +02:00
id = layout . id + ":"
2022-09-24 03:33:09 +02:00
}
2023-09-21 15:29:34 +02:00
msg += ` \ n - ${ id } ${ layer . id } . ${ filter . id } `
2022-09-24 03:33:09 +02:00
}
2023-10-11 04:16:52 +02:00
context . warn ( msg )
2023-09-21 15:29:34 +02:00
} )
2022-09-24 03:33:09 +02:00
2023-10-11 04:16:52 +02:00
return json
2022-09-24 03:33:09 +02:00
}
2023-02-03 03:57:30 +01:00
/ * *
* Add all filter options into 'perOsmTag'
* /
private addLayerFilters (
layer : LayerConfigJson ,
perOsmTag : Map <
string ,
{
layer : LayerConfigJson
layout : LayoutConfigJson | undefined
filter : FilterConfigJson
} [ ]
> ,
2024-08-02 19:06:14 +02:00
layout? : LayoutConfigJson | undefined ,
2023-02-03 03:57:30 +01:00
) : void {
if ( layer . filter === undefined || layer . filter === null ) {
2023-09-21 15:29:34 +02:00
return
2023-02-03 03:57:30 +01:00
}
if ( layer . filter [ "sameAs" ] !== undefined ) {
2023-09-21 15:29:34 +02:00
return
2023-02-03 03:57:30 +01:00
}
for ( const filter of < ( string | FilterConfigJson ) [ ] > layer . filter ) {
if ( typeof filter === "string" ) {
2023-09-21 15:29:34 +02:00
continue
2023-02-03 03:57:30 +01:00
}
if ( filter [ "#" ] ? . indexOf ( "ignore-possible-duplicate" ) >= 0 ) {
2023-09-21 15:29:34 +02:00
continue
2023-02-03 03:57:30 +01:00
}
for ( const option of filter . options ) {
if ( option . osmTags === undefined ) {
2023-09-21 15:29:34 +02:00
continue
2023-02-03 03:57:30 +01:00
}
2023-09-21 15:29:34 +02:00
const key = JSON . stringify ( option . osmTags )
2023-02-03 03:57:30 +01:00
if ( ! perOsmTag . has ( key ) ) {
2023-09-21 15:29:34 +02:00
perOsmTag . set ( key , [ ] )
2023-02-03 03:57:30 +01:00
}
perOsmTag . get ( key ) . push ( {
layer ,
filter ,
2023-09-21 15:29:34 +02:00
layout ,
} )
2023-02-03 03:57:30 +01:00
}
}
}
2022-09-24 03:33:09 +02:00
}
2023-09-24 00:25:10 +02:00
export class DetectDuplicatePresets extends DesugaringStep < LayoutConfig > {
constructor ( ) {
super (
"Detects mappings which have identical (english) names or identical mappings." ,
[ "presets" ] ,
2024-08-02 19:06:14 +02:00
"DetectDuplicatePresets" ,
2023-09-24 00:25:10 +02:00
)
}
2023-10-11 04:16:52 +02:00
convert ( json : LayoutConfig , context : ConversionContext ) : LayoutConfig {
2023-09-24 00:25:10 +02:00
const presets : PresetConfig [ ] = [ ] . concat ( . . . json . layers . map ( ( l ) = > l . presets ) )
const enNames = presets . map ( ( p ) = > p . title . textFor ( "en" ) )
if ( new Set ( enNames ) . size != enNames . length ) {
const dups = Utils . Duplicates ( enNames )
const layersWithDup = json . layers . filter ( ( l ) = >
2024-08-02 19:06:14 +02:00
l . presets . some ( ( p ) = > dups . indexOf ( p . title . textFor ( "en" ) ) >= 0 ) ,
2023-09-24 00:25:10 +02:00
)
const layerIds = layersWithDup . map ( ( l ) = > l . id )
2023-10-11 04:16:52 +02:00
context . err (
2024-05-27 10:21:34 +02:00
` This theme has multiple presets which are named: ${ dups } , namely layers ${ layerIds . join (
2024-08-02 19:06:14 +02:00
", " ,
) } this is confusing for contributors and is probably the result of reusing the same layer multiple times . Use \ ` {"override": {"=presets": []}} \` to remove some presets ` ,
2023-09-24 00:25:10 +02:00
)
}
const optimizedTags = < TagsFilter [ ] > presets . map ( ( p ) = > new And ( p . tags ) . optimize ( ) )
for ( let i = 0 ; i < presets . length ; i ++ ) {
const presetATags = optimizedTags [ i ]
const presetA = presets [ i ]
for ( let j = i + 1 ; j < presets . length ; j ++ ) {
const presetBTags = optimizedTags [ j ]
const presetB = presets [ j ]
if (
Utils . SameObject ( presetATags , presetBTags ) &&
Utils . sameList (
presetA . preciseInput . snapToLayers ,
2024-08-02 19:06:14 +02:00
presetB . preciseInput . snapToLayers ,
2023-09-24 00:25:10 +02:00
)
) {
2023-10-11 04:16:52 +02:00
context . err (
2024-05-27 10:21:34 +02:00
` This theme has multiple presets with the same tags: ${ presetATags . asHumanString (
2023-09-24 00:25:10 +02:00
false ,
false ,
2024-08-02 19:06:14 +02:00
{ } ,
2023-09-24 00:25:10 +02:00
) } , namely the preset '${presets[i].title.textFor("en")}' and ' $ { presets [
j
2024-08-02 19:06:14 +02:00
] . title . textFor ( "en" ) } ' ` ,
2023-09-24 00:25:10 +02:00
)
}
}
}
2023-10-11 04:16:52 +02:00
return json
2023-09-24 00:25:10 +02:00
}
}
2024-01-23 22:03:22 +01:00
2024-02-15 17:48:26 +01:00
export class ValidateThemeEnsemble extends Conversion <
LayoutConfig [ ] ,
Map <
string ,
{
tags : TagsFilter
foundInTheme : string [ ]
2024-03-11 16:36:03 +01:00
isCounted : boolean
2024-02-15 17:48:26 +01:00
}
>
> {
2024-01-23 22:03:22 +01:00
constructor ( ) {
2024-02-15 17:48:26 +01:00
super (
"Validates that all themes together are logical, i.e. no duplicate ids exists within (overriden) themes" ,
[ ] ,
2024-08-02 19:06:14 +02:00
"ValidateThemeEnsemble" ,
2024-02-15 17:48:26 +01:00
)
2024-01-23 22:03:22 +01:00
}
2024-02-15 17:48:26 +01:00
convert (
json : LayoutConfig [ ] ,
2024-08-02 19:06:14 +02:00
context : ConversionContext ,
2024-02-15 17:48:26 +01:00
) : Map <
string ,
{
tags : TagsFilter
2024-04-13 02:40:21 +02:00
foundInTheme : string [ ]
2024-03-11 16:36:03 +01:00
isCounted : boolean
2024-02-15 17:48:26 +01:00
}
> {
2024-04-13 02:40:21 +02:00
const idToSource = new Map <
string ,
{ tags : TagsFilter ; foundInTheme : string [ ] ; isCounted : boolean }
> ( )
2024-01-23 22:03:22 +01:00
for ( const theme of json ) {
2024-07-21 10:52:51 +02:00
if ( theme . id === "personal" ) {
2024-07-20 19:46:22 +02:00
continue
}
2024-01-23 22:03:22 +01:00
for ( const layer of theme . layers ) {
if ( typeof layer . source === "string" ) {
continue
}
if ( Constants . priviliged_layers . indexOf ( < any > layer . id ) >= 0 ) {
continue
}
if ( ! layer . source ) {
console . log ( theme , layer , layer . source )
context . enters ( theme . id , "layers" , "source" , layer . id ) . err ( "No source defined" )
continue
}
if ( layer . source . geojsonSource ) {
continue
}
const id = layer . id
const tags = layer . source . osmTags
if ( ! idToSource . has ( id ) ) {
2024-03-11 16:36:03 +01:00
idToSource . set ( id , { tags , foundInTheme : [ theme . id ] , isCounted : layer.doCount } )
2024-01-23 22:03:22 +01:00
continue
}
const oldTags = idToSource . get ( id ) . tags
const oldTheme = idToSource . get ( id ) . foundInTheme
if ( oldTags . shadows ( tags ) && tags . shadows ( oldTags ) ) {
// All is good, all is well
oldTheme . push ( theme . id )
2024-03-11 16:36:03 +01:00
idToSource . get ( id ) . isCounted || = layer . doCount
2024-01-23 22:03:22 +01:00
continue
}
2024-02-15 17:48:26 +01:00
context . err (
[
"The layer with id '" +
2024-08-02 19:06:14 +02:00
id +
"' is found in multiple themes with different tag definitions:" ,
2024-02-15 17:48:26 +01:00
"\t In theme " + oldTheme + ":\t" + oldTags . asHumanString ( false , false , { } ) ,
"\tIn theme " + theme . id + ":\t" + tags . asHumanString ( false , false , { } ) ,
2024-08-02 19:06:14 +02:00
] . join ( "\n" ) ,
2024-02-15 17:48:26 +01:00
)
2024-01-23 22:03:22 +01:00
}
}
return idToSource
}
}