/* ol-mapbox-style - Use Mapbox Style objects with OpenLayers Copyright 2016-present ol-mapbox-style contributors License: https://raw.githubusercontent.com/openlayers/ol-mapbox-style/master/LICENSE */ import GeoJSON from 'ol/format/GeoJSON.js'; import ImageLayer from 'ol/layer/Image.js'; import Layer from 'ol/layer/Layer.js'; import LayerGroup from 'ol/layer/Group.js'; import MVT from 'ol/format/MVT.js'; import Map from 'ol/Map.js'; import Raster from 'ol/source/Raster.js'; import Source from 'ol/source/Source.js'; import TileGrid from 'ol/tilegrid/TileGrid.js'; import TileJSON from 'ol/source/TileJSON.js'; import TileLayer from 'ol/layer/Tile.js'; import VectorLayer from 'ol/layer/Vector.js'; import VectorSource from 'ol/source/Vector.js'; import VectorTileLayer from 'ol/layer/VectorTile.js'; import VectorTileSource, {defaultLoadFunction} from 'ol/source/VectorTile.js'; import View from 'ol/View.js'; import derefLayers from '@mapbox/mapbox-gl-style-spec/deref.js'; import { METERS_PER_UNIT, equivalent, fromLonLat, get as getProjection, getUserProjection, } from 'ol/proj.js'; import { _colorWithOpacity, stylefunction as applyStyleFunction, getValue, } from './stylefunction.js'; import {bbox as bboxStrategy} from 'ol/loadingstrategy.js'; import {createXYZ} from 'ol/tilegrid.js'; import { defaultResolutions, fetchResource, getFunctionCache, getGlStyle, getTileJson, getZoomForResolution, } from './util.js'; import {getFonts} from './text.js'; import {getTopLeft} from 'ol/extent.js'; import {hillshade} from './shaders.js'; import { normalizeSourceUrl, normalizeSpriteUrl, normalizeStyleUrl, } from './mapbox.js'; /** * @typedef {Object} Options * @property {string} [accessToken] Access token for 'mapbox://' urls. * @property {function(string, ResourceType): (Request|void)} [transformRequest] * Function for controlling how `ol-mapbox-style` fetches resources. Can be used for modifying * the url, adding headers or setting credentials options. Called with the url and the resource * type as arguments, this function is supposed to return a `Request` object. Without a return value, * the original request will not be modified. For `Tiles` and `GeoJSON` resources, only the `url` of * the returned request will be respected. * @property {string} [projection='EPSG:3857'] Only useful when working with non-standard projections. * Code of a projection registered with OpenLayers. All sources of the style must be provided in this * projection. The projection must also have a valid extent defined, which will be used to determine the * origin and resolutions of the tile grid for all tiled sources of the style. When provided, the bbox * placeholder in tile and geojson urls changes: the default is `{bbox-epsg-3857}`, when projection is e.g. * set to `EPSG:4326`, the bbox placeholder will be `{bbox-epsg-4326}`. * @property {Array} [resolutions] Only useful when working with non-standard projections. * Resolutions for mapping resolution to the `zoom` used in the Mapbox style. * @property {string} [styleUrl] URL of the Mapbox GL style. Required for styles that were provided * as object, when they contain a relative sprite url, or sources referencing data by relative url. * @property {function(VectorLayer|VectorTileLayer, string):HTMLImageElement|HTMLCanvasElement|string|undefined} [getImage=undefined] * Function that returns an image for an icon name. If the result is an HTMLImageElement, it must already be * loaded. The layer can be used to call layer.changed() when the loading and processing of the image has finished. * This function be used for icons not in the sprite or to override sprite icons. * @property {string} [accessTokenParam='access_token'] Access token param. For internal use. */ /** * @typedef {Object} ApplyStyleOptions * @property {string} [source=''] Source. Default is `''`, which causes the first source in the * style to be used. * @property {Array} [layers] Layers. If no source is provided, the layers with the * provided ids will be used from the style's `layers` array. All layers need to use the same source. * @property {boolean} [updateSource=true] Update or create vector (tile) layer source with parameters * specified for the source in the mapbox style definition. */ /** @typedef {'Style'|'Source'|'Sprite'|'SpriteImage'|'Tiles'|'GeoJSON'} ResourceType */ /** * @param {import("ol/proj/Projection.js").default} projection Projection. * @param {number} [tileSize=512] Tile size. * @return {Array} Resolutions. */ function getTileResolutions(projection, tileSize = 512) { return projection.getExtent() ? createXYZ({ extent: projection.getExtent(), tileSize: tileSize, maxZoom: 22, }).getResolutions() : defaultResolutions; } /** * @param {string} styleUrl Style URL. * @param {Options} options Options. * @return {Options} Completed options with accessToken and accessTokenParam. */ function completeOptions(styleUrl, options) { if (!options.accessToken) { options = Object.assign({}, options); const searchParams = new URL(styleUrl).searchParams; // The last search parameter is the access token searchParams.forEach((value, key) => { options.accessToken = value; options.accessTokenParam = key; }); } return options; } /** * Applies a style function to an `ol/layer/VectorTile` or `ol/layer/Vector` * with an `ol/source/VectorTile` or an `ol/source/Vector`. If the layer does not have a source * yet, it will be created and populated from the information in the `glStyle` (unless `updateSource` is * set to `false`). * * **Example:** * ```js * import {applyStyle} from 'ol-mapbox-style'; * import {VectorTile} from 'ol/layer.js'; * * const layer = new VectorTile({declutter: true}); * applyStyle(layer, 'https://api.maptiler.com/maps/basic/style.json?key=YOUR_OPENMAPTILES_TOKEN'); * ``` * * The style function will render all layers from the `glStyle` object that use the source * of the first layer, the specified `source`, or a subset of layers from the same source. The * source needs to be a `"type": "vector"` or `"type": "geojson"` source. * * Two additional properties will be set on the provided layer: * * * `mapbox-source`: The `id` of the Mapbox Style document's source that the * OpenLayers layer was created from. Usually `apply()` creates one * OpenLayers layer per Mapbox Style source, unless the layer stack has * layers from different sources in between. * * `mapbox-layers`: The `id`s of the Mapbox Style document's layers that are * included in the OpenLayers layer. * * @param {VectorTileLayer|VectorLayer} layer OpenLayers layer. When the layer has a source configured, * it will be modified to use the configuration from the glStyle's `source`. Options specified on the * layer's source will override those from the glStyle's `source`, except for `url` and * `tileUrlFunction`. When the source projection is the default (`EPSG:3857`), the `tileGrid` will * also be overridden. If you'd rather not have ol-mapbox-style modify the source, configure `applyStyle()` * with the `updateSource: false` option. * @param {string|Object} glStyle Mapbox Style object. * @param {string|Array|Options&ApplyStyleOptions} [sourceOrLayersOrOptions] Options or * `source` key or an array of layer `id`s from the Mapbox Style object. When a `source` key is * provided, all layers for the specified source will be included in the style function. When layer * `id`s are provided, they must be from layers that use the same source. When not provided or a falsey * value, all layers using the first source specified in the glStyle will be rendered. * @param {Options&ApplyStyleOptions|string} [optionsOrPath] **Deprecated**. Options. Alternatively the path of the style file * (only required when a relative path is used for the `"sprite"` property of the style). * @param {Array} [resolutions] **Deprecated**. Resolutions for mapping resolution to zoom level. * Only needed when working with non-standard tile grids or projections, can also be supplied with * options. * @return {Promise} Promise which will be resolved when the style can be used * for rendering. */ export function applyStyle( layer, glStyle, sourceOrLayersOrOptions = '', optionsOrPath = {}, resolutions = undefined ) { let styleUrl, sourceId; /** @type {Options&ApplyStyleOptions} */ let options; let sourceOrLayers; let updateSource = true; if ( typeof sourceOrLayersOrOptions !== 'string' && !Array.isArray(sourceOrLayersOrOptions) ) { options = sourceOrLayersOrOptions; sourceOrLayers = options.source || options.layers; optionsOrPath = options; } else { sourceOrLayers = sourceOrLayersOrOptions; } if (typeof optionsOrPath === 'string') { styleUrl = optionsOrPath; options = {}; } else { styleUrl = optionsOrPath.styleUrl; options = optionsOrPath; } if (options.updateSource === false) { updateSource = false; } if (!resolutions) { resolutions = options.resolutions; } if ( !styleUrl && typeof glStyle === 'string' && !glStyle.trim().startsWith('{') ) { styleUrl = glStyle; } if (styleUrl) { styleUrl = styleUrl.startsWith('data:') ? location.href : normalizeStyleUrl(styleUrl, options.accessToken); options = completeOptions(styleUrl, options); } return new Promise(function (resolve, reject) { // TODO: figure out where best place to check source type is // Note that the source arg is an array of gl layer ids and each must be // dereferenced to get source type to validate getGlStyle(glStyle, options) .then(function (glStyle) { if (glStyle.version != 8) { return reject(new Error('glStyle version 8 required.')); } if ( !(layer instanceof VectorLayer || layer instanceof VectorTileLayer) ) { return reject( new Error('Can only apply to VectorLayer or VectorTileLayer') ); } const type = layer instanceof VectorTileLayer ? 'vector' : 'geojson'; if (!sourceOrLayers) { sourceId = Object.keys(glStyle.sources).find(function (key) { return glStyle.sources[key].type === type; }); sourceOrLayers = sourceId; } else if (Array.isArray(sourceOrLayers)) { sourceId = glStyle.layers.find(function (layer) { return layer.id === sourceOrLayers[0]; }).source; } else { sourceId = sourceOrLayers; } if (!sourceId) { return reject(new Error(`No ${type} source found in the glStyle.`)); } function assignSource() { if (!updateSource) { return Promise.resolve(); } if (layer instanceof VectorTileLayer) { return setupVectorSource( glStyle.sources[sourceId], styleUrl, options ).then(function (source) { const targetSource = layer.getSource(); if (!targetSource) { layer.setSource(source); } else if (source !== targetSource) { targetSource.setTileUrlFunction(source.getTileUrlFunction()); if ( typeof targetSource.setUrls === 'function' && typeof source.getUrls === 'function' ) { // to get correct keys for tile cache and queue targetSource.setUrls(source.getUrls()); } //@ts-ignore if (!targetSource.format_) { //@ts-ignore targetSource.format_ = source.format_; } if (!targetSource.getAttributions()) { targetSource.setAttributions(source.getAttributions()); } if ( targetSource.getTileLoadFunction() === defaultLoadFunction ) { targetSource.setTileLoadFunction( source.getTileLoadFunction() ); } if ( equivalent( targetSource.getProjection(), source.getProjection() ) ) { targetSource.tileGrid = source.getTileGrid(); } } if ( !isFinite(layer.getMaxResolution()) && !isFinite(layer.getMinZoom()) ) { const tileGrid = layer.getSource().getTileGrid(); layer.setMaxResolution( tileGrid.getResolution(tileGrid.getMinZoom()) ); } }); } const glSource = glStyle.sources[sourceId]; let source = layer.getSource(); if (!source || source.get('mapbox-source') !== glSource) { source = setupGeoJSONSource(glSource, styleUrl, options); } const targetSource = /** @type {VectorSource} */ (layer.getSource()); if (!targetSource) { layer.setSource(source); } else if (source !== targetSource) { if (!targetSource.getAttributions()) { targetSource.setAttributions(source.getAttributions()); } //@ts-ignore if (!targetSource.format_) { //@ts-ignore targetSource.format_ = source.getFormat(); } //@ts-ignore targetSource.url_ = source.getUrl(); } return Promise.resolve(); } let spriteScale, spriteData, spriteImageUrl, style; function onChange() { if (!style && (!glStyle.sprite || spriteData)) { if (options.projection && !resolutions) { const projection = getProjection(options.projection); const units = projection.getUnits(); if (units !== 'm') { resolutions = defaultResolutions.map( (resolution) => resolution / METERS_PER_UNIT[units] ); } } style = applyStyleFunction( layer, glStyle, sourceOrLayers, resolutions, spriteData, spriteImageUrl, getFonts, options.getImage ); if (!layer.getStyle()) { reject(new Error(`Nothing to show for source [${sourceId}]`)); } else { assignSource().then(resolve).catch(reject); } } else if (style) { layer.setStyle(style); assignSource().then(resolve).catch(reject); } else { reject(new Error('Something went wrong trying to apply style.')); } } if (glStyle.sprite) { const sprite = new URL( normalizeSpriteUrl( glStyle.sprite, options.accessToken, styleUrl || location.href ) ); spriteScale = window.devicePixelRatio >= 1.5 ? 0.5 : 1; const sizeFactor = spriteScale == 0.5 ? '@2x' : ''; let spriteUrl = sprite.origin + sprite.pathname + sizeFactor + '.json' + sprite.search; new Promise(function (resolve, reject) { fetchResource('Sprite', spriteUrl, options) .then(resolve) .catch(function (error) { spriteUrl = sprite.origin + sprite.pathname + '.json' + sprite.search; fetchResource('Sprite', spriteUrl, options) .then(resolve) .catch(reject); }); }) .then(function (spritesJson) { if (spritesJson === undefined) { reject(new Error('No sprites found.')); } spriteData = spritesJson; spriteImageUrl = sprite.origin + sprite.pathname + sizeFactor + '.png' + sprite.search; if (options.transformRequest) { const transformed = options.transformRequest( spriteImageUrl, 'SpriteImage' ); if (transformed instanceof Request) { spriteImageUrl = encodeURI(transformed.url); } } onChange(); }) .catch(function (err) { reject( new Error( `Sprites cannot be loaded: ${spriteUrl}: ${err.message}` ) ); }); } else { onChange(); } }) .catch(reject); }); } const emptyObj = {}; function setFirstBackground(mapOrLayer, glStyle, options) { glStyle.layers.some(function (layer) { if (layer.type === 'background') { if (mapOrLayer instanceof Layer) { mapOrLayer.setBackground(function (resolution) { return getBackgroundColor(layer, resolution, options, {}); }); return true; } if (mapOrLayer instanceof Map || mapOrLayer instanceof LayerGroup) { mapOrLayer.getLayers().push(setupBackgroundLayer(layer, options, {})); return true; } } }); } /** * Applies properties of the Mapbox Style's first `background` layer to the * provided map or layer (group). * * **Example:** * ```js * import {applyBackground} from 'ol-mapbox-style'; * import {Map} from 'ol'; * * const map = new Map({target: 'map'}); * applyBackground(map, 'https://api.maptiler.com/maps/basic/style.json?key=YOUR_OPENMAPTILES_TOKEN'); * ``` * @param {Map|import("ol/layer/Base.js").default} mapOrLayer OpenLayers Map or layer (group). * @param {Object|string} glStyle Mapbox Style object or url. * @param {Options} options Options. * @return {Promise} Promise that resolves when the background is applied. */ export function applyBackground(mapOrLayer, glStyle, options = {}) { return getGlStyle(glStyle, options).then(function (glStyle) { setFirstBackground(mapOrLayer, glStyle, options); }); } function getSourceIdByRef(layers, ref) { let sourceId; layers.some(function (layer) { if (layer.id == ref) { sourceId = layer.source; return true; } }); return sourceId; } function extentFromTileJSON(tileJSON, projection) { const bounds = tileJSON.bounds; if (bounds) { const ll = fromLonLat([bounds[0], bounds[1]], projection); const tr = fromLonLat([bounds[2], bounds[3]], projection); return [ll[0], ll[1], tr[0], tr[1]]; } return getProjection(projection).getExtent(); } function sourceOptionsFromTileJSON(glSource, tileJSON, options) { const tileJSONSource = new TileJSON({ tileJSON: tileJSON, tileSize: glSource.tileSize || tileJSON.tileSize || 512, }); const tileJSONDoc = tileJSONSource.getTileJSON(); const tileGrid = tileJSONSource.getTileGrid(); const projection = getProjection(options.projection || 'EPSG:3857'); const extent = extentFromTileJSON(tileJSONDoc, projection); const projectionExtent = projection.getExtent(); const minZoom = tileJSONDoc.minzoom || 0; const maxZoom = tileJSONDoc.maxzoom || 22; /** @type {import("ol/source/VectorTile.js").Options} */ const sourceOptions = { attributions: tileJSONSource.getAttributions(), projection: projection, tileGrid: new TileGrid({ origin: projectionExtent ? getTopLeft(projectionExtent) : tileGrid.getOrigin(0), extent: extent || tileGrid.getExtent(), minZoom: minZoom, resolutions: getTileResolutions(projection, tileJSON.tileSize).slice( 0, maxZoom + 1 ), tileSize: tileGrid.getTileSize(0), }), }; if (Array.isArray(tileJSONDoc.tiles)) { sourceOptions.urls = tileJSONDoc.tiles; } else { sourceOptions.url = tileJSONDoc.tiles; } return sourceOptions; } function getBackgroundColor(glLayer, resolution, options, functionCache) { const background = { id: glLayer.id, type: glLayer.type, }; const layout = glLayer.layout || {}; const paint = glLayer.paint || {}; background['paint'] = paint; const zoom = getZoomForResolution( resolution, options.resolutions || defaultResolutions ); let bg, opacity; if (paint['background-color'] !== undefined) { bg = getValue( background, 'paint', 'background-color', zoom, emptyObj, functionCache ); } if (paint['background-opacity'] !== undefined) { opacity = getValue( background, 'paint', 'background-opacity', zoom, emptyObj, functionCache ); } return layout.visibility == 'none' ? undefined : _colorWithOpacity(bg, opacity); } /** * @param {Object} glLayer Mapbox Style layer object. * @param {Options} options Options. * @param {Object} functionCache Cache for functions. * @return {Layer} OpenLayers layer. */ function setupBackgroundLayer(glLayer, options, functionCache) { const div = document.createElement('div'); div.className = 'ol-mapbox-style-background'; div.style.position = 'absolute'; div.style.width = '100%'; div.style.height = '100%'; return new Layer({ source: new Source({}), render(frameState) { const color = getBackgroundColor( glLayer, frameState.viewState.resolution, options, functionCache ); div.style.backgroundColor = color; return div; }, }); } /** * Creates an OpenLayers VectorTile source for a gl source entry. * @param {Object} glSource "source" entry from a Mapbox Style object. * @param {string|undefined} styleUrl URL to use for the source. This is expected to be the complete http(s) url, * with access key applied. * @param {Options} options Options. * @return {Promise} Promise resolving to a VectorTile source. * @private */ export function setupVectorSource(glSource, styleUrl, options) { return new Promise(function (resolve, reject) { getTileJson(glSource, styleUrl, options) .then(function (tileJSON) { const sourceOptions = sourceOptionsFromTileJSON( glSource, tileJSON, options ); sourceOptions.format = new MVT(); resolve(new VectorTileSource(sourceOptions)); }) .catch(reject); }); } function setupVectorLayer(glSource, styleUrl, options) { const layer = new VectorTileLayer({ declutter: true, visible: false, }); setupVectorSource(glSource, styleUrl, options) .then(function (source) { source.set('mapbox-source', glSource); layer.setSource(source); }) .catch(function (error) { layer.setSource(undefined); }); return layer; } function getBboxTemplate(projection) { const projCode = projection ? projection.getCode() : 'EPSG:3857'; return `{bbox-${projCode.toLowerCase().replace(/[^a-z0-9]/g, '-')}}`; } function setupRasterLayer(glSource, styleUrl, options) { const layer = new TileLayer(); getTileJson(glSource, styleUrl, options) .then(function (tileJson) { const source = new TileJSON({ interpolate: options.interpolate === undefined ? true : options.interpolate, transition: 0, crossOrigin: 'anonymous', tileJSON: tileJson, }); source.tileGrid = sourceOptionsFromTileJSON( glSource, tileJson, options ).tileGrid; if (options.projection) { //@ts-ignore source.projection = getProjection(options.projection); } const getTileUrl = source.getTileUrlFunction(); source.setTileUrlFunction(function (tileCoord, pixelRatio, projection) { const bboxTemplate = getBboxTemplate(projection); let src = getTileUrl(tileCoord, pixelRatio, projection); if (src.indexOf(bboxTemplate) != -1) { const bbox = source.getTileGrid().getTileCoordExtent(tileCoord); src = src.replace(bboxTemplate, bbox.toString()); } return src; }); source.set('mapbox-source', glSource); layer.setSource(source); }) .catch(function (error) { layer.setSource(undefined); }); return layer; } /** * * @param {Object} glSource "source" entry from a Mapbox Style object. * @param {string} styleUrl Style url * @param {Options} options ol-mapbox-style options. * @return {ImageLayer} The raster layer */ function setupHillshadeLayer(glSource, styleUrl, options) { const tileLayer = setupRasterLayer(glSource, styleUrl, options); /** @type {ImageLayer} */ const layer = new ImageLayer({ source: new Raster({ operationType: 'image', operation: hillshade, sources: [tileLayer], }), }); return layer; } /** * @param {Object} glSource glStyle source. * @param {string} styleUrl Style URL. * @param {Options} options Options. * @return {VectorSource} Configured vector source. */ function setupGeoJSONSource(glSource, styleUrl, options) { const geoJsonFormat = options.projection ? new GeoJSON({dataProjection: options.projection}) : new GeoJSON(); const data = glSource.data; const sourceOptions = {}; if (typeof data == 'string') { let geoJsonUrl = normalizeSourceUrl( data, options.accessToken, options.accessTokenParam || 'access_token', styleUrl || location.href ); if (options.transformRequest) { const transformed = options.transformRequest(geoJsonUrl, 'GeoJSON'); if (transformed instanceof Request) { geoJsonUrl = decodeURI(transformed.url); } } if (/\{bbox-[0-9a-z-]+\}/.test(geoJsonUrl)) { const extentUrl = (extent, resolution, projection) => { const bboxTemplate = getBboxTemplate(projection); return geoJsonUrl.replace(bboxTemplate, `${extent.join(',')}`); }; const source = new VectorSource({ attributions: glSource.attribution, format: geoJsonFormat, url: extentUrl, strategy: bboxStrategy, }); source.set('mapbox-source', glSource); return source; } return new VectorSource({ attributions: glSource.attribution, format: geoJsonFormat, url: geoJsonUrl, }); } sourceOptions.features = geoJsonFormat.readFeatures(data, { featureProjection: getUserProjection() || 'EPSG:3857', }); const source = new VectorSource( Object.assign( { attributions: glSource.attribution, format: geoJsonFormat, }, sourceOptions ) ); source.set('mapbox-source', glSource); return source; } function setupGeoJSONLayer(glSource, styleUrl, options) { return new VectorLayer({ declutter: true, source: setupGeoJSONSource(glSource, styleUrl, options), visible: false, }); } function prerenderRasterLayer(glLayer, layer, functionCache) { let zoom = null; return function (event) { if ( glLayer.paint && 'raster-opacity' in glLayer.paint && event.frameState.viewState.zoom !== zoom ) { zoom = event.frameState.viewState.zoom; delete functionCache[glLayer.id]; updateRasterLayerProperties(glLayer, layer, zoom, functionCache); } }; } function updateRasterLayerProperties(glLayer, layer, zoom, functionCache) { const opacity = getValue( glLayer, 'paint', 'raster-opacity', zoom, emptyObj, functionCache ); layer.setOpacity(opacity); } function manageVisibility(layer, mapOrGroup) { function onChange() { const glStyle = mapOrGroup.get('mapbox-style'); if (!glStyle) { return; } const mapboxLayers = derefLayers(glStyle.layers); const layerMapboxLayerids = layer.get('mapbox-layers'); const visible = mapboxLayers .filter(function (mapboxLayer) { return layerMapboxLayerids.includes(mapboxLayer.id); }) .some(function (mapboxLayer) { return ( !mapboxLayer.layout || !mapboxLayer.layout.visibility || mapboxLayer.layout.visibility === 'visible' ); }); if (layer.get('visible') !== visible) { layer.setVisible(visible); } } layer.on('change', onChange); onChange(); } export function setupLayer(glStyle, styleUrl, glLayer, options) { const functionCache = getFunctionCache(glStyle); const glLayers = glStyle.layers; const type = glLayer.type; const id = glLayer.source || getSourceIdByRef(glLayers, glLayer.ref); const glSource = glStyle.sources[id]; let layer; if (type == 'background') { layer = setupBackgroundLayer(glLayer, options, functionCache); } else if (glSource.type == 'vector') { layer = setupVectorLayer(glSource, styleUrl, options); } else if (glSource.type == 'raster') { layer = setupRasterLayer(glSource, styleUrl, options); layer.setVisible( glLayer.layout ? glLayer.layout.visibility !== 'none' : true ); layer.on('prerender', prerenderRasterLayer(glLayer, layer, functionCache)); } else if (glSource.type == 'geojson') { layer = setupGeoJSONLayer(glSource, styleUrl, options); } else if (glSource.type == 'raster-dem' && glLayer.type == 'hillshade') { const hillshadeLayer = setupHillshadeLayer(glSource, styleUrl, options); layer = hillshadeLayer; hillshadeLayer.getSource().on('beforeoperations', function (event) { const data = event.data; data.resolution = event.resolution; const zoom = getZoomForResolution( event.resolution, options.resolutions || defaultResolutions ); data.encoding = glSource.encoding; data.vert = 5 * getValue( glLayer, 'paint', 'hillshade-exaggeration', zoom, emptyObj, functionCache ); data.sunAz = getValue( glLayer, 'paint', 'hillshade-illumination-direction', zoom, emptyObj, functionCache ); data.sunEl = 35; data.opacity = 0.3; data.highlightColor = getValue( glLayer, 'paint', 'hillshade-highlight-color', zoom, emptyObj, functionCache ); data.shadowColor = getValue( glLayer, 'paint', 'hillshade-shadow-color', zoom, emptyObj, functionCache ); data.accentColor = getValue( glLayer, 'paint', 'hillshade-accent-color', zoom, emptyObj, functionCache ); }); layer.setVisible( glLayer.layout ? glLayer.layout.visibility !== 'none' : true ); } const glSourceId = id; if (layer) { layer.set('mapbox-source', glSourceId); } return layer; } /** * @param {*} glStyle Mapbox Style. * @param {Map|LayerGroup} mapOrGroup Map or layer group. * @param {string} styleUrl Style URL. * @param {Options} options Options. * @return {Promise} Promise that resolves when the style is loaded. */ function processStyle(glStyle, mapOrGroup, styleUrl, options) { const promises = []; let view = null; if (mapOrGroup instanceof Map) { view = mapOrGroup.getView(); if (!view.isDef() && !view.getRotation() && !view.getResolutions()) { const projection = options.projection ? getProjection(options.projection) : view.getProjection(); view = new View( Object.assign(view.getProperties(), { maxResolution: defaultResolutions[0] / METERS_PER_UNIT[projection.getUnits()], projection: options.projection || view.getProjection(), }) ); mapOrGroup.setView(view); } if ('center' in glStyle && !view.getCenter()) { view.setCenter(fromLonLat(glStyle.center, view.getProjection())); } if ('zoom' in glStyle && view.getZoom() === undefined) { view.setResolution( defaultResolutions[0] / METERS_PER_UNIT[view.getProjection().getUnits()] / Math.pow(2, glStyle.zoom) ); } if (!view.getCenter() || view.getZoom() === undefined) { view.fit(view.getProjection().getExtent(), { nearest: true, size: mapOrGroup.getSize(), }); } } mapOrGroup.set('mapbox-style', glStyle); mapOrGroup.set('mapbox-metadata', {styleUrl, options}); const glLayers = glStyle.layers; let layerIds = []; let layer, glSourceId, id; for (let i = 0, ii = glLayers.length; i < ii; ++i) { const glLayer = glLayers[i]; const type = glLayer.type; if (type == 'heatmap') { //FIXME Unsupported layer type throw new Error(`${type} layers are not supported`); } else { id = glLayer.source || getSourceIdByRef(glLayers, glLayer.ref); // this technique assumes gl layers will be in a particular order if (!id || id != glSourceId) { if (layerIds.length) { promises.push( finalizeLayer( layer, layerIds, glStyle, styleUrl, mapOrGroup, options ) ); layerIds = []; } layer = setupLayer(glStyle, styleUrl, glLayer, options); if ( !(layer instanceof VectorLayer || layer instanceof VectorTileLayer) ) { layerIds = []; } glSourceId = layer.get('mapbox-source'); } layerIds.push(glLayer.id); } } promises.push( finalizeLayer(layer, layerIds, glStyle, styleUrl, mapOrGroup, options) ); return Promise.all(promises); } /** * Loads and applies a Mapbox Style object into an OpenLayers Map or LayerGroup. * This includes the map background, the layers, and for Map instances that did not * have a View defined yet also the center and the zoom. * * **Example:** * ```js * import apply from 'ol-mapbox-style'; * * apply('map', 'mapbox://styles/mapbox/bright-v9', {accessToken: 'YOUR_MAPBOX_TOKEN'}); * ``` * * The center and zoom will only be set if present in the Mapbox Style document, * and if not already set on the OpenLayers map. * * Layers will be added to the OpenLayers map, without affecting any layers that * might already be set on the map. * * Layers added by `apply()` will have two additional properties: * * * `mapbox-source`: The `id` of the Mapbox Style document's source that the * OpenLayers layer was created from. Usually `apply()` creates one * OpenLayers layer per Mapbox Style source, unless the layer stack has * layers from different sources in between. * * `mapbox-layers`: The `id`s of the Mapbox Style document's layers that are * included in the OpenLayers layer. * * This function sets an additional `mapbox-style` property on the OpenLayers * Map or LayerGroup instance, which holds the Mapbox Style object. * * @param {Map|HTMLElement|string|LayerGroup} mapOrGroupOrElement Either an existing * OpenLayers Map instance, or a HTML element, or the id of a HTML element that will be * the target of a new OpenLayers Map, or a layer group. If layer group, styles * releated to the map and view will be ignored. * @param {string|Object} style JSON style object or style url pointing to a * Mapbox Style object. When using Mapbox APIs, the url is the `styleUrl` * shown in Mapbox Studio's "share" panel. In addition, the `accessToken` option * (see below) must be set. * When passed as JSON style object, all OpenLayers layers created by `apply()` * will be immediately available, but they may not have a source yet (i.e. when * they are defined by a TileJSON url in the Mapbox Style document). When passed * as style url, layers will be added to the map when the Mapbox Style document * is loaded and parsed. * @param {Options} options Options. * @return {Promise} A promise that resolves after all layers have been added to * the OpenLayers Map instance or LayerGroup, their sources set, and their styles applied. The * `resolve` callback will be called with the OpenLayers Map instance or LayerGroup as * argument. */ export function apply(mapOrGroupOrElement, style, options = {}) { let promise; /** @type {Map|LayerGroup} */ let mapOrGroup; if ( typeof mapOrGroupOrElement === 'string' || mapOrGroupOrElement instanceof HTMLElement ) { mapOrGroup = new Map({ target: mapOrGroupOrElement, }); } else { mapOrGroup = mapOrGroupOrElement; } if (typeof style === 'string') { const styleUrl = style.startsWith('data:') ? location.href : normalizeStyleUrl(style, options.accessToken); options = completeOptions(styleUrl, options); promise = new Promise(function (resolve, reject) { getGlStyle(style, options) .then(function (glStyle) { processStyle(glStyle, mapOrGroup, styleUrl, options) .then(function () { resolve(mapOrGroup); }) .catch(reject); }) .catch(function (err) { reject(new Error(`Could not load ${style}: ${err.message}`)); }); }); } else { promise = new Promise(function (resolve, reject) { processStyle( style, mapOrGroup, !options.styleUrl || options.styleUrl.startsWith('data:') ? location.href : normalizeStyleUrl(options.styleUrl, options.accessToken), options ) .then(function () { resolve(mapOrGroup); }) .catch(reject); }); } return promise; } /** * If layerIds is not empty, applies the style specified in glStyle to the layer, * and adds the layer to the map. * * The layer may not yet have a source when the function is called. If so, the style * is applied to the layer via a once listener on the 'change:source' event. * * @param {Layer} layer An OpenLayers layer instance. * @param {Array} layerIds Array containing layer ids of already-processed layers. * @param {Object} glStyle Style as a JSON object. * @param {string|undefined} styleUrl The original style URL. Only required * when a relative path is used with the `"sprite"` property of the style. * @param {Map|LayerGroup} mapOrGroup OpenLayers Map. * @param {Options} options Options. * @return {Promise} Returns a promise that resolves after the source has * been set on the specified layer, and the style has been applied. */ export function finalizeLayer( layer, layerIds, glStyle, styleUrl, mapOrGroup, options = {} ) { let minZoom = 24; let maxZoom = 0; const glLayers = glStyle.layers; for (let i = 0, ii = glLayers.length; i < ii; ++i) { const glLayer = glLayers[i]; if (layerIds.indexOf(glLayer.id) !== -1) { minZoom = Math.min('minzoom' in glLayer ? glLayer.minzoom : 0, minZoom); maxZoom = Math.max('maxzoom' in glLayer ? glLayer.maxzoom : 24, maxZoom); } } return new Promise(function (resolve, reject) { const setStyle = function () { const source = layer.getSource(); if (!source || source.getState() === 'error') { reject( new Error( 'Error accessing data for source ' + layer.get('mapbox-source') ) ); return; } if ('getTileGrid' in source) { const tileGrid = /** @type {import("ol/source/Tile.js").default|import("ol/source/VectorTile.js").default} */ ( source ).getTileGrid(); if (tileGrid) { const sourceMinZoom = tileGrid.getMinZoom(); if (minZoom > 0 || sourceMinZoom > 0) { layer.setMaxResolution( Math.min( defaultResolutions[minZoom], tileGrid.getResolution(sourceMinZoom) ) + 1e-9 ); } if (maxZoom < 24) { layer.setMinResolution(defaultResolutions[maxZoom] + 1e-9); } } } else { if (minZoom > 0) { layer.setMaxResolution(defaultResolutions[minZoom] + 1e-9); } } if ( source instanceof VectorSource || source instanceof VectorTileSource ) { applyStyle( /** @type {import("ol/layer/Vector").default|import("ol/layer/VectorTile").default} */ ( layer ), glStyle, layerIds, Object.assign({styleUrl: styleUrl}, options) ) .then(function () { manageVisibility(layer, mapOrGroup); resolve(); }) .catch(reject); } else { resolve(); } }; layer.set('mapbox-layers', layerIds); const layers = mapOrGroup.getLayers(); if (layers.getArray().indexOf(layer) === -1) { layers.push(layer); } if (layer.getSource()) { setStyle(); } else { layer.once('change:source', setStyle); } }); }