import "@here/harp-fetch" ; import { isJsonExpr } from "@here/harp-datasource-protocol" ; import { Definitions , FlatTheme , getDefinitionValue , ImageTexture , isJsonExprReference , Style , Styles , StyleSet , Theme } from "@here/harp-datasource-protocol/lib/Theme" ; import { composeUriResolvers , ContextLogger , getAppBaseUrl , getOptionValue , IContextLogger , ISimpleChannel , RelativeUriResolver , UriResolver } from "@here/harp-utils" ; import { SKY_CUBEMAP_FACE_COUNT , SkyCubemapFaceId } from "./SkyCubemapTexture" ; internal export const DEFAULT_MAX_THEME_INTHERITANCE_DEPTH = 4 ; /**
 * Options to customize  {@link @here/harp-datasource-protocol#Theme} loading process.
 *
 *  see ThemeLoader . load export interface ThemeLoadOptions { default resolveDefinitions ? : boolean ; default resolveResourceUris ? : boolean ; default resolveIncludeUris ? : boolean ; see https see https signal ? : AbortSignal ; /**
     * Maximum recursion depth when resolving base themes
     * through [ {@link @here/harp-datasource-protocol#Theme} s `extends` property.
     *
     *  default maxInheritanceDepth ? : number ; /**
     * Custom logging channel on which diagnostics and warnings will be reported.
     *
     * If not specified,  ThemeLoader . load will log to `console`.
      logger ? : ISimpleChannel ; /**
     * Resolve asset `URI`s referenced in `Theme` assets using this resolver.
     */ uriResolver ? : UriResolver ; } /**
 * Loads and validates a theme from URL objects.
 */ export class ThemeLoader { /**
     * Loads a  {@link @here/harp-datasource-protocol#Theme} from a
     * remote resource, provided as a URL that points to a
     * JSON-encoded theme.
     *
     * By default, resolves following features of theme:
     *
     *  -  `extends` - loads and merges all inherited themes (see [[resolveBaseTheme]])
     *  -  `ref` - resolves all `ref` instances to their values defined in `definitions` section
     *     of theme (see [[resolveThemeReferences]])
     *
     * Relative URIs of reference resources are resolved to full URL using the document's base URL
     * (see [[resolveUrls]]).
     *
     * Custom URIs (of theme itself and of resources referenced by theme) may be resolved with by
     * providing  {@link @here/harp-utils#UriResolver} using  ThemeLoadOptions . uriResolver * option.
     *
     *  param theme -  {@link @here/harp-datasource-protocol#Theme} instance or theme URL
     *                to the theme.
     *  param options - Optional, a  ThemeLoadOptions objects
     *                  containing any custom settings for
     *                  this load request.
      static async load ( theme , options ? ) { options = options ?? { } ; if ( typeof theme === "string" ) { const uriResolver = options . uriResolver ; const themeUrl = uriResolver !== undefined ? uriResolver . resolveUri ( theme ) : theme ; const response = await fetch ( themeUrl , { signal : options . signal } ) ; if ( ! response . ok ) { throw new Error ( `ThemeLoader#load: cannot load theme: ${ response . statusText }` ) ; } theme = ( await response . json ( ) ) as Theme ; theme . url = themeUrl ; theme = this . resolveUrls ( theme , options ) ; } else if ( theme . url === undefined ) { theme . url = getAppBaseUrl ( ) ; theme = this . resolveUrls ( theme , options ) ; } else { theme = this . convertFlatTheme ( theme ) ; } if ( theme === null || theme === undefined ) { throw new Error ( "ThemeLoader#load: loaded resource is not valid JSON" ) ; } ThemeLoader . checkTechniqueSupport ( theme ) ; const resolveDefinitions = getOptionValue < boolean > ( options . resolveDefinitions , false ) ; theme = await ThemeLoader . resolveBaseThemes ( theme , options ) ; if ( resolveDefinitions ) { const contextLoader = new ContextLogger ( options . logger ?? console , `when processing Theme ${ theme . url }:` ) ; ThemeLoader . resolveThemeReferences ( theme , contextLoader ) ; } return theme ; } param theme static isThemeLoaded ( theme ) { return theme . extends === undefined && ! Array . isArray ( theme . styles ) ; } deprecated Please use `ThemeLoader.load`
     *
     * Loads a  {@link @here/harp-datasource-protocol#Theme} from a remote resource,
     * provided as a URL that points to a JSON-encoded
     * theme.
     *
     *  param themeUrl static async loadAsync ( themeUrl ) { return await ThemeLoader . load ( themeUrl ) ; } /**
     * Resolves all  {@link @here/harp-datasource-protocol#Theme} 's relatives URLs
     * to full URL using the  {@link @here/harp-datasource-protocol#Theme} 's URL
     * (see: https://www.w3.org/TR/WD-html40-970917/htmlweb.html#h-5.1.2).
     *
     * This method mutates original `theme` instance.
     *
     *  param theme - The  {@link @here/harp-datasource-protocol#Theme} to resolve.
      private static resolveUrls ( theme , options ? ) { theme = ThemeLoader . convertFlatTheme ( theme ) ; if ( theme . url === undefined ) { return theme ; } const childUrlResolver = composeUriResolvers ( options ?. uriResolver , new RelativeUriResolver ( theme . url ) ) ; const resolveIncludes = options === undefined || ! ( options . resolveIncludeUris === false ) ; if ( theme . extends && resolveIncludes ) { theme . extends = ( Array . isArray ( theme . extends ) ? theme . extends : [ theme . extends ] ) . map ( baseTheme => { if ( typeof baseTheme === "string" ) { return childUrlResolver . resolveUri ( baseTheme ) ; } else { if ( baseTheme . url !== undefined ) { return baseTheme ; } else { baseTheme . url = theme . url ; return this . resolveUrls ( baseTheme , options ) ; } } } ) ; } if ( ! ThemeLoader . convertFlatTheme ( theme ) ) { return theme ; } const resolveResources = options === undefined || ! ( options . resolveResourceUris === false ) ; if ( resolveResources ) { ThemeLoader . resolveResources ( theme , childUrlResolver ) ; } return theme ; } private static checkTechniqueSupport ( theme ) { if ( theme . styles !== undefined ) { for ( const styleSetName in theme . styles ) { if ( ! theme . styles . hasOwnProperty ( styleSetName ) ) { continue ; } for ( const style of theme . styles [ styleSetName ] ) { switch ( ( style as any ) . technique ) { default : break ; } } } } } /**
     * Expand all `ref` expressions in  {@link @here/harp-datasource-protocol#Theme} * basing on `definitions`.
     *
     *  remarks private static resolveThemeReferences ( theme , contextLogger ) { if ( theme . styles !== undefined ) { for ( const styleSetName in theme . styles ) { if ( ! theme . styles . hasOwnProperty ( styleSetName ) ) { continue ; } contextLogger . pushAttr ( "styles" ) ; contextLogger . pushAttr ( styleSetName ) ; theme . styles [ styleSetName ] = ThemeLoader . resolveStyleSet ( theme . styles [ styleSetName ] , theme . definitions , contextLogger ) ; contextLogger . pop ( ) ; contextLogger . pop ( ) ; } } return theme ; } /**
     * Expand all `ref` in [[StyleSet]] basing on `definitions`.
     */ private static resolveStyleSet ( styleSet , definitions , contextLogger ) { const result = [ ] ; for ( let index = 0 ; index < styleSet . length ; ++ index ) { const currentStyle = styleSet [ index ] ; contextLogger . pushIndex ( index ) ; const resolvedStyle = ThemeLoader . resolveStyle ( currentStyle , definitions , contextLogger ) ; if ( resolvedStyle !== undefined ) { result . push ( resolvedStyle ) ; } else { contextLogger . warn ( "invalid style, ignored" ) ; } contextLogger . pop ( ) ; } return result ; } /**
     * Expand all `ref` in [[Style]] instance basing on `definitions`.
     */ private static resolveStyle ( style , definitions , contextLogger ) { if ( Array . isArray ( style . when ) ) { contextLogger . pushAttr ( "when" ) ; const resolvedWhen = this . resolveExpressionReferences ( style . when , definitions , contextLogger ) ; contextLogger . pop ( ) ; if ( resolvedWhen === undefined ) { return undefined ; } style . when = resolvedWhen ; } if ( style . attr !== undefined ) { const attr = style . attr as any ; contextLogger . pushAttr ( "attr" ) ; for ( const prop in attr ) { if ( ! attr . hasOwnProperty ( prop ) ) { continue ; } const value = attr [ prop ] ; if ( ! Array . isArray ( value ) ) { continue ; } contextLogger . pushAttr ( prop ) ; const resolvedValue = this . resolveExpressionReferences ( value , definitions , contextLogger ) ; contextLogger . pop ( ) ; if ( resolvedValue !== undefined ) { attr [ prop ] = resolvedValue ; } else { delete attr [ prop ] ; } } contextLogger . pop ( ) ; } return style ; } /**
     * Resolve `[ref, ...]` in expressions.
     *
     * Returns `undefined` some reference was invalid (missing or wrong type).
     */ private static resolveExpressionReferences < T > ( value , definitions , contextLogger ) { let failed = false ; function resolveInternal ( node ) { if ( isJsonExprReference ( node ) ) { const defName = node [ 1 ] ; const def = definitions && definitions [ defName ] ; if ( def === undefined ) { contextLogger . warn ( `invalid reference '${ defName }' - not found` ) ; failed = true ; return undefined ; } if ( isJsonExpr ( def ) ) { return def ; } return getDefinitionValue ( def ) ; } else if ( Array . isArray ( node ) ) { const result = [ ... node ] ; for ( let i = 1 ; i < result . length ; ++ i ) { result [ i ] = resolveInternal ( result [ i ] ) ; } return result ; } else { return node ; } } const r = resolveInternal ( value ) ; if ( failed ) { return undefined ; } return r ; } /**
     * Realize `extends` clause by merging `theme` with
     * its base  {@link @here/harp-datasource-protocol#Theme} .
     *
     *  param theme -  {@link @here/harp-datasource-protocol#Theme} object
     *  param options - Optional, a  ThemeLoadOptions objects
     *                  containing any custom settings for
     *                  this load request.
      private static async resolveBaseThemes ( theme , options ? ) { options = options ?? { } ; if ( theme . extends === undefined ) { return theme ; } const maxInheritanceDepth = getOptionValue ( options . maxInheritanceDepth , DEFAULT_MAX_THEME_INTHERITANCE_DEPTH ) ; if ( maxInheritanceDepth <= 0 ) { throw new Error ( `maxInheritanceDepth reached when attempting to load base theme` ) ; } const baseThemes = ! Array . isArray ( theme . extends ) ? [ theme . extends ] : theme . extends ; delete theme . extends ; let baseThemesMerged = { } ; for ( const baseTheme of baseThemes ) { const actualBaseTheme = await ThemeLoader . load ( baseTheme , { ... options , resolveDefinitions : false , maxInheritanceDepth : maxInheritanceDepth - 1 } ) ; baseThemesMerged = ThemeLoader . mergeThemes ( actualBaseTheme , baseThemesMerged ) ; } return ThemeLoader . mergeThemes ( theme , baseThemesMerged ) ; } private static mergeThemes ( theme , baseTheme ) { const definitions = { ... baseTheme . definitions , ... theme . definitions } ; let styles ! ; if ( baseTheme . styles && theme . styles ) { const currentStyleSets = Object . keys ( baseTheme . styles ) ; const incomingStyleSets = Object . keys ( theme . styles ) ; styles = { } ; currentStyleSets . forEach ( styleSetName => { const index = incomingStyleSets . indexOf ( styleSetName ) ; if ( index !== - 1 ) { const baseStyleSet = baseTheme . styles ! [ styleSetName ] ; const newStyleSet = [ ] ; const styleIdMap = new Map < string , number > ( ) ; baseStyleSet . forEach ( style => { if ( typeof style . id === "string" ) { styleIdMap . set ( style . id , newStyleSet . length ) ; } newStyleSet . push ( style ) ; } ) ; const incomingStyleSet = theme . styles ! [ styleSetName ] ; incomingStyleSet . forEach ( style => { if ( typeof style . extends === "string" && styleIdMap . has ( style . extends ) ) { const baseStyleIndex = styleIdMap . get ( style . extends ) ! ; const baseStyle = newStyleSet [ baseStyleIndex ] ; newStyleSet [ baseStyleIndex ] = { ... baseStyle , ... style } as any ; newStyleSet [ baseStyleIndex ] . extends = undefined ; return ; } if ( typeof style . id === "string" && styleIdMap . has ( style . id ) ) { const styleIndex = styleIdMap . get ( style . id ) ! ; newStyleSet [ styleIndex ] = style ; return ; } newStyleSet . push ( style ) ; } ) ; styles [ styleSetName ] = newStyleSet ; incomingStyleSets . splice ( index , 1 ) ; } else { styles [ styleSetName ] = baseTheme . styles ! [ styleSetName ] ; } } ) ; incomingStyleSets . forEach ( p => { styles [ p ] = theme . styles ! [ p ] ; } ) ; } else if ( baseTheme . styles ) { styles = { ... baseTheme . styles } ; } else if ( theme . styles ) { styles = { ... theme . styles } ; } return { ... baseTheme , ... theme , ... ThemeLoader . mergeImageTextures ( theme , baseTheme ) , definitions , styles } ; } private static mergeImageTextures ( theme , baseTheme ) { const images = { ... baseTheme . images , ... theme . images } ; let imageTextures = [ ] ; if ( ! baseTheme . imageTextures && theme . imageTextures ) { imageTextures = theme . imageTextures ; } else if ( baseTheme . imageTextures && ! theme . imageTextures ) { imageTextures = baseTheme . imageTextures ; } else if ( baseTheme . imageTextures && theme . imageTextures ) { imageTextures = theme . imageTextures . slice ( ) ; baseTheme . imageTextures . forEach ( val => { if ( ! imageTextures . find ( ( { name } ) => name === val . name ) ) { imageTextures . push ( val ) ; } } ) ; } return { images , imageTextures } ; } private static convertFlatTheme ( theme ) { if ( Array . isArray ( theme . styles ) ) { const styles = { } ; theme . styles . forEach ( style => { if ( isJsonExpr ( style ) ) { throw new Error ( "invalid usage of theme reference" ) ; } const styleSetName = style . styleSet ; if ( styleSetName === undefined ) { throw new Error ( "missing reference to style set" ) ; } if ( ! styles [ styleSetName ] ) { styles [ styleSetName ] = [ ] ; } styles [ styleSetName ] . push ( style ) ; } ) ; theme . styles = styles ; } return theme as Theme ; } private static resolveResources ( theme , childUrlResolver ) { if ( theme . sky && theme . sky . type === "cubemap" ) { for ( let i = 0 ; i < SKY_CUBEMAP_FACE_COUNT ; ++ i ) { const faceUrl = ( theme . sky as any ) [ SkyCubemapFaceId [ i ] ] ; if ( faceUrl !== undefined ) { ( theme . sky as any ) [ SkyCubemapFaceId [ i ] ] = childUrlResolver . resolveUri ( faceUrl ) ; } } } if ( theme . images ) { for ( const name of Object . keys ( theme . images ) ) { const image = theme . images [ name ] ; image . url = childUrlResolver . resolveUri ( image . url ) ; if ( image . atlas !== undefined ) { image . atlas = childUrlResolver . resolveUri ( image . atlas ) ; } } } if ( theme . fontCatalogs ) { for ( const font of theme . fontCatalogs ) { font . url = childUrlResolver . resolveUri ( font . url ) ; } } if ( theme . poiTables ) { for ( const poiTable of theme . poiTables ) { poiTable . url = childUrlResolver . resolveUri ( poiTable . url ) ; } } if ( theme . styles !== undefined ) { for ( const styleSetName in theme . styles ) { if ( ! theme . styles . hasOwnProperty ( styleSetName ) ) { continue ; } const styleSet = theme . styles [ styleSetName ] as Style [ ] ; for ( const style of styleSet ) { if ( ! style . attr ) { continue ; } [ "map" , "normalMap" , "displacementMap" , "roughnessMap" ] . forEach ( texturePropertyName => { const textureProperty = ( style . attr ! as any ) [ texturePropertyName ] ; if ( textureProperty && typeof textureProperty === "string" ) { ( style . attr ! as any ) [ texturePropertyName ] = childUrlResolver . resolveUri ( textureProperty ) ; } } ) ; } } } } }