2020-11-11 16:23:49 +01:00
import { Translation } from "../../UI/i18n/Translation" ;
2021-08-07 23:11:34 +02:00
import { LayoutConfigJson } from "./Json/LayoutConfigJson" ;
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" ;
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" ;
2021-10-14 21:43:14 +02:00
import TilesourceConfig from "./TilesourceConfig" ;
2021-12-05 02:06:14 +01:00
import DependencyCalculator from "./DependencyCalculator" ;
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 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 defaultBackgroundId? : string ;
2021-05-17 00:18:21 +02:00
public layers : LayerConfig [ ] ;
2021-10-14 21:43:14 +02:00
public tileLayerSources : TilesourceConfig [ ]
2021-01-06 02:52:38 +01:00
public readonly clustering ? : {
2021-01-05 00:21:00 +01:00
maxZoom : number ,
2021-10-13 00:43:19 +02:00
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 ;
2021-10-15 05:20:02 +02:00
public readonly enableIframePopout : boolean ;
2021-07-28 02:51:07 +02:00
2020-11-11 16:23:49 +01:00
public readonly customCss? : string ;
2021-10-25 20:38:57 +02:00
2021-09-29 16:55:05 +02:00
public readonly overpassUrl : string [ ] ;
2021-08-23 15:48:42 +02:00
public readonly 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 ;
2021-11-07 16:34:51 +01:00
public readonly trackAllNodes : boolean ;
2021-04-17 11:37:22 +02:00
constructor ( json : LayoutConfigJson , official = true , context? : string ) {
2021-09-18 02:32:40 +02:00
this . official = official ;
2020-11-11 16:23:49 +01:00
this . id = json . id ;
2021-12-30 22:36:34 +01:00
if ( json . id . toLowerCase ( ) !== json . id ) {
throw "The id of a theme should be lowercase: " + json . id
}
if ( json . id . match ( /[a-z0-9-_]/ ) == null ) {
throw "The id of a theme should match [a-z0-9-_]*: " + 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 . version = json . version ;
this . language = [ ] ;
2021-11-07 16:34:51 +01:00
2020-11-11 16:23:49 +01:00
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-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-10-01 04:49:19 +02:00
this . descriptionTail = json . descriptionTail === undefined ? undefined : 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 ;
2021-11-07 16:34:51 +01:00
if ( json . widenFactor <= 0 ) {
throw "Widenfactor too small, shoud be > 0"
2021-10-08 12:09:53 +02:00
}
2021-11-07 16:34:51 +01:00
if ( json . widenFactor > 20 ) {
throw "Widenfactor is very big, use a value between 1 and 5 (current value is " + json . widenFactor + ") at " + context
2021-09-26 17:36:39 +02:00
}
2021-11-07 16:34:51 +01:00
2021-09-22 16:07:56 +02:00
this . widenFactor = json . widenFactor ? ? 1.5 ;
2021-11-07 16:34:51 +01:00
2020-11-11 16:23:49 +01:00
this . defaultBackgroundId = json . defaultBackgroundId ;
2021-11-07 16:34:51 +01:00
this . tileLayerSources = ( json . tileLayerSources ? ? [ ] ) . map ( ( config , i ) = > new TilesourceConfig ( config , ` ${ this . id } .tileLayerSources[ ${ i } ] ` ) )
const layerInfo = LayoutConfig . ExtractLayers ( json , official , context ) ;
2021-10-31 02:08:39 +01:00
this . layers = layerInfo . layers
2021-11-07 16:34:51 +01:00
2021-06-15 01:24:04 +02:00
this . clustering = {
2021-01-05 00:21:00 +01:00
maxZoom : 16 ,
2021-10-13 00:43:19 +02:00
minNeededElements : 25 ,
2021-01-05 00:21:00 +01:00
} ;
2021-11-07 16:34:51 +01:00
if ( json . clustering === false ) {
2021-10-13 01:28:46 +02:00
this . clustering = {
maxZoom : 0 ,
minNeededElements : 100000 ,
} ;
2021-11-07 16:34:51 +01:00
} else 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-10-13 00:43:19 +02:00
minNeededElements : json.clustering.minNeededElements ? ? 25 ,
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-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 ;
2021-07-28 02:51:07 +02:00
this . enableExportButton = json . enableDownload ? ? false ;
this . enablePdfDownload = json . enablePdfDownload ? ? false ;
2021-10-15 05:20:02 +02:00
this . enableIframePopout = json . enableIframePopout ? ? true
2020-11-11 16:23:49 +01:00
this . customCss = json . customCss ;
2021-09-29 16:55:05 +02:00
this . overpassUrl = Constants . defaultOverpassUrls
2021-11-07 16:34:51 +01:00
if ( json . overpassUrl !== undefined ) {
if ( typeof json . overpassUrl === "string" ) {
2021-09-29 16:55:05 +02:00
this . overpassUrl = [ json . overpassUrl ]
2021-11-07 16:34:51 +01:00
} else {
2021-09-29 16:55:05 +02:00
this . overpassUrl = json . overpassUrl
}
}
2021-08-23 15:48:42 +02:00
this . overpassTimeout = json . overpassTimeout ? ? 30
2021-10-15 05:20:02 +02:00
this . overpassMaxZoom = json . overpassMaxZoom ? ? 17
this . osmApiTileSize = json . osmApiTileSize ? ? this . overpassMaxZoom + 1
2021-06-22 00:29:07 +02:00
2021-06-22 03:16:45 +02:00
}
2021-11-07 16:34:51 +01:00
private static ExtractLayers ( json : LayoutConfigJson , official : boolean , context : string ) : { layers : LayerConfig [ ] , extractAllNodes : boolean } {
2021-07-07 18:07:29 +02:00
const result : LayerConfig [ ] = [ ]
2021-10-31 02:08:39 +01:00
let exportAllNodes = false
2021-12-19 02:11:22 +01:00
if ( json . layers === undefined ) {
throw "Got undefined layers for " + json . id + " at " + context
}
2021-07-07 18:07:29 +02:00
json . layers . forEach ( ( layer , i ) = > {
2021-11-07 16:34:51 +01:00
2021-07-07 18:07:29 +02:00
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 ) {
2021-11-08 02:36:01 +01:00
let lyr = JSON . parse ( JSON . stringify ( AllKnownLayers . sharedLayersJson . get ( layer ) ) ) ;
2021-09-13 01:21:47 +02:00
const newLayer = new LayerConfig ( Utils . Merge ( json . overrideAll , lyr ) , ` ${ json . id } +overrideAll.layers[ ${ i } ] ` , official )
2021-07-07 18:07:29 +02:00
result . push ( newLayer )
2021-07-07 18:12:07 +02:00
return
2021-07-07 18:07:29 +02:00
} else {
2021-11-08 02:36:01 +01:00
const shared = AllKnownLayers . sharedLayers . get ( layer )
2021-12-04 21:49:17 +01:00
if ( shared === undefined ) {
2021-11-08 02:36:01 +01:00
throw ` Shared layer ${ layer } not found (at ${ context } .layers[ ${ i } ]) `
}
result . push ( shared )
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
2021-11-08 02:36:01 +01:00
result . push ( new LayerConfig ( layer , ` ${ json . id } .layers[ ${ i } ] ` , official ) )
2021-07-07 18:12:07 +02:00
return
2021-07-07 18:07:29 +02:00
}
2021-11-07 16:34:51 +01:00
2021-07-07 18:07:29 +02:00
// @ts-ignore
let names = layer . builtin ;
if ( typeof names === "string" ) {
names = [ names ]
}
2021-12-19 02:11:22 +01:00
// This is a very special layer which triggers special behaviour
exportAllNodes = names . some ( name = > name === "type_node" ) ;
2021-07-07 18:07:29 +02:00
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
}
2021-09-07 00:23:00 +02:00
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
2021-07-07 18:12:07 +02:00
if ( json . overrideAll !== undefined ) {
newLayer = Utils . Merge ( json . overrideAll , newLayer ) ;
}
2021-11-08 02:36:01 +01:00
result . push ( new LayerConfig ( newLayer , ` ${ json . id } .layers[ ${ i } ] ` , official ) )
2021-07-07 18:12:07 +02:00
return
} )
2021-07-07 18:07:29 +02:00
} ) ;
2021-12-04 21:49:17 +01:00
2021-11-08 02:36:01 +01:00
// Some special layers which are always included by default
for ( const defaultLayer of AllKnownLayers . added_by_default ) {
2021-12-04 21:49:17 +01:00
if ( result . some ( l = > l ? . id === defaultLayer ) ) {
2021-11-08 02:36:01 +01:00
continue ; // Already added
}
2021-12-04 21:49:17 +01:00
const sharedLayer = AllKnownLayers . sharedLayers . get ( defaultLayer )
if ( sharedLayer !== undefined ) {
result . push ( sharedLayer )
2021-12-05 02:06:14 +01:00
} else if ( ! AllKnownLayers . runningGenerateScript ) {
throw "SharedLayer " + defaultLayer + " not found"
2021-12-04 21:49:17 +01:00
}
2021-11-08 02:36:01 +01:00
}
2021-07-07 18:07:29 +02:00
2021-12-05 02:06:14 +01:00
if ( AllKnownLayers . runningGenerateScript ) {
return { layers : result , extractAllNodes : exportAllNodes }
}
// Verify cross-dependencies
let unmetDependencies : { neededLayer : string , neededBy : string , reason : string , context? : string } [ ] = [ ]
2021-12-04 21:49:17 +01:00
do {
2021-12-05 02:06:14 +01:00
const dependencies : { neededLayer : string , reason : string , context? : string , neededBy : string } [ ] = [ ]
for ( const layerConfig of result ) {
const layerDeps = DependencyCalculator . getLayerDependencies ( layerConfig )
dependencies . push ( . . . layerDeps )
}
2021-12-04 21:49:17 +01:00
const loadedLayers = new Set ( result . map ( r = > r . id ) )
2021-12-05 02:06:14 +01:00
// During the generate script, builtin layers are verified but not loaded - so we have to add them manually here
// Their existance is checked elsewhere, so this is fine
unmetDependencies = dependencies . filter ( dep = > ! loadedLayers . has ( dep . neededLayer ) )
2021-12-04 21:49:17 +01:00
for ( const unmetDependency of unmetDependencies ) {
2021-12-05 02:06:14 +01:00
const dep = AllKnownLayers . sharedLayers . get ( unmetDependency . neededLayer )
2021-12-04 21:49:17 +01:00
if ( dep === undefined ) {
2021-12-05 02:06:14 +01:00
const message =
[ "Loading a dependency failed: layer " + unmetDependency . neededLayer + " is not found, neither as layer of " + json . id + " nor as builtin layer." ,
"This layer is needed by " + unmetDependency . neededBy ,
unmetDependency . reason + " (at " + unmetDependency . context + ")" ,
"Loaded layers are: " + result . map ( l = > l . id ) . join ( "," )
]
throw message . join ( "\n\t" ) ;
2021-12-04 21:49:17 +01:00
}
result . unshift ( dep )
2021-12-05 02:06:14 +01:00
unmetDependencies = unmetDependencies . filter ( d = > d . neededLayer !== unmetDependency . neededLayer )
2021-12-04 21:49:17 +01:00
}
} while ( unmetDependencies . length > 0 )
2021-12-05 02:06:14 +01:00
2021-10-31 02:08:39 +01:00
return { layers : result , extractAllNodes : exportAllNodes }
2021-07-07 18:07:29 +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
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
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" )
}
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
public getMatchingLayer ( tags : any ) : LayerConfig | undefined {
if ( tags === undefined ) {
2021-11-08 20:49:51 +01:00
return undefined
}
for ( const layer of this . layers ) {
if ( layer . source . osmTags . matchesProperties ( tags ) ) {
return layer
}
}
return undefined
}
2021-04-17 11:37:22 +02:00
2020-11-11 16:23:49 +01:00
}