From 0eec3284f1d557e03d328655f1dbe4b0bec25629 Mon Sep 17 00:00:00 2001 From: orangemug Date: Tue, 12 Dec 2023 13:50:05 +0000 Subject: [PATCH 1/9] Added raster-(hue-rotate/saturation/opacity) as operations. --- src/apply.js | 87 ++++++++++++++++++++++------------- src/shaders.js | 121 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 31 deletions(-) diff --git a/src/apply.js b/src/apply.js index 2567eb87..7a07842f 100644 --- a/src/apply.js +++ b/src/apply.js @@ -50,7 +50,7 @@ import { } from 'ol/proj.js'; import {getFonts} from './text.js'; import {getTopLeft} from 'ol/extent.js'; -import {hillshade} from './shaders.js'; +import {hillshade, raster as rasterShader} from './shaders.js'; import { normalizeSourceUrl, normalizeSpriteUrl, @@ -691,6 +691,7 @@ function setupRasterSource(glSource, styleUrl, options) { } return src; }); + source.set('mapbox-source', glSource); resolve(source); }) @@ -700,7 +701,7 @@ function setupRasterSource(glSource, styleUrl, options) { }); } -function setupRasterLayer(glSource, styleUrl, options) { +function setupRasterLayerAbstract(glSource, styleUrl, options) { const layer = new TileLayer(); setupRasterSource(glSource, styleUrl, options) .then(function (source) { @@ -712,6 +713,26 @@ function setupRasterLayer(glSource, styleUrl, options) { 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 setupRasterLayer(glSource, styleUrl, options) { + const tileLayer = setupRasterLayerAbstract(glSource, styleUrl, options); + /** @type {ImageLayer} */ + const layer = new ImageLayer({ + source: new Raster({ + operationType: 'image', + operation: rasterShader, + sources: [tileLayer], + }), + }); + return layer; +} + /** * * @param {Object} glSource "source" entry from a Mapbox Style object. @@ -720,7 +741,7 @@ function setupRasterLayer(glSource, styleUrl, options) { * @return {ImageLayer} The raster layer */ function setupHillshadeLayer(glSource, styleUrl, options) { - const tileLayer = setupRasterLayer(glSource, styleUrl, options); + const tileLayer = setupRasterLayerAbstract(glSource, styleUrl, options); /** @type {ImageLayer} */ const layer = new ImageLayer({ source: new Raster({ @@ -832,33 +853,6 @@ function setupGeoJSONLayer(glSource, styleUrl, options) { }); } -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'); @@ -903,7 +897,38 @@ export function setupLayer(glStyle, styleUrl, glLayer, options) { layer.setVisible( glLayer.layout ? glLayer.layout.visibility !== 'none' : true ); - layer.on('prerender', prerenderRasterLayer(glLayer, layer, functionCache)); + layer.getSource().on('beforeoperations', function (event) { + const zoom = getZoomForResolution( + event.resolution, + options.resolutions || defaultResolutions + ); + + const data = event.data; + data.hue = getValue( + glLayer, + 'paint', + 'raster-hue-rotate', + zoom, + emptyObj, + functionCache + ); + data.opacity = ('raster-opacity' in glLayer.paint) ? getValue( + glLayer, + 'paint', + 'raster-opacity', + zoom, + emptyObj, + functionCache + ) : undefined; + data.saturation = getValue( + glLayer, + 'paint', + 'raster-saturation', + zoom, + emptyObj, + functionCache + ); + }); } else if (glSource.type == 'geojson') { layer = setupGeoJSONLayer(glSource, styleUrl, options); } else if (glSource.type == 'raster-dem' && glLayer.type == 'hillshade') { diff --git a/src/shaders.js b/src/shaders.js index edb8ee87..bc2271a1 100644 --- a/src/shaders.js +++ b/src/shaders.js @@ -180,3 +180,124 @@ export function hillshade(inputs, data) { return new ImageData(shadeData, width, height); } + +export function raster (inputs, data) { + const image = inputs[0]; + const width = image.width; + const height = image.height; + const imageData = image.data; + const shadeData = new Uint8ClampedArray(imageData.length); + const maxX = width - 1; + const maxY = height - 1; + const pixel = [0, 0, 0, 0]; + + let pixelX, + pixelY, + x0, + x1, + y0, + y1, + offset; + + // [start] from + /** + * @param {number} h + * @param {number} s + * @param {number} l + */ + function hslToRgb(h, s, l) { + let r, g, b; + + if (s === 0) { + r = g = b = l; // achromatic + } else { + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hueToRgb(p, q, h + 1/3); + g = hueToRgb(p, q, h); + b = hueToRgb(p, q, h - 1/3); + } + + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; + } + + /** + * @param {number} p + * @param {number} q + * @param {number} t + */ + function hueToRgb(p, q, t) { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1/6) return p + (q - p) * 6 * t; + if (t < 1/2) return q; + if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + } + + /** + * Converts an RGB color value to HSL. Conversion formula + * adapted from http://en.wikipedia.org/wiki/HSL_color_space. + * Assumes r, g, and b are contained in the set [0, 255] and + * returns h, s, and l in the set [0, 1]. + * + * @param {number} r The red color value + * @param {number} g The green color value + * @param {number} b The blue color value + * @return {Array} The HSL representation + */ + function rgbToHsl(r, g, b) { + (r /= 255), (g /= 255), (b /= 255); + const vmax = Math.max(r, g, b), vmin = Math.min(r, g, b); + let h, s, l = (vmax + vmin) / 2; + + if (vmax === vmin) { + return [0, 0, l]; // achromatic + } + + const d = vmax - vmin; + s = l > 0.5 ? d / (2 - vmax - vmin) : d / (vmax + vmin); + if (vmax === r) h = (g - b) / d + (g < b ? 6 : 0); + if (vmax === g) h = (b - r) / d + 2; + if (vmax === b) h = (r - g) / d + 4; + h /= 6; + + return [h, s, l]; + } + // [end] from + + const hOffset = 1 / 360 * data.hue + const sOffset = data.saturation + + for (pixelY = 0; pixelY <= maxY; ++pixelY) { + y0 = pixelY === 0 ? 0 : pixelY - 1; + y1 = pixelY === maxY ? maxY : pixelY + 1; + for (pixelX = 0; pixelX <= maxX; ++pixelX) { + x0 = pixelX === 0 ? 0 : pixelX - 1; + x1 = pixelX === maxX ? maxX : pixelX + 1; + + offset = (pixelY * width + x0) * 4; + pixel[0] = imageData[offset]; + pixel[1] = imageData[offset + 1]; + pixel[2] = imageData[offset + 2]; + pixel[3] = imageData[offset + 3]; + + let [h,s,l] = rgbToHsl(pixel[0], pixel[1], pixel[2]); + + h += hOffset; + h = h % 1; + + s += sOffset; + s = Math.max(0, Math.min(s, 1)) + + const [r, g, b] = hslToRgb(h, s, l); + shadeData[offset] = r + shadeData[offset+1] = g + shadeData[offset+2] = b + shadeData[offset+3] = data.opacity !== undefined ? data.opacity*255 : pixel[3] + } + } + + return new ImageData(shadeData, width, height); +} + From a81b5d2b599f40465fd6473cbfc179ca30bcfbc0 Mon Sep 17 00:00:00 2001 From: orangemug Date: Tue, 12 Dec 2023 13:52:12 +0000 Subject: [PATCH 2/9] Fixed some linting --- src/apply.js | 19 +++++++----- src/shaders.js | 81 +++++++++++++++++++++++++++++--------------------- 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/src/apply.js b/src/apply.js index 7a07842f..9680c147 100644 --- a/src/apply.js +++ b/src/apply.js @@ -912,14 +912,17 @@ export function setupLayer(glStyle, styleUrl, glLayer, options) { emptyObj, functionCache ); - data.opacity = ('raster-opacity' in glLayer.paint) ? getValue( - glLayer, - 'paint', - 'raster-opacity', - zoom, - emptyObj, - functionCache - ) : undefined; + data.opacity = + 'raster-opacity' in glLayer.paint + ? getValue( + glLayer, + 'paint', + 'raster-opacity', + zoom, + emptyObj, + functionCache + ) + : undefined; data.saturation = getValue( glLayer, 'paint', diff --git a/src/shaders.js b/src/shaders.js index bc2271a1..5408d801 100644 --- a/src/shaders.js +++ b/src/shaders.js @@ -181,7 +181,7 @@ export function hillshade(inputs, data) { return new ImageData(shadeData, width, height); } -export function raster (inputs, data) { +export function raster(inputs, data) { const image = inputs[0]; const width = image.width; const height = image.height; @@ -191,13 +191,7 @@ export function raster (inputs, data) { const maxY = height - 1; const pixel = [0, 0, 0, 0]; - let pixelX, - pixelY, - x0, - x1, - y0, - y1, - offset; + let pixelX, pixelY, x0, x1, y0, y1, offset; // [start] from /** @@ -207,35 +201,45 @@ export function raster (inputs, data) { */ function hslToRgb(h, s, l) { let r, g, b; - + if (s === 0) { r = g = b = l; // achromatic } else { const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; - r = hueToRgb(p, q, h + 1/3); + r = hueToRgb(p, q, h + 1 / 3); g = hueToRgb(p, q, h); - b = hueToRgb(p, q, h - 1/3); + b = hueToRgb(p, q, h - 1 / 3); } - + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; } - + /** * @param {number} p * @param {number} q * @param {number} t */ function hueToRgb(p, q, t) { - if (t < 0) t += 1; - if (t > 1) t -= 1; - if (t < 1/6) return p + (q - p) * 6 * t; - if (t < 1/2) return q; - if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; + if (t < 0) { + t += 1; + } + if (t > 1) { + t -= 1; + } + if (t < 1 / 6) { + return p + (q - p) * 6 * t; + } + if (t < 1 / 2) { + return q; + } + if (t < 2 / 3) { + return p + (q - p) * (2 / 3 - t) * 6; + } return p; } - /** + /** * Converts an RGB color value to HSL. Conversion formula * adapted from http://en.wikipedia.org/wiki/HSL_color_space. * Assumes r, g, and b are contained in the set [0, 255] and @@ -248,8 +252,11 @@ export function raster (inputs, data) { */ function rgbToHsl(r, g, b) { (r /= 255), (g /= 255), (b /= 255); - const vmax = Math.max(r, g, b), vmin = Math.min(r, g, b); - let h, s, l = (vmax + vmin) / 2; + const vmax = Math.max(r, g, b), + vmin = Math.min(r, g, b); + let h, + s, + l = (vmax + vmin) / 2; if (vmax === vmin) { return [0, 0, l]; // achromatic @@ -257,17 +264,23 @@ export function raster (inputs, data) { const d = vmax - vmin; s = l > 0.5 ? d / (2 - vmax - vmin) : d / (vmax + vmin); - if (vmax === r) h = (g - b) / d + (g < b ? 6 : 0); - if (vmax === g) h = (b - r) / d + 2; - if (vmax === b) h = (r - g) / d + 4; + if (vmax === r) { + h = (g - b) / d + (g < b ? 6 : 0); + } + if (vmax === g) { + h = (b - r) / d + 2; + } + if (vmax === b) { + h = (r - g) / d + 4; + } h /= 6; return [h, s, l]; } // [end] from - - const hOffset = 1 / 360 * data.hue - const sOffset = data.saturation + + const hOffset = (1 / 360) * data.hue; + const sOffset = data.saturation; for (pixelY = 0; pixelY <= maxY; ++pixelY) { y0 = pixelY === 0 ? 0 : pixelY - 1; @@ -282,22 +295,22 @@ export function raster (inputs, data) { pixel[2] = imageData[offset + 2]; pixel[3] = imageData[offset + 3]; - let [h,s,l] = rgbToHsl(pixel[0], pixel[1], pixel[2]); + let [h, s, l] = rgbToHsl(pixel[0], pixel[1], pixel[2]); h += hOffset; h = h % 1; s += sOffset; - s = Math.max(0, Math.min(s, 1)) + s = Math.max(0, Math.min(s, 1)); const [r, g, b] = hslToRgb(h, s, l); - shadeData[offset] = r - shadeData[offset+1] = g - shadeData[offset+2] = b - shadeData[offset+3] = data.opacity !== undefined ? data.opacity*255 : pixel[3] + shadeData[offset] = r; + shadeData[offset + 1] = g; + shadeData[offset + 2] = b; + shadeData[offset + 3] = + data.opacity !== undefined ? data.opacity * 255 : pixel[3]; } } return new ImageData(shadeData, width, height); } - From cf2091c8a38eb2363536bf0a3bd069c797279834 Mon Sep 17 00:00:00 2001 From: orangemug Date: Tue, 12 Dec 2023 14:01:51 +0000 Subject: [PATCH 3/9] Fix lint errors. --- src/shaders.js | 69 ++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/src/shaders.js b/src/shaders.js index 5408d801..3262aae9 100644 --- a/src/shaders.js +++ b/src/shaders.js @@ -191,36 +191,10 @@ export function raster(inputs, data) { const maxY = height - 1; const pixel = [0, 0, 0, 0]; - let pixelX, pixelY, x0, x1, y0, y1, offset; + let pixelX, pixelY, x0, offset; // [start] from - /** - * @param {number} h - * @param {number} s - * @param {number} l - */ - function hslToRgb(h, s, l) { - let r, g, b; - - if (s === 0) { - r = g = b = l; // achromatic - } else { - const q = l < 0.5 ? l * (1 + s) : l + s - l * s; - const p = 2 * l - q; - r = hueToRgb(p, q, h + 1 / 3); - g = hueToRgb(p, q, h); - b = hueToRgb(p, q, h - 1 / 3); - } - - return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; - } - - /** - * @param {number} p - * @param {number} q - * @param {number} t - */ - function hueToRgb(p, q, t) { + const hueToRgb = (p, q, t) => { if (t < 0) { t += 1; } @@ -237,6 +211,31 @@ export function raster(inputs, data) { return p + (q - p) * (2 / 3 - t) * 6; } return p; + }; + + /** + * @param {number} h The hue value + * @param {number} s The saturation value + * @param {number} l The lightness value + * + * @return {[number, number, number]} [r,g,b] 0-255 + */ + function hslToRgb(h, s, l) { + let r, g, b; + + if (s === 0) { + r = l; + g = l; + b = l; + } else { + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hueToRgb(p, q, h + 1 / 3); + g = hueToRgb(p, q, h); + b = hueToRgb(p, q, h - 1 / 3); + } + + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; } /** @@ -254,16 +253,15 @@ export function raster(inputs, data) { (r /= 255), (g /= 255), (b /= 255); const vmax = Math.max(r, g, b), vmin = Math.min(r, g, b); - let h, - s, - l = (vmax + vmin) / 2; + let h; + const l = (vmax + vmin) / 2; if (vmax === vmin) { return [0, 0, l]; // achromatic } const d = vmax - vmin; - s = l > 0.5 ? d / (2 - vmax - vmin) : d / (vmax + vmin); + const s = l > 0.5 ? d / (2 - vmax - vmin) : d / (vmax + vmin); if (vmax === r) { h = (g - b) / d + (g < b ? 6 : 0); } @@ -283,11 +281,8 @@ export function raster(inputs, data) { const sOffset = data.saturation; for (pixelY = 0; pixelY <= maxY; ++pixelY) { - y0 = pixelY === 0 ? 0 : pixelY - 1; - y1 = pixelY === maxY ? maxY : pixelY + 1; for (pixelX = 0; pixelX <= maxX; ++pixelX) { x0 = pixelX === 0 ? 0 : pixelX - 1; - x1 = pixelX === maxX ? maxX : pixelX + 1; offset = (pixelY * width + x0) * 4; pixel[0] = imageData[offset]; @@ -295,7 +290,9 @@ export function raster(inputs, data) { pixel[2] = imageData[offset + 2]; pixel[3] = imageData[offset + 3]; - let [h, s, l] = rgbToHsl(pixel[0], pixel[1], pixel[2]); + const hsl = rgbToHsl(pixel[0], pixel[1], pixel[2]); + let [h, s] = hsl; + const l = hsl[2]; h += hOffset; h = h % 1; From 09846111990da4cd70c923619bd43dabb93da513 Mon Sep 17 00:00:00 2001 From: orangemug Date: Thu, 14 Dec 2023 16:55:33 +0000 Subject: [PATCH 4/9] Allow for missing 'paint' prop --- src/apply.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apply.js b/src/apply.js index 9680c147..335e333f 100644 --- a/src/apply.js +++ b/src/apply.js @@ -913,7 +913,7 @@ export function setupLayer(glStyle, styleUrl, glLayer, options) { functionCache ); data.opacity = - 'raster-opacity' in glLayer.paint + glLayer.paint && 'raster-opacity' in glLayer.paint ? getValue( glLayer, 'paint', From 03997805e1a0f77aa955dfcd5cb3f80d73511460 Mon Sep 17 00:00:00 2001 From: orangemug Date: Thu, 14 Dec 2023 17:50:31 +0000 Subject: [PATCH 5/9] Fix so you can use multiple raster layers of the same source. --- src/apply.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/apply.js b/src/apply.js index 335e333f..b513334d 100644 --- a/src/apply.js +++ b/src/apply.js @@ -1061,7 +1061,11 @@ function processStyle(glStyle, mapOrGroup, styleUrl, options) { } else { id = glLayer.source || getSourceIdByRef(glLayers, glLayer.ref); // this technique assumes gl layers will be in a particular order - if (!id || id != glSourceId) { + if ( + // This line is because rasters set properties on their source + glLayer.type === "raster" || + !id || id != glSourceId + ) { if (layerIds.length) { promises.push( finalizeLayer( From fb2b4d4fbd7ce2525c3d755e2eede83afd6255ec Mon Sep 17 00:00:00 2001 From: orangemug Date: Thu, 14 Dec 2023 18:01:31 +0000 Subject: [PATCH 6/9] Remove support for raster-saturation for now as its not right. --- src/shaders.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shaders.js b/src/shaders.js index 3262aae9..9b653916 100644 --- a/src/shaders.js +++ b/src/shaders.js @@ -278,7 +278,7 @@ export function raster(inputs, data) { // [end] from const hOffset = (1 / 360) * data.hue; - const sOffset = data.saturation; + // const sOffset = data.saturation; for (pixelY = 0; pixelY <= maxY; ++pixelY) { for (pixelX = 0; pixelX <= maxX; ++pixelX) { @@ -297,8 +297,8 @@ export function raster(inputs, data) { h += hOffset; h = h % 1; - s += sOffset; - s = Math.max(0, Math.min(s, 1)); + // s += sOffset; + // s = Math.max(0, Math.min(s, 1)); const [r, g, b] = hslToRgb(h, s, l); shadeData[offset] = r; From a3586eecdf878ed09af47ab19b7bfeed36701e19 Mon Sep 17 00:00:00 2001 From: orangemug Date: Thu, 14 Dec 2023 18:03:31 +0000 Subject: [PATCH 7/9] Fix lint errors. --- src/apply.js | 5 +++-- src/shaders.js | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/apply.js b/src/apply.js index b513334d..b75dee3f 100644 --- a/src/apply.js +++ b/src/apply.js @@ -1063,8 +1063,9 @@ function processStyle(glStyle, mapOrGroup, styleUrl, options) { // this technique assumes gl layers will be in a particular order if ( // This line is because rasters set properties on their source - glLayer.type === "raster" || - !id || id != glSourceId + glLayer.type === 'raster' || + !id || + id != glSourceId ) { if (layerIds.length) { promises.push( diff --git a/src/shaders.js b/src/shaders.js index 9b653916..3110b48f 100644 --- a/src/shaders.js +++ b/src/shaders.js @@ -291,7 +291,8 @@ export function raster(inputs, data) { pixel[3] = imageData[offset + 3]; const hsl = rgbToHsl(pixel[0], pixel[1], pixel[2]); - let [h, s] = hsl; + let h = hsl[0]; + const s = hsl[1]; const l = hsl[2]; h += hOffset; From f31c8d899f9b02c859e3f0e7a830f1e58764dd8c Mon Sep 17 00:00:00 2001 From: orangemug Date: Mon, 18 Dec 2023 16:00:26 +0000 Subject: [PATCH 8/9] Made raster operations work the same as maplibre. --- src/apply.js | 43 ++++++++++++--- src/shaders.js | 146 ++++++++++++++++++++----------------------------- 2 files changed, 93 insertions(+), 96 deletions(-) diff --git a/src/apply.js b/src/apply.js index 0b2c1c20..46b0bd6f 100644 --- a/src/apply.js +++ b/src/apply.js @@ -902,14 +902,6 @@ export function setupLayer(glStyle, styleUrl, glLayer, options) { ); const data = event.data; - data.hue = getValue( - glLayer, - 'paint', - 'raster-hue-rotate', - zoom, - emptyObj, - functionCache - ); data.opacity = glLayer.paint && 'raster-opacity' in glLayer.paint ? getValue( @@ -929,6 +921,41 @@ export function setupLayer(glStyle, styleUrl, glLayer, options) { emptyObj, functionCache ); + data.contrast = getValue( + glLayer, + 'paint', + 'raster-contrast', + zoom, + emptyObj, + functionCache + ); + + data.brightnessHigh = getValue( + glLayer, + 'paint', + 'raster-brightness-max', + zoom, + emptyObj, + functionCache + ); + + data.brightnessLow = getValue( + glLayer, + 'paint', + 'raster-brightness-min', + zoom, + emptyObj, + functionCache + ); + + data.hueRotate = getValue( + glLayer, + 'paint', + 'raster-hue-rotate', + zoom, + emptyObj, + functionCache + ); }); } else if (glSource.type == 'geojson') { layer = setupGeoJSONLayer(glSource, styleUrl, options); diff --git a/src/shaders.js b/src/shaders.js index 3110b48f..21ff1366 100644 --- a/src/shaders.js +++ b/src/shaders.js @@ -193,92 +193,41 @@ export function raster(inputs, data) { let pixelX, pixelY, x0, offset; - // [start] from - const hueToRgb = (p, q, t) => { - if (t < 0) { - t += 1; - } - if (t > 1) { - t -= 1; - } - if (t < 1 / 6) { - return p + (q - p) * 6 * t; - } - if (t < 1 / 2) { - return q; - } - if (t < 2 / 3) { - return p + (q - p) * (2 / 3 - t) * 6; - } - return p; - }; - - /** - * @param {number} h The hue value - * @param {number} s The saturation value - * @param {number} l The lightness value - * - * @return {[number, number, number]} [r,g,b] 0-255 + /* + * The following functions have the same math as + * - calculateContrastFactor + * - calculateSaturationFactor + * - generateSpinWeights */ - function hslToRgb(h, s, l) { - let r, g, b; - - if (s === 0) { - r = l; - g = l; - b = l; - } else { - const q = l < 0.5 ? l * (1 + s) : l + s - l * s; - const p = 2 * l - q; - r = hueToRgb(p, q, h + 1 / 3); - g = hueToRgb(p, q, h); - b = hueToRgb(p, q, h - 1 / 3); - } + function calculateContrastFactor(contrast) { + return contrast > 0 ? 1 / (1 - contrast) : 1 + contrast; + } - return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; + function calculateSaturationFactor(saturation) { + return saturation > 0 ? 1 - 1 / (1.001 - saturation) : -saturation; } - /** - * Converts an RGB color value to HSL. Conversion formula - * adapted from http://en.wikipedia.org/wiki/HSL_color_space. - * Assumes r, g, and b are contained in the set [0, 255] and - * returns h, s, and l in the set [0, 1]. - * - * @param {number} r The red color value - * @param {number} g The green color value - * @param {number} b The blue color value - * @return {Array} The HSL representation - */ - function rgbToHsl(r, g, b) { - (r /= 255), (g /= 255), (b /= 255); - const vmax = Math.max(r, g, b), - vmin = Math.min(r, g, b); - let h; - const l = (vmax + vmin) / 2; - - if (vmax === vmin) { - return [0, 0, l]; // achromatic - } + function generateSpinWeights(angle) { + angle *= Math.PI / 180; + const s = Math.sin(angle); + const c = Math.cos(angle); + return [ + (2 * c + 1) / 3, + (-Math.sqrt(3) * s - c + 1) / 3, + (Math.sqrt(3) * s - c + 1) / 3, + ]; + } - const d = vmax - vmin; - const s = l > 0.5 ? d / (2 - vmax - vmin) : d / (vmax + vmin); - if (vmax === r) { - h = (g - b) / d + (g < b ? 6 : 0); - } - if (vmax === g) { - h = (b - r) / d + 2; - } - if (vmax === b) { - h = (r - g) / d + 4; - } - h /= 6; + const sFactor = calculateSaturationFactor(data.saturation); + const cFactor = calculateContrastFactor(data.contrast); - return [h, s, l]; - } - // [end] from + const cSpinWeights = generateSpinWeights(data.hueRotate); + const cSpinWeightsXYZ = cSpinWeights; + const cSpinWeightsZXY = [cSpinWeights[2], cSpinWeights[0], cSpinWeights[1]]; + const cSpinWeightsYZX = [cSpinWeights[1], cSpinWeights[2], cSpinWeights[0]]; - const hOffset = (1 / 360) * data.hue; - // const sOffset = data.saturation; + const bLow = data.brightnessLow; + const bHigh = data.brightnessHigh; for (pixelY = 0; pixelY <= maxY; ++pixelY) { for (pixelX = 0; pixelX <= maxX; ++pixelX) { @@ -290,18 +239,39 @@ export function raster(inputs, data) { pixel[2] = imageData[offset + 2]; pixel[3] = imageData[offset + 3]; - const hsl = rgbToHsl(pixel[0], pixel[1], pixel[2]); - let h = hsl[0]; - const s = hsl[1]; - const l = hsl[2]; + const or = pixel[0]; + const og = pixel[1]; + const ob = pixel[2]; + + const dotProduct = (vector1, vector2) => { + let result = 0; + for (let i = 0; i < vector1.length; i++) { + result += vector1[i] * vector2[i]; + } + return result; + }; + + // hue-rotate + let r = dotProduct([or, og, ob], cSpinWeightsXYZ); + let g = dotProduct([or, og, ob], cSpinWeightsZXY); + let b = dotProduct([or, og, ob], cSpinWeightsYZX); + + // saturation + const average = (r + g + b) / 3; + r += (average - r) * sFactor; + g += (average - g) * sFactor; + b += (average - b) * sFactor; - h += hOffset; - h = h % 1; + // contrast + r = (r - 0.5) * cFactor + 0.5; + g = (g - 0.5) * cFactor + 0.5; + b = (b - 0.5) * cFactor + 0.5; - // s += sOffset; - // s = Math.max(0, Math.min(s, 1)); + // brightness + r = bLow * (1 - r) + bHigh * r; + g = bLow * (1 - r) + bHigh * g; + b = bLow * (1 - r) + bHigh * b; - const [r, g, b] = hslToRgb(h, s, l); shadeData[offset] = r; shadeData[offset + 1] = g; shadeData[offset + 2] = b; From d903086260c524595a18f27d5bcd9058aecfbf3b Mon Sep 17 00:00:00 2001 From: orangemug Date: Tue, 19 Dec 2023 13:44:05 +0000 Subject: [PATCH 9/9] Restore original layer setup for raster layers with out special paint props --- src/apply.js | 171 +++++++++++++++++++++++++++++++------------------ src/shaders.js | 3 +- 2 files changed, 110 insertions(+), 64 deletions(-) diff --git a/src/apply.js b/src/apply.js index 46b0bd6f..06dff9fd 100644 --- a/src/apply.js +++ b/src/apply.js @@ -716,9 +716,21 @@ function setupRasterLayerAbstract(glSource, styleUrl, options) { * @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 + * @return {TileLayer} The raster layer */ function setupRasterLayer(glSource, styleUrl, options) { + const tileLayer = setupRasterLayerAbstract(glSource, styleUrl, options); + return tileLayer; +} + +/** + * + * @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 setupRasterOpLayer(glSource, styleUrl, options) { const tileLayer = setupRasterLayerAbstract(glSource, styleUrl, options); /** @type {ImageLayer} */ const layer = new ImageLayer({ @@ -851,6 +863,33 @@ function setupGeoJSONLayer(glSource, styleUrl, options) { }); } +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'); @@ -891,72 +930,80 @@ export function setupLayer(glStyle, styleUrl, glLayer, options) { } 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 + const keys = [ + 'raster-saturation', + 'raster-contrast', + 'raster-brightness-max', + 'raster-brightness-min', + 'raster-hue-rotate', + ]; + const requiresOperations = !!Object.keys(glLayer.paint || {}).find( + (key) => { + return keys.includes(key); + } ); - layer.getSource().on('beforeoperations', function (event) { - const zoom = getZoomForResolution( - event.resolution, - options.resolutions || defaultResolutions - ); - const data = event.data; - data.opacity = - glLayer.paint && 'raster-opacity' in glLayer.paint - ? getValue( - glLayer, - 'paint', - 'raster-opacity', - zoom, - emptyObj, - functionCache - ) - : undefined; - data.saturation = getValue( - glLayer, - 'paint', - 'raster-saturation', - zoom, - emptyObj, - functionCache - ); - data.contrast = getValue( - glLayer, - 'paint', - 'raster-contrast', - zoom, - emptyObj, - functionCache - ); + if (requiresOperations) { + layer = setupRasterOpLayer(glSource, styleUrl, options); + layer.getSource().on('beforeoperations', function (event) { + const zoom = getZoomForResolution( + event.resolution, + options.resolutions || defaultResolutions + ); - data.brightnessHigh = getValue( - glLayer, - 'paint', - 'raster-brightness-max', - zoom, - emptyObj, - functionCache - ); + const data = event.data; + data.saturation = getValue( + glLayer, + 'paint', + 'raster-saturation', + zoom, + emptyObj, + functionCache + ); + data.contrast = getValue( + glLayer, + 'paint', + 'raster-contrast', + zoom, + emptyObj, + functionCache + ); - data.brightnessLow = getValue( - glLayer, - 'paint', - 'raster-brightness-min', - zoom, - emptyObj, - functionCache - ); + data.brightnessHigh = getValue( + glLayer, + 'paint', + 'raster-brightness-max', + zoom, + emptyObj, + functionCache + ); - data.hueRotate = getValue( - glLayer, - 'paint', - 'raster-hue-rotate', - zoom, - emptyObj, - functionCache - ); - }); + data.brightnessLow = getValue( + glLayer, + 'paint', + 'raster-brightness-min', + zoom, + emptyObj, + functionCache + ); + + data.hueRotate = getValue( + glLayer, + 'paint', + 'raster-hue-rotate', + zoom, + emptyObj, + functionCache + ); + }); + } else { + 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') { diff --git a/src/shaders.js b/src/shaders.js index 21ff1366..bc18aa89 100644 --- a/src/shaders.js +++ b/src/shaders.js @@ -275,8 +275,7 @@ export function raster(inputs, data) { shadeData[offset] = r; shadeData[offset + 1] = g; shadeData[offset + 2] = b; - shadeData[offset + 3] = - data.opacity !== undefined ? data.opacity * 255 : pixel[3]; + shadeData[offset + 3] = pixel[3]; } }