Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added raster-(hue-rotate/contrast/saturation/opacity/brightness-*/) as operations #1055

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
97 changes: 65 additions & 32 deletions src/apply.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -691,6 +691,7 @@ function setupRasterSource(glSource, styleUrl, options) {
}
return src;
});

source.set('mapbox-source', glSource);
resolve(source);
})
Expand All @@ -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) {
Expand All @@ -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<Raster>} The raster layer
*/
function setupRasterLayer(glSource, styleUrl, options) {
const tileLayer = setupRasterLayerAbstract(glSource, styleUrl, options);
/** @type {ImageLayer<Raster>} */
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.
Expand All @@ -720,7 +741,7 @@ function setupRasterLayer(glSource, styleUrl, options) {
* @return {ImageLayer<Raster>} The raster layer
*/
function setupHillshadeLayer(glSource, styleUrl, options) {
const tileLayer = setupRasterLayer(glSource, styleUrl, options);
const tileLayer = setupRasterLayerAbstract(glSource, styleUrl, options);
/** @type {ImageLayer<Raster>} */
const layer = new ImageLayer({
source: new Raster({
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -903,7 +897,41 @@ 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 =
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
);
});
} else if (glSource.type == 'geojson') {
layer = setupGeoJSONLayer(glSource, styleUrl, options);
} else if (glSource.type == 'raster-dem' && glLayer.type == 'hillshade') {
Expand Down Expand Up @@ -1033,7 +1061,12 @@ 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' ||
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the line I'm most unsure about. We need this because is we have two layers with the same source they need to apply different operations to their sources.

!id ||
id != glSourceId
) {
if (layerIds.length) {
promises.push(
finalizeLayer(
Expand Down
132 changes: 132 additions & 0 deletions src/shaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,135 @@ 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, offset;

// [start] from <https://stackoverflow.com/a/9493060>
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
*/
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)];
}

/**
* 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
}

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;

return [h, s, l];
}
// [end] from <https://stackoverflow.com/a/9493060>

const hOffset = (1 / 360) * data.hue;
// const sOffset = data.saturation;

for (pixelY = 0; pixelY <= maxY; ++pixelY) {
for (pixelX = 0; pixelX <= maxX; ++pixelX) {
x0 = pixelX === 0 ? 0 : 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];

const hsl = rgbToHsl(pixel[0], pixel[1], pixel[2]);
let h = hsl[0];
const s = hsl[1];
const l = hsl[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);
}
Loading