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
117 changes: 112 additions & 5 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 @@ -689,6 +689,7 @@ function setupRasterSource(glSource, styleUrl, options) {
}
return src;
});

source.set('mapbox-source', glSource);
resolve(source);
})
Expand All @@ -698,7 +699,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 @@ -710,6 +711,38 @@ 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 {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<Raster>} The raster layer
*/
function setupRasterOpLayer(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 @@ -718,7 +751,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 @@ -897,10 +930,79 @@ 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);
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);
}
);

if (requiresOperations) {
layer = setupRasterOpLayer(glSource, styleUrl, options);
layer.getSource().on('beforeoperations', function (event) {
const zoom = getZoomForResolution(
event.resolution,
options.resolutions || defaultResolutions
);

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.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 {
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);
Expand Down Expand Up @@ -1031,7 +1133,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
101 changes: 101 additions & 0 deletions src/shaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,104 @@ 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;

/*
* The following functions have the same math as <https://github.com/maplibre/maplibre-gl-js/blob/5518ede00ef769fed1ca4f54f6d970885987fb22/src/render/program/raster_program.ts#L76>
* - calculateContrastFactor
* - calculateSaturationFactor
* - generateSpinWeights
*/
function calculateContrastFactor(contrast) {
return contrast > 0 ? 1 / (1 - contrast) : 1 + contrast;
}

function calculateSaturationFactor(saturation) {
return saturation > 0 ? 1 - 1 / (1.001 - saturation) : -saturation;
}

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 sFactor = calculateSaturationFactor(data.saturation);
const cFactor = calculateContrastFactor(data.contrast);

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 bLow = data.brightnessLow;
const bHigh = data.brightnessHigh;

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 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;

// contrast
r = (r - 0.5) * cFactor + 0.5;
g = (g - 0.5) * cFactor + 0.5;
b = (b - 0.5) * cFactor + 0.5;

// brightness
r = bLow * (1 - r) + bHigh * r;
g = bLow * (1 - r) + bHigh * g;
b = bLow * (1 - r) + bHigh * b;

shadeData[offset] = r;
shadeData[offset + 1] = g;
shadeData[offset + 2] = b;
shadeData[offset + 3] = pixel[3];
}
}

return new ImageData(shadeData, width, height);
}