@here/harp-mapview
Version:
Functionality needed to render a map.
465 lines • 19.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ThemeLoader = exports.DEFAULT_MAX_THEME_INTHERITANCE_DEPTH = void 0;
/*
* Copyright (C) 2019-2021 HERE Europe B.V.
* Licensed under Apache 2.0, see full license in LICENSE
* SPDX-License-Identifier: Apache-2.0
*/
require("@here/harp-fetch");
const harp_datasource_protocol_1 = require("@here/harp-datasource-protocol");
const Theme_1 = require("@here/harp-datasource-protocol/lib/Theme");
const harp_utils_1 = require("@here/harp-utils");
const SkyCubemapTexture_1 = require("./SkyCubemapTexture");
/**
* @internal
*/
exports.DEFAULT_MAX_THEME_INTHERITANCE_DEPTH = 4;
/**
* Loads and validates a theme from URL objects.
*/
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 {@link ThemeLoadOptions.uriResolver}
* option.
*
* @param theme - {@link @here/harp-datasource-protocol#Theme} instance or theme URL
* to the theme.
* @param options - Optional, a {@link ThemeLoadOptions} objects
* containing any custom settings for
* this load request.
*/
static async load(theme, options) {
var _a;
options = options !== null && options !== void 0 ? 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());
theme.url = themeUrl;
theme = this.resolveUrls(theme, options);
}
else if (theme.url === undefined) {
// assume that theme url is same as baseUrl
theme.url = harp_utils_1.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 = harp_utils_1.getOptionValue(options.resolveDefinitions, false);
theme = await ThemeLoader.resolveBaseThemes(theme, options);
if (resolveDefinitions) {
const contextLoader = new harp_utils_1.ContextLogger((_a = options.logger) !== null && _a !== void 0 ? _a : console, `when processing Theme ${theme.url}:`);
ThemeLoader.resolveThemeReferences(theme, contextLoader);
}
return theme;
}
/**
* Checks if `theme` instance is completely loaded, meaning that `extends` property is resolved.
*
* @param theme -
*/
static isThemeLoaded(theme) {
// TODO: Remove array check, when FlatTheme is fully supported
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 - The URL to the theme.
*
*/
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.
*/
static resolveUrls(theme, options) {
// Ensure that all resources referenced in theme by relative URIs are in fact relative to
// theme.
theme = ThemeLoader.convertFlatTheme(theme);
if (theme.url === undefined) {
return theme;
}
const childUrlResolver = harp_utils_1.composeUriResolvers(options === null || options === void 0 ? void 0 : options.uriResolver, new harp_utils_1.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;
}
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.technique) {
// TODO: Re-enable this once "dashed-line" is deprecated.
/* case "dashed-line":
console.warn(
`Using deprecated "dashed-line" technique.
Use "solid-line" technique instead`
); */
default:
break;
}
}
}
}
}
/**
* Expand all `ref` expressions in {@link @here/harp-datasource-protocol#Theme}
* basing on `definitions`.
*
* @remarks
* This method mutates original `theme` instance.
*/
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`.
*/
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`.
*/
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;
contextLogger.pushAttr("attr");
for (const prop in attr) {
if (!attr.hasOwnProperty(prop)) {
continue;
}
const value = attr[prop];
if (!Array.isArray(value)) {
continue; // nothing to do
}
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).
*/
static resolveExpressionReferences(value, definitions, contextLogger) {
let failed = false;
function resolveInternal(node) {
if (Theme_1.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 (harp_datasource_protocol_1.isJsonExpr(def)) {
return def;
}
return Theme_1.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 {@link ThemeLoadOptions} objects
* containing any custom settings for
* this load request.
*/
static async resolveBaseThemes(theme, options) {
options = options !== null && options !== void 0 ? options : {};
if (theme.extends === undefined) {
return theme;
}
const maxInheritanceDepth = harp_utils_1.getOptionValue(options.maxInheritanceDepth, exports.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, Object.assign(Object.assign({}, options), { resolveDefinitions: false, maxInheritanceDepth: maxInheritanceDepth - 1 }));
baseThemesMerged = ThemeLoader.mergeThemes(actualBaseTheme, baseThemesMerged);
}
return ThemeLoader.mergeThemes(theme, baseThemesMerged);
}
static mergeThemes(theme, baseTheme) {
const definitions = Object.assign(Object.assign({}, 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) {
// merge the current and incoming styleset
// and add the result to `styles`.
const baseStyleSet = baseTheme.styles[styleSetName];
const newStyleSet = [];
const styleIdMap = new Map();
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)) {
// extends the existing style referenced by `style.extends`.
const baseStyleIndex = styleIdMap.get(style.extends);
const baseStyle = newStyleSet[baseStyleIndex];
newStyleSet[baseStyleIndex] = Object.assign(Object.assign({}, baseStyle), style);
newStyleSet[baseStyleIndex].extends = undefined;
return;
}
if (typeof style.id === "string" && styleIdMap.has(style.id)) {
// overrides the existing style with `id` equals to `style.id`.
const styleIndex = styleIdMap.get(style.id);
newStyleSet[styleIndex] = style;
return;
}
newStyleSet.push(style);
});
styles[styleSetName] = newStyleSet;
// remove the styleset from the incoming list
incomingStyleSets.splice(index, 1);
}
else {
// copy the existing style set to `styles`.
styles[styleSetName] = baseTheme.styles[styleSetName];
}
});
// add the remaining stylesets to styles.
incomingStyleSets.forEach(p => {
styles[p] = theme.styles[p];
});
}
else if (baseTheme.styles) {
styles = Object.assign({}, baseTheme.styles);
}
else if (theme.styles) {
styles = Object.assign({}, theme.styles);
}
return Object.assign(Object.assign(Object.assign(Object.assign({}, baseTheme), theme), ThemeLoader.mergeImageTextures(theme, baseTheme)), { definitions,
styles });
}
static mergeImageTextures(theme, baseTheme) {
const images = Object.assign(Object.assign({}, 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
};
}
static convertFlatTheme(theme) {
if (Array.isArray(theme.styles)) {
// Convert the flat theme to a standard theme.
const styles = {};
theme.styles.forEach(style => {
if (harp_datasource_protocol_1.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;
}
static resolveResources(theme, childUrlResolver) {
if (theme.sky && theme.sky.type === "cubemap") {
for (let i = 0; i < SkyCubemapTexture_1.SKY_CUBEMAP_FACE_COUNT; ++i) {
const faceUrl = theme.sky[SkyCubemapTexture_1.SkyCubemapFaceId[i]];
if (faceUrl !== undefined) {
theme.sky[SkyCubemapTexture_1.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];
for (const style of styleSet) {
if (!style.attr) {
continue;
}
["map", "normalMap", "displacementMap", "roughnessMap"].forEach(texturePropertyName => {
const textureProperty = style.attr[texturePropertyName];
if (textureProperty && typeof textureProperty === "string") {
style.attr[texturePropertyName] = childUrlResolver.resolveUri(textureProperty);
}
});
}
}
}
}
}
exports.ThemeLoader = ThemeLoader;
//# sourceMappingURL=ThemeLoader.js.map