2020-11-11 16:23:49 +01:00
import { Translation } from "../../UI/i18n/Translation" ;
import TagRenderingConfig from "./TagRenderingConfig" ;
2021-08-07 23:11:34 +02:00
import { LayoutConfigJson } from "./Json/LayoutConfigJson" ;
import SharedTagRenderings from "../../Customizations/SharedTagRenderings" ;
import AllKnownLayers from "../../Customizations/AllKnownLayers" ;
2021-01-06 02:52:38 +01:00
import { Utils } from "../../Utils" ;
2021-08-07 23:11:34 +02:00
import LayerConfig from "./LayerConfig" ;
import { Unit } from "../Unit" ;
import { Denomination } from "../Denomination" ;
2020-11-11 16:23:49 +01:00
export default class LayoutConfig {
public readonly id : string ;
public readonly maintainer : string ;
2021-04-09 17:56:13 +02:00
public readonly credits? : string ;
2020-11-11 16:23:49 +01:00
public readonly changesetmessage? : string ;
public readonly version : string ;
public readonly language : string [ ] ;
public readonly title : Translation ;
public readonly shortDescription? : Translation ;
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 ;
public readonly widenFactor : number ;
public readonly roamingRenderings : TagRenderingConfig [ ] ;
public readonly defaultBackgroundId? : string ;
2021-05-17 00:18:21 +02:00
public layers : LayerConfig [ ] ;
2021-01-06 02:52:38 +01:00
public readonly clustering ? : {
2021-01-05 00:21:00 +01:00
maxZoom : number ,
minNeededElements : number
2021-01-04 20:09:07 +01:00
} ;
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 ;
2020-11-11 16:23:49 +01:00
public readonly customCss? : string ;
2021-04-17 15:42:22 +02:00
/ *
How long is the cache valid , in seconds ?
* /
public readonly cacheTimeout? : number ;
2021-06-22 03:16:45 +02:00
public readonly units : Unit [ ] = [ ]
2021-04-17 11:37:22 +02:00
private readonly _official : boolean ;
2020-11-11 16:23:49 +01:00
2021-04-17 11:37:22 +02:00
constructor ( json : LayoutConfigJson , official = true , context? : string ) {
2021-03-24 01:25:57 +01:00
this . _official = official ;
2020-11-11 16:23:49 +01:00
this . id = json . id ;
2021-01-04 18:55:10 +01:00
context = ( context ? ? "" ) + "." + this . id ;
2020-11-11 16:23:49 +01:00
this . maintainer = json . maintainer ;
2021-04-09 17:56:13 +02:00
this . credits = json . credits ;
2020-11-11 16:23:49 +01:00
this . changesetmessage = json . changesetmessage ;
this . version = json . version ;
this . language = [ ] ;
if ( typeof json . language === "string" ) {
this . language = [ json . language ] ;
} else {
this . language = json . language ;
}
2021-03-24 01:25:57 +01:00
if ( this . language . length == 0 ) {
2021-07-15 20:47:28 +02:00
throw ` No languages defined. Define at least one language. ( ${ context } .languages) `
2021-03-13 19:08:31 +01:00
}
2021-01-04 18:55:10 +01:00
if ( json . title === undefined ) {
throw "Title not defined in " + this . id ;
2020-11-11 16:23:49 +01:00
}
2021-01-04 18:55:10 +01:00
if ( json . description === undefined ) {
throw "Description not defined in " + this . id ;
2020-11-11 16:23:49 +01:00
}
2021-06-24 14:21:41 +02:00
this . units = LayoutConfig . ExtractUnits ( json , context ) ? ? [ ] ;
2021-01-04 18:55:10 +01:00
this . title = new Translation ( json . title , context + ".title" ) ;
this . description = new Translation ( json . description , context + ".description" ) ;
this . shortDescription = json . shortDescription === undefined ? this . description . FirstSentence ( ) : new Translation ( json . shortDescription , context + ".shortdescription" ) ;
2021-03-24 01:25:57 +01:00
this . descriptionTail = json . descriptionTail === undefined ? new Translation ( { "*" : "" } , context + ".descriptionTail" ) : new Translation ( json . descriptionTail , context + ".descriptionTail" ) ;
2020-11-11 16:23:49 +01:00
this . icon = json . icon ;
this . socialImage = json . socialImage ;
this . startZoom = json . startZoom ;
this . startLat = json . startLat ;
this . startLon = json . startLon ;
this . widenFactor = json . widenFactor ? ? 0.05 ;
this . roamingRenderings = ( json . roamingRenderings ? ? [ ] ) . map ( ( tr , i ) = > {
if ( typeof tr === "string" ) {
2021-04-21 01:26:36 +02:00
if ( SharedTagRenderings . SharedTagRendering . get ( tr ) !== undefined ) {
return SharedTagRenderings . SharedTagRendering . get ( tr ) ;
2020-11-11 16:23:49 +01:00
}
}
2021-03-24 01:25:57 +01:00
return new TagRenderingConfig ( tr , undefined , ` ${ this . id } .roaming_renderings[ ${ i } ] ` ) ;
2020-11-11 16:23:49 +01:00
}
) ;
this . defaultBackgroundId = json . defaultBackgroundId ;
2021-07-26 17:38:56 +02:00
this . layers = LayoutConfig . ExtractLayers ( json , this . units , official , context ) ;
2021-05-17 00:18:21 +02:00
2021-08-07 21:19:01 +02:00
// ALl the layers are constructed, let them share tagRenderings now!
2021-03-24 01:25:57 +01:00
const roaming : { r , source : LayerConfig } [ ] = [ ]
2021-01-08 03:57:18 +01:00
for ( const layer of this . layers ) {
2021-03-24 01:25:57 +01:00
roaming . push ( { r : layer.GetRoamingRenderings ( ) , source : layer } ) ;
2021-01-08 03:57:18 +01:00
}
2021-01-04 18:55:10 +01:00
2021-01-08 03:57:18 +01:00
for ( const layer of this . layers ) {
for ( const r of roaming ) {
2021-03-24 01:25:57 +01:00
if ( r . source == layer ) {
2021-01-08 03:57:18 +01:00
continue ;
}
layer . AddRoamingRenderings ( r . r ) ;
}
}
2021-03-24 01:25:57 +01:00
for ( const layer of this . layers ) {
2021-03-09 11:42:23 +01:00
layer . AddRoamingRenderings (
{
2021-03-24 01:25:57 +01:00
titleIcons : [ ] ,
2021-03-09 11:42:23 +01:00
iconOverlays : [ ] ,
tagRenderings : this.roamingRenderings
2021-03-24 01:25:57 +01:00
}
2021-03-09 11:42:23 +01:00
) ;
}
2021-01-04 18:55:10 +01:00
2021-06-15 01:24:04 +02:00
this . clustering = {
2021-01-05 00:21:00 +01:00
maxZoom : 16 ,
2021-04-23 16:50:07 +02:00
minNeededElements : 500
2021-01-05 00:21:00 +01:00
} ;
2021-01-06 02:52:38 +01:00
if ( json . clustering ) {
2021-01-04 20:09:07 +01:00
this . clustering = {
2021-01-06 02:52:38 +01:00
maxZoom : json.clustering.maxZoom ? ? 18 ,
2021-01-05 00:21:00 +01:00
minNeededElements : json.clustering.minNeededElements ? ? 1
2021-01-04 20:09:07 +01:00
}
2021-01-04 18:55:10 +01:00
for ( const layer of this . layers ) {
2021-01-06 02:52:38 +01:00
if ( layer . wayHandling !== LayerConfig . WAYHANDLING_CENTER_ONLY ) {
2021-06-15 01:24:04 +02:00
console . debug ( "WARNING: In order to allow clustering, every layer must be set to CENTER_ONLY. Layer" , layer . id , "does not respect this for layout" , this . id ) ;
2021-01-04 18:55:10 +01:00
}
}
}
2021-01-05 00:21:00 +01:00
2020-11-11 16:23:49 +01:00
this . hideFromOverview = json . hideFromOverview ? ? false ;
2021-03-15 16:35:00 +01:00
// @ts-ignore
2021-03-24 01:25:57 +01:00
if ( json . hideInOverview ) {
throw "The json for " + this . id + " contains a 'hideInOverview'. Did you mean hideFromOverview instead?"
2021-03-15 16:35:00 +01:00
}
2021-05-27 18:56:02 +02:00
this . lockLocation = 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 ;
2021-07-28 02:51:07 +02:00
this . enableExportButton = json . enableDownload ? ? false ;
this . enablePdfDownload = json . enablePdfDownload ? ? false ;
2020-11-11 16:23:49 +01:00
this . customCss = json . customCss ;
2021-04-17 15:42:22 +02:00
this . cacheTimeout = json . cacheTimout ? ? ( 60 * 24 * 60 * 60 )
2021-06-22 00:29:07 +02:00
2021-06-22 03:16:45 +02:00
}
2021-07-26 17:38:56 +02:00
private static ExtractLayers ( json : LayoutConfigJson , units : Unit [ ] , official : boolean , context : string ) : LayerConfig [ ] {
2021-07-07 18:07:29 +02:00
const result : LayerConfig [ ] = [ ]
json . layers . forEach ( ( layer , i ) = > {
if ( typeof layer === "string" ) {
2021-07-26 17:38:56 +02:00
if ( AllKnownLayers . sharedLayersJson . get ( layer ) !== undefined ) {
2021-07-07 18:07:29 +02:00
if ( json . overrideAll !== undefined ) {
let lyr = JSON . parse ( JSON . stringify ( AllKnownLayers . sharedLayersJson [ layer ] ) ) ;
const newLayer = new LayerConfig ( Utils . Merge ( json . overrideAll , lyr ) , units , ` ${ json . id } +overrideAll.layers[ ${ i } ] ` , official )
result . push ( newLayer )
2021-07-07 18:12:07 +02:00
return
2021-07-07 18:07:29 +02:00
} else {
result . push ( AllKnownLayers . sharedLayers [ layer ] )
2021-07-07 18:12:07 +02:00
return
2021-07-07 18:07:29 +02:00
}
} else {
2021-07-28 02:51:07 +02:00
console . log ( "Layer " , layer , " not kown, try one of" , Array . from ( AllKnownLayers . sharedLayers . keys ( ) ) . join ( ", " ) )
2021-07-26 17:38:56 +02:00
throw ` Unknown builtin layer ${ layer } at ${ context } .layers[ ${ i } ] ` ;
2021-07-07 18:07:29 +02:00
}
}
2021-07-07 18:12:07 +02:00
2021-07-07 18:07:29 +02:00
if ( layer [ "builtin" ] === undefined ) {
if ( json . overrideAll !== undefined ) {
layer = Utils . Merge ( json . overrideAll , layer ) ;
}
// @ts-ignore
const newLayer = new LayerConfig ( layer , units , ` ${ json . id } .layers[ ${ i } ] ` , official )
result . push ( newLayer )
2021-07-07 18:12:07 +02:00
return
2021-07-07 18:07:29 +02:00
}
// @ts-ignore
let names = layer . builtin ;
if ( typeof names === "string" ) {
names = [ names ]
}
names . forEach ( name = > {
2021-07-26 17:38:56 +02:00
const shared = AllKnownLayers . sharedLayersJson . get ( name ) ;
2021-07-07 18:12:07 +02:00
if ( shared === undefined ) {
2021-07-26 17:38:56 +02:00
throw ` Unknown shared/builtin layer ${ name } at ${ context } .layers[ ${ i } ]. Available layers are ${ Array . from ( AllKnownLayers . sharedLayersJson . keys ( ) ) . join ( ", " ) } ` ;
2021-07-07 18:12:07 +02:00
}
// @ts-ignore
let newLayer : LayerConfigJson = Utils . Merge ( layer . override , JSON . parse ( JSON . stringify ( shared ) ) ) ; // We make a deep copy of the shared layer, in order to protect it from changes
if ( json . overrideAll !== undefined ) {
newLayer = Utils . Merge ( json . overrideAll , newLayer ) ;
}
// @ts-ignore
const layerConfig = new LayerConfig ( newLayer , units , ` ${ json . id } .layers[ ${ i } ] ` , official )
result . push ( layerConfig )
return
} )
2021-07-07 18:07:29 +02:00
} ) ;
return result
}
2021-06-22 12:13:44 +02:00
private static ExtractUnits ( json : LayoutConfigJson , context : string ) : Unit [ ] {
2021-06-22 03:16:45 +02:00
const result : Unit [ ] = [ ]
2021-06-22 00:29:07 +02:00
if ( ( json . units ? ? [ ] ) . length !== 0 ) {
for ( let i1 = 0 ; i1 < json . units . length ; i1 ++ ) {
let unit = json . units [ i1 ] ;
const appliesTo = unit . appliesToKey
for ( let i = 0 ; i < appliesTo . length ; i ++ ) {
let key = appliesTo [ i ] ;
if ( key . trim ( ) !== key ) {
throw ` ${ context } .unit[ ${ i1 } ].appliesToKey[ ${ i } ] is invalid: it starts or ends with whitespace `
}
}
if ( ( unit . applicableUnits ? ? [ ] ) . length === 0 ) {
throw ` ${ context } : define at least one applicable unit `
}
// Some keys do have unit handling
const defaultSet = unit . applicableUnits . filter ( u = > u . default === true )
// No default is defined - we pick the first as default
2021-06-22 03:16:45 +02:00
if ( defaultSet . length === 0 ) {
unit . applicableUnits [ 0 ] . default = true
2021-06-22 00:29:07 +02:00
}
2021-06-22 03:16:45 +02:00
2021-06-22 00:29:07 +02:00
// Check that there are not multiple defaults
if ( defaultSet . length > 1 ) {
throw ` Multiple units are set as default: they have canonical values of ${ defaultSet . map ( u = > u . canonicalDenomination ) . join ( ", " ) } `
}
2021-06-22 03:16:45 +02:00
const applicable = unit . applicableUnits . map ( ( u , i ) = > new Denomination ( u , ` ${ context } .units[ ${ i } ] ` ) )
2021-06-22 12:13:44 +02:00
result . push ( new Unit ( appliesTo , applicable , unit . eraseInvalidValues ? ? false ) ) ;
2021-06-22 00:29:07 +02:00
}
const seenKeys = new Set < string > ( )
2021-06-22 03:16:45 +02:00
for ( const unit of result ) {
const alreadySeen = Array . from ( unit . appliesToKeys ) . filter ( ( key : string ) = > seenKeys . has ( key ) ) ;
2021-06-22 00:29:07 +02:00
if ( alreadySeen . length > 0 ) {
throw ` ${ context } .units: multiple units define the same keys. The key(s) ${ alreadySeen . join ( "," ) } occur multiple times `
}
unit . appliesToKeys . forEach ( key = > seenKeys . add ( key ) )
}
2021-06-22 03:16:45 +02:00
return result ;
2021-06-22 00:29:07 +02:00
}
2021-06-22 03:16:45 +02:00
2020-11-11 16:23:49 +01:00
}
2021-03-24 01:25:57 +01:00
public CustomCodeSnippets ( ) : string [ ] {
2021-04-17 11:37:22 +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
public ExtractImages ( ) : Set < string > {
2021-04-09 02:57:06 +02:00
const icons = new Set < string > ( )
for ( const layer of this . layers ) {
layer . ExtractImages ( ) . forEach ( icons . add , icons )
}
icons . add ( this . icon )
icons . add ( this . socialImage )
return icons
}
2021-05-17 00:18:21 +02:00
public LayerIndex ( ) : Map < string , LayerConfig > {
2021-04-25 13:23:27 +02:00
const index = new Map < string , LayerConfig > ( ) ;
for ( const layer of this . layers ) {
index . set ( layer . id , layer )
}
return index ;
}
2021-04-17 11:37:22 +02:00
/ * *
* Replaces all the relative image - urls with a fixed image url
* This is to fix loading from external sources
*
* It should be passed the location where the original theme file is hosted .
*
* If no images are rewritten , the same object is returned , otherwise a new copy is returned
* /
public patchImages ( originalURL : string , originalJson : string ) : LayoutConfig {
const allImages = Array . from ( this . ExtractImages ( ) )
const rewriting = new Map < string , string > ( )
// Needed for absolute urls: note that it doesn't contain a trailing slash
const origin = new URL ( originalURL ) . origin
let path = new URL ( originalURL ) . href
path = path . substring ( 0 , path . lastIndexOf ( "/" ) )
for ( const image of allImages ) {
2021-05-17 00:18:21 +02:00
if ( image == "" || image == undefined ) {
2021-04-17 11:37:22 +02:00
continue
}
if ( image . startsWith ( "http://" ) || image . startsWith ( "https://" ) ) {
continue
}
if ( image . startsWith ( "/" ) ) {
// This is an absolute path
rewriting . set ( image , origin + image )
} else if ( image . startsWith ( "./assets/themes" ) ) {
// Legacy workaround
rewriting . set ( image , path + image . substring ( image . lastIndexOf ( "/" ) ) )
} else if ( image . startsWith ( "./" ) ) {
// This is a relative url
rewriting . set ( image , path + image . substring ( 1 ) )
} else {
// This is a relative URL with only the path
rewriting . set ( image , path + image )
}
}
if ( rewriting . size == 0 ) {
return this ;
}
rewriting . forEach ( ( value , key ) = > {
2021-05-17 00:18:21 +02:00
console . log ( "Rewriting" , key , "==>" , value )
2021-04-17 11:37:22 +02:00
originalJson = originalJson . replace ( new RegExp ( key , "g" ) , value )
} )
return new LayoutConfig ( JSON . parse ( originalJson ) , false , "Layout rewriting" )
}
2020-11-11 16:23:49 +01:00
}