UNPKG

@elastic/ems-client

Version:

JavaScript client library for the Elastic Maps Service

472 lines (435 loc) 14.9 kB
/* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ import _ from 'lodash'; import { DataDrivenPropertyValueSpecification, FormattedSpecification, LayerSpecification, StyleSpecification, VectorSourceSpecification, } from 'maplibre-gl'; import { EMSClient, EmsTmsFormat, TMSServiceConfig } from './ems_client'; import { AbstractEmsService } from './ems_service'; import { layerPaintProperty, colorizeColor, blendMode, mbColorDefinition } from './utils'; export type EmsSprite = { height: number; pixelRatio: number; width: number; x: number; y: number; sdf?: boolean; }; export type EmsSpriteSheet = { [spriteName: string]: EmsSprite; }; type EmsVectorSources = { [sourceName: string]: VectorSourceSpecification; }; type EmsVectorStyle = StyleSpecification & { sources: EmsVectorSources; }; type EmsRasterStyle = { tilejson: string; name: string; attribution: string; minzoom: number; maxzoom: number; bounds: number[]; format: string; type: string; tiles: string[]; center: number[]; }; export class TMSService extends AbstractEmsService { /* List of supported languages with labels and OMT codes extracted from https://openmaptiles.org/languages Keys are values for i18.locale from Kibana settings and OMT codes for the rest. */ public static SupportedLanguages = [ { key: 'ar', omt: 'ar' }, { key: 'az', omt: 'az' }, { key: 'be', omt: 'be' }, { key: 'bg', omt: 'bg' }, { key: 'br', omt: 'br' }, { key: 'bs', omt: 'bs' }, { key: 'ca', omt: 'ca' }, { key: 'cs', omt: 'cs' }, { key: 'cy', omt: 'cy' }, { key: 'da', omt: 'da' }, { key: 'de', omt: 'de' }, { key: 'el', omt: 'el' }, { key: 'en', omt: 'en' }, { key: 'eo', omt: 'eo' }, { key: 'es', omt: 'es' }, { key: 'et', omt: 'et' }, { key: 'fi', omt: 'fi' }, { key: 'fr-fr', omt: 'fr' }, { key: 'fy', omt: 'fy' }, { key: 'ga', omt: 'ga' }, { key: 'gd', omt: 'gd' }, { key: 'he', omt: 'he' }, { key: 'hi-in', omt: 'hi' }, { key: 'hr', omt: 'hr' }, { key: 'hu', omt: 'hu' }, { key: 'hy', omt: 'hy' }, { key: 'is', omt: 'is' }, { key: 'it', omt: 'it' }, { key: 'ja_kana', omt: 'ja_kana' }, { key: 'ja_rm', omt: 'ja_rm' }, { key: 'ja-jp', omt: 'ja' }, { key: 'ka', omt: 'ka' }, { key: 'kk', omt: 'kk' }, { key: 'kn', omt: 'kn' }, { key: 'ko_rm', omt: 'ko_rm' }, { key: 'ko', omt: 'ko' }, { key: 'la', omt: 'la' }, { key: 'lb', omt: 'lb' }, { key: 'lt', omt: 'lt' }, { key: 'lv', omt: 'lv' }, { key: 'mk', omt: 'mk' }, { key: 'mt', omt: 'mt' }, { key: 'nl', omt: 'nl' }, { key: 'no', omt: 'no' }, { key: 'pl', omt: 'pl' }, { key: 'pt-pt', omt: 'pt' }, { key: 'rm', omt: 'rm' }, { key: 'ro', omt: 'ro' }, { key: 'ru-ru', omt: 'ru' }, { key: 'sk', omt: 'sk' }, { key: 'sl', omt: 'sl' }, { key: 'sq', omt: 'sq' }, { key: 'sr-Ltn', omt: 'sr-Ltn' }, { key: 'sr', omt: 'sr' }, { key: 'sv', omt: 'sv' }, { key: 'th', omt: 'th' }, { key: 'tr', omt: 'tr' }, { key: 'uk', omt: 'uk' }, { key: 'zh-cn', omt: 'zh' }, ]; /* Suggested default operations for the different EMS styles */ public static colorOperationDefaults: { style: string; operation: blendMode; percentage: number; }[] = [ { style: 'road_map', operation: 'mix', percentage: 0.25 }, { style: 'road_map_desaturated', operation: 'screen', percentage: 0.25 }, { style: 'dark_map', operation: 'dodge', percentage: 0.25 }, { style: 'high_contrast', operation: 'screen', percentage: 0.25 }, { style: 'road_map_desaturated_v9', operation: 'screen', percentage: 0.25 }, { style: 'dark_map_v9', operation: 'dodge', percentage: 0.25 }, ]; protected readonly _config: TMSServiceConfig; private _getRasterStyleJson = _.once(async (): Promise<EmsRasterStyle | undefined> => { const rasterUrl = this._getStyleUrlForLocale('raster'); if (rasterUrl) { const url = this._proxyPath + this._getAbsoluteUrl(rasterUrl); return this._emsClient.getManifest<EmsRasterStyle>(this._emsClient.extendUrlWithParams(url)); } else { return; } }); private _getVectorStyleJsonRaw = _.once(async (): Promise<EmsVectorStyle | undefined> => { const vectorUrl = this._getStyleUrlForLocale('vector'); if (vectorUrl) { const url = this._proxyPath + this._getAbsoluteUrl(vectorUrl); const vectorJson = await this._emsClient.getManifest<EmsVectorStyle>( this._emsClient.extendUrlWithParams(url) ); return { ...vectorJson }; } else { return; } }); private _getVectorStyleJsonInlined = _.once(async (): Promise<EmsVectorStyle | undefined> => { const vectorJson = await this._getVectorStyleJsonRaw(); if (vectorJson) { const inlinedSources: EmsVectorSources = {}; const { sources } = vectorJson; for (const sourceName of Object.getOwnPropertyNames(sources)) { const { url } = sources[sourceName]; if (url) { const sourceUrl = this._proxyPath + this._getAbsoluteUrl(url); const extendedUrl = this._emsClient.extendUrlWithParams(sourceUrl); const sourceJson = await this._emsClient.getManifest<VectorSourceSpecification>(extendedUrl); const tiles = sourceJson?.tiles?.map((tileUrl) => { const directUrl = this._proxyPath + this._getAbsoluteUrl(tileUrl); return this._emsClient.extendUrlWithParams(directUrl); }); inlinedSources[sourceName] = { ...sourceJson, type: 'vector', tiles, }; } } return { ...vectorJson, sources: inlinedSources, sprite: await this._getSpriteSheetRootPath(), glyphs: await this._getUrlTemplateForGlyphs(), }; } else { return; } }); constructor(config: TMSServiceConfig, emsClient: EMSClient, proxyPath: string) { super(config, emsClient, proxyPath); this._config = config; } /* This method returns an array of objects with the layers and the new layout['text-field'] to apply using map.setLayoutProperty */ public static transformLanguageProperty( layer: LayerSpecification, lang: string ): DataDrivenPropertyValueSpecification<FormattedSpecification> | undefined { const supportedLang = this.SupportedLanguages.find((l) => l.key === lang); if (layer.type === 'symbol' && layer.layout !== undefined && supportedLang !== undefined) { const omtLang = supportedLang.omt; const textField = layer.layout['text-field']; if (textField && typeof textField === 'string') { return TMSService._getTextField(textField, omtLang); } } return; } /* This method returns an array of objects per layer, containing a list of properties with new colors to apply using map.setPaintProperty */ public static transformColorProperties( layer: LayerSpecification, color?: string, operation?: blendMode, percentage?: number ): { property: keyof layerPaintProperty; color: mbColorDefinition | undefined }[] { if (['background', 'fill', 'line', 'symbol'].indexOf(layer.type) !== -1 && layer.paint) { const paint = layer.paint as layerPaintProperty; if (layer.type === 'symbol' && Object.keys(paint).length === 0) { paint['text-color'] = 'rgb(0,0,0)'; } const types = Object.keys(paint).filter((key) => { return ( [ 'background-color', 'circle-color', 'circle-stroke-color', 'fill-color', 'fill-extrusion-color', 'fill-outline-color', 'icon-color', 'icon-halo-color', 'line-color', 'text-color', 'text-halo-color', ].indexOf(key) !== -1 ); }) as Array<keyof layerPaintProperty>; return types.map((type) => { const paintColor = paint[type]; return { property: type, color: paintColor && color ? colorizeColor(paintColor, color, operation, percentage) : paintColor, }; }); } else { return []; } } private static _getTextField( label: string, lang: string ): DataDrivenPropertyValueSpecification<string> { // Capture {name:xx} labels const labelMatch = label.match(/\{name([:_])(.{2})\}/); if (labelMatch && labelMatch[1] != lang) { // Only apply if the languages are different return ['coalesce', ['get', `name:${lang}`], ['get', `name${labelMatch[1]}${labelMatch[2]}`]]; } else if (label.includes('latin') && label.includes('nonlatin')) { // Capture the common pattern {name:latin}\n{name:nonlatin} return [ 'coalesce', ['get', `name:${lang}`], ['concat', ['get', 'name:latin'], '\n', ['get', 'name:nonlatin']], ]; } // If no case is found, return the input label return label; } async getDefaultRasterStyle(): Promise<EmsRasterStyle | undefined> { const tileJson = await this._getRasterStyleJson(); if (tileJson) { const tiles = tileJson.tiles.map( (tile: string) => this._proxyPath + this._getAbsoluteUrl(tile) ); return { ...tileJson, ...{ tiles }, }; } else { return; } } async getUrlTemplate(): Promise<string> { const tileJson = await this._getRasterStyleJson(); if (tileJson) { const directUrl = this._proxyPath + this._getAbsoluteUrl(tileJson.tiles[0]); return this._emsClient.extendUrlWithParams(directUrl); } else { return ''; } } async getUrlTemplateForVector(sourceId: string): Promise<string> { const tileJson = await this._getVectorStyleJsonInlined(); const url = tileJson?.sources[sourceId]?.tiles?.pop(); if (url) { const directUrl = this._proxyPath + this._getAbsoluteUrl(url); return this._emsClient.extendUrlWithParams(directUrl); } else { return ''; } } async getVectorStyleSheet(): Promise<EmsVectorStyle | undefined> { return await this._getVectorStyleJsonInlined(); } async getVectorStyleSheetRaw(): Promise<EmsVectorStyle | undefined> { return await this._getVectorStyleJsonRaw(); } async getSpriteSheetMeta( isRetina: boolean = false ): Promise<{ png: string; json: EmsSpriteSheet } | undefined> { const metaUrl = await this.getSpriteSheetJsonPath(isRetina); const spritePngs = await this.getSpriteSheetPngPath(isRetina); if (metaUrl && spritePngs) { const metaUrlExtended = this._emsClient.extendUrlWithParams(metaUrl); const jsonMeta = await this._emsClient.getManifest<EmsSpriteSheet>(metaUrlExtended); return { png: spritePngs, json: jsonMeta, }; } else { return; } } async getSpriteSheetJsonPath(isRetina: boolean = false): Promise<string> { const spriteSheetRootPath = await this._getSpriteSheetRootPath(); if (spriteSheetRootPath) { const suffix = isRetina ? '@2x' : ''; return spriteSheetRootPath + suffix + '.json'; } else { return ''; } } async getSpriteSheetPngPath(isRetina: boolean = false): Promise<string> { const spriteSheetRootPath = await this._getSpriteSheetRootPath(); if (spriteSheetRootPath) { const suffix = isRetina ? '@2x' : ''; return spriteSheetRootPath + suffix + '.png'; } else { return ''; } } getDisplayName(): string { return this._emsClient.getValueInLanguage(this._config.name); } async getMinZoom(format = 'vector'): Promise<number | undefined> { switch (format) { case 'vector': { const { sources } = (await this._getVectorStyleJsonInlined()) || { sources: {} }; return Math.min( ...Object.values(sources) .map((s) => { return s && s instanceof Object && 'minzoom' in s ? s['minzoom'] : null; }) .filter((minzoom): minzoom is number => Number.isFinite(minzoom)) ); } case 'raster': { const { minzoom } = (await this._getRasterStyleJson()) || {}; return minzoom; } default: return; } } async getMaxZoom(format = 'vector'): Promise<number | undefined> { switch (format) { case 'vector': { const { sources } = (await this._getVectorStyleJsonInlined()) || { sources: {} }; return Math.max( ...Object.values(sources) .map((s) => { return s && s instanceof Object && 'maxzoom' in s ? s['maxzoom'] : null; }) .filter((maxzoom): maxzoom is number => Number.isFinite(maxzoom)) ); } case 'raster': { const { maxzoom } = (await this._getRasterStyleJson()) || {}; return maxzoom; } default: return; } } getId(): string { return this._config.id; } hasId(id: string): boolean { return this._config.id === id; } getApiUrl(): string { return this._emsClient.getTileApiUrl(); } private _getStyleUrlForLocale(formatType: string): string | undefined { let vectorFormats = this._getFormats(formatType, this._emsClient.getLocale()); if (!vectorFormats.length) { //fallback to default locale vectorFormats = this._getFormats(formatType, this._emsClient.getDefaultLocale()); } if (!vectorFormats.length) { throw new Error( `Cannot find ${formatType} tile layer for locale ${this._emsClient.getLocale()} or ${this._emsClient.getDefaultLocale()}` ); } const defaultStyle = vectorFormats[0]; if (defaultStyle && Object.prototype.hasOwnProperty.call(defaultStyle, 'url')) { return defaultStyle.url; } } private _getFormats(formatType: string, locale: string): EmsTmsFormat[] { return this._config.formats.filter( (format) => format.locale === locale && format.format === formatType ); } private async _getSpriteSheetRootPath(): Promise<string> { const vectorStyleJson = await this._getVectorStyleJsonRaw(); if (vectorStyleJson?.sprite) { return this._proxyPath + this._getAbsoluteUrl(vectorStyleJson.sprite.toString()); } else { return ''; } } private async _getUrlTemplateForGlyphs(): Promise<string> { const vectorStyleJson = await this._getVectorStyleJsonRaw(); if (vectorStyleJson?.glyphs) { return this._proxyPath + this._getAbsoluteUrl(vectorStyleJson.glyphs); } else { return ''; } } }