ol-mapbox-style
Version:
Create OpenLayers layers or maps from Mapbox/MapLibre styles
287 lines (267 loc) • 8.71 kB
JavaScript
/*
ol-mapbox-style - Use Mapbox/MapLibre Style objects with OpenLayers
Copyright 2016-present ol-mapbox-style contributors
License: https://raw.githubusercontent.com/openlayers/ol-mapbox-style/master/LICENSE
*/
import {Color} from '@maplibre/maplibre-gl-style-spec';
import {getCenter} from 'ol/extent.js';
import ImageLayer from 'ol/layer/Image.js';
import {getPointResolution} from 'ol/proj.js';
import Raster from 'ol/source/Raster.js';
import {cameraObj} from './expressions.js';
import {hillshade, raster as rasterShader} from './shaders.js';
import {getValue} from './stylefunction.js';
import {defaultResolutions, emptyObj, getZoomForResolution} from './util.js';
const defaultShadowColor = Color.parse('#000000');
const defaultHighlightColor = Color.parse('#FFFFFF');
const defaultAccentColor = Color.parse('#000000');
/**
* Creates a raster operations layer from a base tile layer.
* Applies hue rotation, saturation, contrast, and brightness adjustments.
* @param {import("ol/layer/Tile.js").default} tileLayer Base raster tile layer.
* @return {ImageLayer<Raster>} The raster operations layer.
*/
export function createRasterOpLayer(tileLayer) {
return new ImageLayer({
source: new Raster({
operationType: 'image',
operation: rasterShader,
sources: [tileLayer],
}),
});
}
/**
* Creates a hillshade layer from a base tile layer.
* @param {import("ol/layer/Tile.js").default} tileLayer Base raster tile layer.
* @return {ImageLayer<Raster>} The hillshade layer.
*/
export function createHillshadeLayer(tileLayer) {
return new ImageLayer({
source: new Raster({
operationType: 'image',
operation: hillshade,
sources: [tileLayer],
}),
});
}
/**
* Attaches the `beforeoperations` event handler to a raster operations layer.
* Reads raster paint properties from the style and passes them to the shader.
* @param {ImageLayer<Raster>} layer The raster operations layer.
* @param {Object} glLayer The Mapbox/MapLibre Style layer object.
* @param {Object} options Options including `resolutions`.
* @param {Object} functionCache Cache for style functions.
*/
export function configureRasterOpLayer(layer, glLayer, options, functionCache) {
layer.getSource().on('beforeoperations', function (event) {
cameraObj.zoom = getZoomForResolution(
event.resolution,
options.resolutions || defaultResolutions,
);
cameraObj.distanceFromCenter = 0;
const data = event.data;
data.saturation = getValue(
glLayer,
'paint',
'raster-saturation',
emptyObj,
functionCache,
);
data.contrast = getValue(
glLayer,
'paint',
'raster-contrast',
emptyObj,
functionCache,
);
data.brightnessHigh = getValue(
glLayer,
'paint',
'raster-brightness-max',
emptyObj,
functionCache,
);
data.brightnessLow = getValue(
glLayer,
'paint',
'raster-brightness-min',
emptyObj,
functionCache,
);
data.hueRotate = getValue(
glLayer,
'paint',
'raster-hue-rotate',
emptyObj,
functionCache,
);
});
}
/**
* Attaches the `beforeoperations` event handler to a hillshade layer.
* Reads hillshade paint properties from the style and passes them to the shader.
* @param {ImageLayer<Raster>} layer The hillshade layer.
* @param {Object} glSource The Mapbox/MapLibre Style source object.
* @param {Object} glLayer The Mapbox/MapLibre Style layer object.
* @param {Object} options Options including `projection` and `resolutions`.
* @param {Object} functionCache Cache for style functions.
*/
export function configureHillshadeLayer(
layer,
glSource,
glLayer,
options,
functionCache,
) {
layer.getSource().on('beforeoperations', function (event) {
const data = event.data;
data.resolution = getPointResolution(
options.projection || 'EPSG:3857',
event.resolution,
getCenter(event.extent),
'm',
);
const zoom = getZoomForResolution(
event.resolution,
options.resolutions || defaultResolutions,
);
cameraObj.zoom = zoom;
cameraObj.distanceFromCenter = 0;
data.zoom = zoom;
data.encoding = glSource.encoding;
// Hillshade method (standard, basic, combined, igor, multidirectional)
data.method =
getValue(glLayer, 'paint', 'hillshade-method', emptyObj, functionCache) ||
'standard';
data.exaggeration = getValue(
glLayer,
'paint',
'hillshade-exaggeration',
emptyObj,
functionCache,
);
// Illumination directions - normalize to array (for multidirectional)
let dirValue = getValue(
glLayer,
'paint',
'hillshade-illumination-direction',
emptyObj,
functionCache,
);
if (dirValue === null || dirValue === undefined) {
dirValue = 335;
}
data.azimuths = Array.isArray(dirValue) ? dirValue : [dirValue];
data.sunAz = data.azimuths[0];
// Illumination altitudes - normalize to array (for basic, combined, multidirectional)
let altValue = getValue(
glLayer,
'paint',
'hillshade-illumination-altitude',
emptyObj,
functionCache,
);
if (altValue === null || altValue === undefined) {
altValue = 45;
}
data.altitudes = Array.isArray(altValue) ? altValue : [altValue];
// Helper to unwrap Color values from expression wrappers
function unwrapColor(val) {
if (val && val.values) {
return val.values[0];
}
return val;
}
// Helper to parse color array properties.
// colorArray values may be a single color string, a Color object,
// or an array of color strings (for multidirectional).
// Must distinguish ["#ff0000", "#00ff00"] from ["interpolate", ...].
function getColorArray(property) {
const raw = glLayer.paint?.[property];
if (
Array.isArray(raw) &&
raw.length > 0 &&
typeof raw[0] === 'string' &&
Color.parse(raw[0]) !== undefined
) {
// Array of color strings - parse each individually
return raw.map((c) => Color.parse(c));
}
// Single value or expression - use getValue
let val = getValue(glLayer, 'paint', property, emptyObj, functionCache);
val = unwrapColor(val);
return val ? [val] : undefined;
}
data.highlightColors = getColorArray('hillshade-highlight-color');
data.highlightColor = data.highlightColors?.[0] || defaultHighlightColor;
if (!data.highlightColors) {
data.highlightColors = [data.highlightColor];
}
data.shadowColors = getColorArray('hillshade-shadow-color');
data.shadowColor = data.shadowColors?.[0] || defaultShadowColor;
if (!data.shadowColors) {
data.shadowColors = [data.shadowColor];
}
data.accentColor =
unwrapColor(
getValue(
glLayer,
'paint',
'hillshade-accent-color',
emptyObj,
functionCache,
),
) || defaultAccentColor;
});
}
/**
* Keys that indicate a raster layer requires shader operations.
* @type {Array<string>}
*/
export const rasterOperationKeys = [
'raster-saturation',
'raster-contrast',
'raster-brightness-max',
'raster-brightness-min',
'raster-hue-rotate',
];
/**
* Returns a prerender listener for raster layers that updates opacity.
* @param {Object} glLayer The Mapbox/MapLibre Style layer object.
* @param {import("ol/layer/Base.js").default} layer The OpenLayers layer.
* @param {Object} functionCache Cache for style functions.
* @return {function(import("ol/render/Event.js").default): void} Prerender listener.
*/
export 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);
}
};
}
/**
* Updates raster layer opacity from style properties.
* @param {Object} glLayer The Mapbox/MapLibre Style layer object.
* @param {import("ol/layer/Base.js").default} layer The OpenLayers layer.
* @param {number} zoom Current zoom level.
* @param {Object} functionCache Cache for style functions.
*/
function updateRasterLayerProperties(glLayer, layer, zoom, functionCache) {
cameraObj.zoom = zoom;
cameraObj.distanceFromCenter = 0;
const opacity = getValue(
glLayer,
'paint',
'raster-opacity',
emptyObj,
functionCache,
);
layer.setOpacity(opacity);
}