2020-11-11 16:23:49 +01:00
import { Translation } from "../../UI/i18n/Translation"
2024-10-17 04:06:03 +02:00
import { ThemeConfigJson } from "./Json/ThemeConfigJson"
2021-08-07 23:11:34 +02:00
import LayerConfig from "./LayerConfig"
2021-09-07 00:23:00 +02:00
import { LayerConfigJson } from "./Json/LayerConfigJson"
2021-09-29 16:55:05 +02:00
import Constants from "../Constants"
2022-02-14 04:48:33 +01:00
import ExtraLinkConfig from "./ExtraLinkConfig"
2023-01-13 02:48:48 +01:00
import { Utils } from "../../Utils"
2023-03-09 17:11:44 +01:00
import LanguageUtils from "../../Utils/LanguageUtils"
2023-04-21 16:19:36 +02:00
import { RasterLayerProperties } from "../RasterLayerProperties"
2024-04-22 14:43:05 +02:00
import { Translatable } from "./Json/Translatable"
2025-03-17 01:17:02 +01:00
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import TagRenderingConfig from "./TagRenderingConfig"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
2023-04-21 01:53:24 +02:00
2024-08-22 02:54:46 +02:00
/ * *
* Minimal information about a theme
* * /
2024-10-17 04:06:03 +02:00
export class MinimalThemeInformation {
2024-08-22 02:54:46 +02:00
id : string
icon : string
title : Translatable
shortDescription : Translatable
mustHaveLanguage? : boolean
hideFromOverview? : boolean
2024-08-27 21:33:47 +02:00
keywords? : Record < string , string [ ] >
2024-09-05 02:25:03 +02:00
layers : string [ ]
2024-08-22 02:54:46 +02:00
}
2024-10-12 12:57:09 +02:00
2023-02-11 15:04:20 +01:00
/ * *
* Minimal information about a theme
* * /
2024-10-17 04:06:03 +02:00
export class ThemeInformation {
2023-02-11 15:04:20 +01:00
id : string
icon : string
2024-04-22 14:43:05 +02:00
title : Translatable | Translation
2024-06-16 16:06:26 +02:00
shortDescription : Translatable | Translation
definition? : Translatable | Translation
2023-02-11 15:04:20 +01:00
mustHaveLanguage? : boolean
hideFromOverview? : boolean
2024-06-16 16:06:26 +02:00
keywords ? : ( Translatable | Translation ) [ ]
2023-02-11 15:04:20 +01:00
}
2024-10-17 04:06:03 +02:00
export default class ThemeConfig implements ThemeInformation {
2022-03-08 04:09:03 +01:00
public static readonly defaultSocialImage = "assets/SocialImage.png"
2020-11-11 16:23:49 +01:00
public readonly id : string
2021-04-09 17:56:13 +02:00
public readonly credits? : string
2023-10-30 13:45:44 +01:00
/ * *
* The languages this theme supports .
* Defaults to all languages the title has
* /
2020-11-11 16:23:49 +01:00
public readonly language : string [ ]
public readonly title : Translation
2021-12-21 18:35:31 +01:00
public readonly shortDescription : Translation
2020-11-11 16:23:49 +01:00
public readonly description : Translation
public readonly descriptionTail? : Translation
public readonly icon : string
public readonly socialImage? : string
public readonly startZoom : number
public readonly startLat : number
public readonly startLon : number
2022-09-14 12:18:51 +02:00
public defaultBackgroundId? : string
2021-05-17 00:18:21 +02:00
public layers : LayerConfig [ ]
2023-04-21 01:53:24 +02:00
public tileLayerSources : ( RasterLayerProperties & { defaultState? : true | boolean } ) [ ]
2020-11-11 16:23:49 +01:00
public readonly hideFromOverview : boolean
2021-05-27 18:56:02 +02:00
public lockLocation : boolean | [ [ number , number ] , [ number , number ] ]
2020-11-11 16:23:49 +01:00
public readonly enableUserBadge : boolean
public readonly enableShareScreen : boolean
public readonly enableMoreQuests : boolean
public readonly enableAddNewPoints : boolean
public readonly enableLayers : boolean
public readonly enableSearch : boolean
public readonly enableGeolocation : boolean
2021-01-04 18:55:10 +01:00
public readonly enableBackgroundLayerSelection : boolean
2021-05-17 00:18:21 +02:00
public readonly enableShowAllQuestions : boolean
2021-07-16 01:42:09 +02:00
public readonly enableExportButton : boolean
2021-07-28 02:51:07 +02:00
public readonly enablePdfDownload : boolean
2024-02-03 14:33:10 +01:00
public readonly enableTerrain : boolean
2024-05-06 14:23:54 +02:00
public readonly enableMorePrivacy : boolean
2020-11-11 16:23:49 +01:00
public readonly customCss? : string
2022-09-08 21:40:48 +02:00
2021-09-29 16:55:05 +02:00
public readonly overpassUrl : string [ ]
2022-09-14 12:18:51 +02:00
public overpassTimeout : number
2021-10-15 05:20:02 +02:00
public readonly overpassMaxZoom : number
public readonly osmApiTileSize : number
2021-09-18 02:32:40 +02:00
public readonly official : boolean
2022-02-14 04:48:33 +01:00
2024-04-22 14:43:05 +02:00
private usedImages : string [ ]
2022-02-14 04:48:33 +01:00
public readonly extraLink? : ExtraLinkConfig
2022-07-01 00:16:05 +02:00
public readonly definedAtUrl? : string
2022-04-18 02:39:30 +02:00
public readonly definitionRaw? : string
2022-07-01 00:16:05 +02:00
2023-06-15 02:42:12 +02:00
private readonly layersDict : Map < string , LayerConfig >
2025-01-08 15:11:17 +01:00
public readonly source : ThemeConfigJson
2024-08-02 13:32:47 +02:00
public readonly enableCache : boolean
2023-06-15 02:42:12 +02:00
2025-03-17 01:17:02 +01:00
public readonly popups : Readonly < {
2025-03-17 02:54:12 +01:00
id : string
dismissible? : boolean
condition : TagsFilter
title : TagRenderingConfig
2025-03-17 01:17:02 +01:00
body : TagRenderingConfig [ ]
} > [ ]
2022-07-01 00:16:05 +02:00
constructor (
2024-10-17 04:06:03 +02:00
json : ThemeConfigJson ,
2022-07-01 00:16:05 +02:00
official = true ,
options ? : {
2022-04-18 02:39:30 +02:00
definedAtUrl? : string
definitionRaw? : string
2025-01-18 00:30:06 +01:00
}
2022-04-18 02:39:30 +02:00
) {
2023-10-30 13:45:44 +01:00
if ( json === undefined ) {
throw "Cannot construct a layout config, the parameter 'json' is undefined"
}
2024-04-22 14:43:05 +02:00
this . source = json
2021-09-18 02:32:40 +02:00
this . official = official
2020-11-11 16:23:49 +01:00
this . id = json . id
2022-04-18 02:39:30 +02:00
this . definedAtUrl = options ? . definedAtUrl
this . definitionRaw = options ? . definitionRaw
2024-08-02 13:32:47 +02:00
this . enableCache = json . enableCache ? ? true
2022-01-26 21:40:38 +01:00
if ( official ) {
if ( json . id . toLowerCase ( ) !== json . id ) {
throw "The id of a theme should be lowercase: " + json . id
2022-01-08 13:04:11 +01:00
}
2022-01-26 21:40:38 +01:00
if ( json . id . match ( /[a-z0-9-_]/ ) == null ) {
throw "The id of a theme should match [a-z0-9-_]*: " + json . id
2022-01-08 13:04:11 +01:00
}
2021-12-30 22:36:34 +01:00
}
2022-07-01 00:16:05 +02:00
const context = this . id
2023-10-30 16:32:43 +01:00
this . credits = Array . isArray ( json . credits ) ? json . credits . join ( "; " ) : json . credits
2023-10-16 14:27:05 +02:00
if ( ! json . title ) {
2023-10-10 13:27:56 +02:00
throw ` The theme ${ json . id } does not have a title defined. `
}
2022-10-04 14:23:19 +02:00
this . language = json . mustHaveLanguage ? ? Object . keys ( json . title )
2024-04-22 14:43:05 +02:00
2021-12-21 18:35:31 +01:00
{
2022-01-27 01:23:04 +01:00
if ( typeof json . title === "string" ) {
2022-01-31 20:52:56 +01:00
throw ` The title of a theme should always be a translation, as it sets the corresponding languages ( ${ context } .title). The themenID is ${
this . id
} ; the offending object is $ { JSON . stringify (
2025-01-18 00:30:06 +01:00
json . title
2022-01-31 20:52:56 +01:00
) } which is a $ { typeof json . title } ) `
2022-01-27 01:23:04 +01:00
}
2021-12-21 18:35:31 +01:00
if ( this . language . length == 0 ) {
2023-10-30 13:45:44 +01:00
throw ` No languages defined. Define at least one language. You can do this by adding a title `
2021-12-21 18:35:31 +01:00
}
if ( json . title === undefined ) {
throw "Title not defined in " + this . id
}
if ( json . description === undefined ) {
throw "Description not defined in " + this . id
}
2024-04-22 14:43:05 +02:00
2021-12-21 18:35:31 +01:00
if ( json [ "hideInOverview" ] ) {
throw (
"The json for " +
this . id +
" contains a 'hideInOverview'. Did you mean hideFromOverview instead?"
2022-09-08 21:40:48 +02:00
)
2021-12-21 18:35:31 +01:00
}
if ( json . layers === undefined ) {
throw "Got undefined layers for " + json . id + " at " + context
}
2020-11-11 16:23:49 +01:00
}
2022-07-01 00:16:05 +02:00
this . title = new Translation ( json . title , "themes:" + context + ".title" )
this . description = new Translation ( json . description , "themes:" + context + ".description" )
this . shortDescription =
json . shortDescription === undefined
? this . description . FirstSentence ( )
: new Translation ( json . shortDescription , "themes:" + context + ".shortdescription" )
this . descriptionTail =
json . descriptionTail === undefined
? undefined
: new Translation ( json . descriptionTail , "themes:" + context + ".descriptionTail" )
2020-11-11 16:23:49 +01:00
this . icon = json . icon
2024-10-17 04:06:03 +02:00
this . socialImage = json . socialImage ? ? ThemeConfig . defaultSocialImage
2022-03-08 04:09:03 +01:00
if ( this . socialImage === "" ) {
2022-01-26 21:40:38 +01:00
if ( official ) {
2022-03-08 04:09:03 +01:00
throw "Theme " + json . id + " has empty string as social image"
2022-01-18 20:18:12 +01:00
}
}
2020-11-11 16:23:49 +01:00
this . startZoom = json . startZoom
this . startLat = json . startLat
this . startLon = json . startLon
2022-09-08 21:40:48 +02:00
2020-11-11 16:23:49 +01:00
this . defaultBackgroundId = json . defaultBackgroundId
2023-04-21 01:53:24 +02:00
this . tileLayerSources = json . tileLayerSources ? ? [ ]
2021-12-21 18:35:31 +01:00
// At this point, layers should be expanded and validated either by the generateScript or the LegacyJsonConvert
this . layers = json . layers . map (
( lyrJson ) = >
new LayerConfig (
< LayerConfigJson > lyrJson ,
json . id + ".layers." + lyrJson [ "id" ] ,
2025-01-08 16:18:55 +01:00
official ,
2025-01-18 00:30:06 +01:00
< LayerConfigJson [ ] > json . layers
)
2022-09-08 21:40:48 +02:00
)
2022-07-01 00:16:05 +02:00
this . extraLink = new ExtraLinkConfig (
json . extraLink ? ? {
2022-02-14 04:48:33 +01:00
icon : "./assets/svg/pop-out.svg" ,
2022-07-01 00:16:05 +02:00
href : "https://{basepath}/{theme}.html?lat={lat}&lon={lon}&z={zoom}&language={language}" ,
2022-02-14 04:48:33 +01:00
newTab : true ,
2025-03-17 02:54:12 +01:00
requirements : [ "iframe" , "no-welcome-message" ] ,
2022-07-01 00:16:05 +02:00
} ,
2025-01-18 00:30:06 +01:00
context + ".extraLink"
2022-07-01 00:16:05 +02:00
)
2021-11-07 16:34:51 +01:00
2025-03-17 01:17:02 +01:00
this . popups = ( json . popup ? ? [ ] ) . map ( ( p , i ) = > {
const ctx = context + ".popup." + i
if ( ! p . id ) {
2025-03-17 02:54:12 +01:00
throw ctx + ": an id is required"
2025-03-17 01:17:02 +01:00
}
const body : TagRenderingConfigJson [ ] = Array . isArray ( p . body ) ? p . body : [ p . body ]
return {
id : p.id ,
dismissible : p.dismissible ? ? false ,
condition : TagUtils.Tag ( p . condition ) ,
title : new TagRenderingConfig ( p . title , ctx + ".title" ) ,
2025-03-17 02:54:12 +01:00
body : body.map ( ( body , i ) = > new TagRenderingConfig ( body , ctx + ".body." + i ) ) ,
2025-03-17 01:17:02 +01:00
}
} )
2020-11-11 16:23:49 +01:00
this . hideFromOverview = json . hideFromOverview ? ? false
2021-11-07 16:34:51 +01:00
this . lockLocation = < [ [ number , number ] , [ number , number ] ] > json . lockLocation ? ? undefined
2020-11-11 16:23:49 +01:00
this . enableUserBadge = json . enableUserBadge ? ? true
this . enableShareScreen = json . enableShareScreen ? ? true
this . enableMoreQuests = json . enableMoreQuests ? ? true
this . enableLayers = json . enableLayers ? ? true
this . enableSearch = json . enableSearch ? ? true
this . enableGeolocation = json . enableGeolocation ? ? true
this . enableAddNewPoints = json . enableAddNewPoints ? ? true
this . enableBackgroundLayerSelection = json . enableBackgroundLayerSelection ? ? true
2021-05-17 00:18:21 +02:00
this . enableShowAllQuestions = json . enableShowAllQuestions ? ? false
2023-04-25 02:37:23 +02:00
this . enableExportButton = json . enableDownload ? ? true
this . enablePdfDownload = json . enablePdfDownload ? ? true
2024-02-03 14:33:10 +01:00
this . enableTerrain = json . enableTerrain ? ? false
2020-11-11 16:23:49 +01:00
this . customCss = json . customCss
2023-10-30 13:45:44 +01:00
this . overpassUrl = json . overpassUrl ? ? Constants . defaultOverpassUrls
2021-08-23 15:48:42 +02:00
this . overpassTimeout = json . overpassTimeout ? ? 30
2022-01-15 02:44:11 +01:00
this . overpassMaxZoom = json . overpassMaxZoom ? ? 16
2021-10-15 05:20:02 +02:00
this . osmApiTileSize = json . osmApiTileSize ? ? this . overpassMaxZoom + 1
2024-06-16 16:06:26 +02:00
this . enableMorePrivacy =
json . enableMorePrivacy ||
json . layers . some ( ( l ) = > ( < LayerConfigJson > l ) . enableMorePrivacy )
2023-06-15 02:42:12 +02:00
this . layersDict = new Map < string , LayerConfig > ( )
for ( const layer of this . layers ) {
this . layersDict . set ( layer . id , layer )
}
2021-06-22 03:16:45 +02:00
}
2021-03-24 01:25:57 +01:00
public CustomCodeSnippets ( ) : string [ ] {
2021-09-18 02:32:40 +02:00
if ( this . official ) {
2021-03-24 01:25:57 +01:00
return [ ]
}
const msg =
"<br/><b>This layout uses <span class='alert'>custom javascript</span>, loaded for the wide internet. The code is printed below, please report suspicious code on the issue tracker of MapComplete:</b><br/>"
const custom = [ ]
for ( const layer of this . layers ) {
2021-04-17 11:37:22 +02:00
custom . push ( . . . layer . CustomCodeSnippets ( ) . map ( ( code ) = > code + "<br />" ) )
2021-03-24 01:25:57 +01:00
}
if ( custom . length === 0 ) {
return custom
}
custom . splice ( 0 , 0 , msg )
return custom
}
2021-04-17 11:37:22 +02:00
2023-06-15 02:42:12 +02:00
public getLayer ( id : string ) {
return this . layersDict . get ( id )
}
2021-11-07 16:34:51 +01:00
public isLeftRightSensitive() {
2021-10-22 14:01:40 +02:00
return this . layers . some ( ( l ) = > l . isLeftRightSensitive ( ) )
}
2021-12-04 21:49:17 +01:00
2024-02-15 17:39:59 +01:00
public hasNoteLayer() {
return this . layers . some ( ( l ) = > l . id === "note" )
}
public hasPresets() {
return this . layers . some ( ( l ) = > l . presets ? . length > 0 )
}
2023-12-26 12:09:48 +01:00
public missingTranslations ( extraInspection : any ) : {
2023-01-13 02:48:48 +01:00
untranslated : Map < string , string [ ] >
total : number
} {
let total = 0
const untranslated = new Map < string , string [ ] > ( )
Utils . WalkObject (
2024-04-22 14:43:05 +02:00
[ this , extraInspection ] ,
2023-01-13 02:48:48 +01:00
( o ) = > {
const translation = < Translation > ( < any > o )
if ( translation . translations [ "*" ] !== undefined ) {
return
}
if ( translation . context === undefined || translation . context . indexOf ( ":" ) < 0 ) {
// no source given - lets ignore
return
}
total ++
2023-03-09 17:11:44 +01:00
LanguageUtils . usedLanguagesSorted . forEach ( ( ln ) = > {
2023-01-13 02:48:48 +01:00
const trans = translation . translations
if ( trans [ "*" ] !== undefined ) {
return
}
2023-02-09 02:45:19 +01:00
if ( translation . context . indexOf ( ":" ) < 0 ) {
return
}
2023-01-13 02:48:48 +01:00
if ( trans [ ln ] === undefined ) {
if ( ! untranslated . has ( ln ) ) {
untranslated . set ( ln , [ ] )
}
2024-10-19 14:44:55 +02:00
untranslated . get ( ln ) . push ( translation . context )
2023-01-13 02:48:48 +01:00
}
} )
} ,
( o ) = > {
if ( o === undefined || o === null ) {
return false
}
return o instanceof Translation
2025-01-18 00:30:06 +01:00
}
2023-01-13 02:48:48 +01:00
)
2023-02-09 02:45:19 +01:00
return { untranslated , total }
2023-01-13 02:48:48 +01:00
}
2024-10-12 12:57:09 +02:00
2024-12-11 02:45:44 +01:00
public getMatchingLayer (
tags : Record < string , string > ,
2025-01-18 00:30:06 +01:00
blacklistLayers? : Set < string >
2024-12-11 02:45:44 +01:00
) : LayerConfig | undefined {
2021-12-04 21:49:17 +01:00
if ( tags === undefined ) {
2021-11-08 20:49:51 +01:00
return undefined
}
2024-04-13 02:40:21 +02:00
if ( tags . id . startsWith ( "current_view" ) ) {
2024-02-29 10:57:44 +01:00
return this . getLayer ( "current_view" )
}
2021-11-08 20:49:51 +01:00
for ( const layer of this . layers ) {
2024-12-11 02:45:44 +01:00
if ( blacklistLayers ? . has ( layer . id ) ) {
2024-11-25 23:44:26 +01:00
continue
}
2023-04-20 18:58:31 +02:00
if ( ! layer . source ) {
2023-11-22 19:39:19 +01:00
if ( layer . isShown ? . matchesProperties ( tags ) ) {
return layer
}
2023-04-20 18:58:31 +02:00
continue
}
2021-11-08 20:49:51 +01:00
if ( layer . source . osmTags . matchesProperties ( tags ) ) {
2024-06-16 16:06:26 +02:00
if ( ! layer . isShown || layer . isShown . matchesProperties ( tags ) ) {
2025-03-06 02:17:29 +01:00
// https://source.mapcomplete.org/MapComplete/MapComplete/issues/1959
2024-05-23 14:32:53 +02:00
return layer
}
2021-11-08 20:49:51 +01:00
}
}
2024-10-19 14:44:55 +02:00
console . trace (
"Fallthrough: could not find the appropriate layer for an object with tags" ,
tags ,
"within layout" ,
2025-01-18 00:30:06 +01:00
this
2024-10-19 14:44:55 +02:00
)
2021-11-08 20:49:51 +01:00
return undefined
}
2024-04-22 14:43:05 +02:00
2024-06-16 16:06:26 +02:00
public getUsedImages() {
if ( this . usedImages ) {
2024-04-22 14:43:05 +02:00
return this . usedImages
}
const json = this . source
// The 'favourite'-layer contains pretty much all images as it bundles all layers, so we exclude it
2024-06-16 16:06:26 +02:00
const jsonNoFavourites = {
. . . json ,
2025-03-17 02:54:12 +01:00
layers : json.layers.filter ( ( l ) = > l [ "id" ] !== "favourite" ) ,
2024-06-16 16:06:26 +02:00
}
2024-08-23 02:16:24 +02:00
const usedImages = jsonNoFavourites . _usedImages
2024-06-27 03:39:04 +02:00
usedImages . sort ( )
this . usedImages = Utils . Dedup ( usedImages )
2024-04-22 14:43:05 +02:00
return this . usedImages
}
2020-11-11 16:23:49 +01:00
}