2024-08-11 12:03:24 +02:00
import { Conversion , DesugaringStep , Fuse } 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 { And } from "../../../Logic/Tags/And"
import FilterConfigJson from "../Json/FilterConfigJson"
2024-08-11 12:03:24 +02:00
import { 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-12 23:19:31 +01:00
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
2024-08-11 12:03:24 +02:00
import { PrevalidateLayer } from "./PrevalidateLayer"
2022-02-04 00:44:09 +01:00
2024-08-11 12:03:24 +02:00
export 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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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
}
}
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-14 13:53:56 +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 ) ) {
2024-08-14 13:53:56 +02:00
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 ']'?"
)
2024-08-02 19:06:14 +02:00
}
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
2024-08-14 13:53:56 +02:00
if ( json . layers === undefined ) {
2024-08-10 15:36:47 +02:00
context . err ( "This theme has no layers defined" )
2024-08-14 13:53:56 +02:00
} 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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +02:00
"DetectNonErasedKeysInMappings"
2023-11-09 15:42:15 +01:00
)
}
convert (
json : QuestionableTagRenderingConfigJson ,
2024-08-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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
}
}
2024-08-11 12:03:24 +02:00
export class ValidatePossibleLinks extends DesugaringStep < string | Record < string , string > > {
2023-09-02 02:04:59 +02:00
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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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
}
}
2024-08-11 12:03:24 +02:00
export class CheckTranslation extends DesugaringStep < Translatable > {
2023-11-02 04:35:32 +01:00
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-14 13:53:56 +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
}
}
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-14 13:53:56 +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-14 13:53:56 +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-08-11 12:03:24 +02:00
export class ValidatePointRendering extends DesugaringStep < PointRenderingConfigJson > {
2024-01-12 23:19:31 +01:00
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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +02:00
studioValidations
2023-11-02 04:35:32 +01:00
)
this . _skipDefaultLayers = skipDefaultLayers
}
convert (
json : LayerConfigJson ,
2024-08-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +02:00
{ }
2023-09-24 00:25:10 +02:00
) } , namely the preset '${presets[i].title.textFor("en")}' and ' $ { presets [
j
2024-08-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +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-14 13:53:56 +02:00
] . join ( "\n" )
2024-02-15 17:48:26 +01:00
)
2024-01-23 22:03:22 +01:00
}
}
return idToSource
}
}