ol
Version:
OpenLayers mapping library
986 lines (903 loc) • 32.9 kB
JavaScript
/**
* @module ol/render/webgl/VectorStyleRenderer
*/
import Disposable from '../../Disposable.js';
import {createCanvasContext2D} from '../../dom.js';
import {
create as createTransform,
makeInverse as makeInverseTransform,
} from '../../transform.js';
import {ARRAY_BUFFER, DYNAMIC_DRAW, ELEMENT_ARRAY_BUFFER} from '../../webgl.js';
import WebGLArrayBuffer from '../../webgl/Buffer.js';
import {AttributeType} from '../../webgl/Helper.js';
import LabelsArray from '../../webgl/LabelsArray.js';
import {create as createTextOverlayWorker} from '../../worker/textOverlay.js';
import {create as createWebGLWorker} from '../../worker/webgl.js';
import {
TextOverlayWorkerMessageType,
WebGLWorkerMessageType,
} from './constants.js';
import {colorEncodeIdAndPack} from './encodeUtil.js';
import {
generateLineStringRenderInstructions,
generatePointRenderInstructions,
generatePolygonRenderInstructions,
getCustomAttributesSize,
} from './renderinstructions.js';
import {serializeFrameState} from './serialize.js';
import {parseLiteralStyle} from './style.js';
import {hasTextStyle} from './textUtil.js';
const tmpColor = [];
/** @type {Worker|undefined} */
let WEBGL_WORKER;
function getWebGLWorker() {
if (!WEBGL_WORKER) {
WEBGL_WORKER = createWebGLWorker();
}
return WEBGL_WORKER;
}
let workerMessageCounter = 0;
/**
*
* @param {Worker} worker Worker to send the message to
* @param {Object} message Message
* @param {Array<Transferable>} [transferables] Transferables
* @return {Promise<Object>} Response received by the worker
*/
function messageWorker(worker, message, transferables) {
const messageId = workerMessageCounter++;
if (transferables) {
worker.postMessage({...message, id: messageId}, transferables);
} else {
worker.postMessage({...message, id: messageId});
}
return new Promise((resolve) => {
const handleMessage = (event) => {
const received = event.data;
// this is not the response to our request: skip
if (received.id !== messageId) {
return;
}
// we've received our response: stop listening
worker.removeEventListener('message', handleMessage);
resolve(received);
};
worker.addEventListener('message', handleMessage);
});
}
/**
* Names of attributes made available to the vertex shader.
* Please note: changing these *will* break custom shaders!
* @enum {string}
*/
export const Attributes = {
POSITION: 'a_position',
LOCAL_POSITION: 'a_localPosition',
SEGMENT_START: 'a_segmentStart',
SEGMENT_END: 'a_segmentEnd',
MEASURE_START: 'a_measureStart',
MEASURE_END: 'a_measureEnd',
ANGLE_TANGENT_SUM: 'a_angleTangentSum',
JOIN_ANGLES: 'a_joinAngles',
DISTANCE_LOW: 'a_distanceLow',
DISTANCE_HIGH: 'a_distanceHigh',
};
/**
* @typedef {Object} AttributeDefinition A description of a custom attribute to be passed on to the GPU, with a value different
* for each feature.
* @property {number} [size] Amount of numerical values composing the attribute, either 1, 2, 3 or 4; in case size is > 1, the return value
* of the callback should be an array; if unspecified, assumed to be a single float value
* @property {function(this:import("./MixedGeometryBatch.js").GeometryBatchItem, import("../../Feature.js").FeatureLike):number|Array<number>} callback This callback computes the numerical value of the
* attribute for a given feature.
*/
/**
* @typedef {Object<string, AttributeDefinition>} AttributeDefinitions
* @typedef {Object<string, import("../../webgl/Helper.js").UniformValue>} UniformDefinitions
*/
/**
* @typedef {Array<WebGLArrayBuffer>} WebGLArrayBufferSet Buffers organized like so: [indicesBuffer, vertexAttributesBuffer, instanceAttributesBuffer]
*/
/**
* @typedef {Object} WebGLBuffers
* Anything set to null means there's nothing to render for that category.
* @property {WebGLArrayBufferSet|null} polygonBuffers Array containing indices and vertices buffers for polygons
* @property {WebGLArrayBufferSet|null} lineStringBuffers Array containing indices and vertices buffers for line strings
* @property {WebGLArrayBufferSet|null} pointBuffers Array containing indices and vertices buffers for points
* @property {string|null} textInstructionsKey Key corresponding to a text instructions set
* @property {import("../../transform.js").Transform} invertVerticesTransform Inverse of the transform applied when generating buffers
*/
/**
* @typedef {Object} RenderInstructions
* @property {Float32Array|null} polygonInstructions Polygon instructions; null if nothing to render
* @property {Float32Array|null} lineStringInstructions LineString instructions; null if nothing to render
* @property {Float32Array|null} pointInstructions Point instructions; null if nothing to render
*/
/**
* @typedef {Object} ShaderProgram An object containing both shaders (vertex and fragment)
* @property {string} vertex Vertex shader source
* @property {string} fragment Fragment shader source
*/
/**
* @typedef {import('./style.js').StyleParseResult} StyleShaders
*/
/**
* @typedef {import('../../style/flat.js').FlatStyleLike} FlatStyleLike
*/
/**
* @typedef {import('../../style/flat.js').FlatStyle} FlatStyle
*/
/**
* @typedef {import('../../style/flat.js').Rule} FlatStyleRule
*/
/**
* @typedef {Object} SubRenderPass
* @property {string} vertexShader Vertex shader
* @property {string} fragmentShader Fragment shader
* @property {Array<import('../../webgl/Helper.js').AttributeDescription>} attributesDesc Attributes description, defined for each primitive vertex
* @property {Array<import('../../webgl/Helper.js').AttributeDescription>} instancedAttributesDesc Attributes description, defined once per primitive
* @property {number} instancePrimitiveVertexCount Number of vertices per instance primitive in this render pass
* @property {WebGLProgram} [program] Program; this has to be recreated if the helper is lost/changed
*/
/**
* @typedef {Object} RenderPass
* @property {SubRenderPass} [fillRenderPass] Fill render pass; undefined if no fill in pass
* @property {SubRenderPass} [strokeRenderPass] Stroke render pass; undefined if no stroke in pass
* @property {SubRenderPass} [symbolRenderPass] Symbol render pass; undefined if no symbol in pass
*/
/**
* @classdesc This class is responsible for:
* 1. generating WebGL buffers according to a provided style, using a MixedGeometryBatch as input
* 2. rendering geometries contained in said buffers
*
* A VectorStyleRenderer instance can be created either from a literal style or from shaders.
* The shaders should not be provided explicitly but instead as a preconfigured ShaderBuilder instance.
*
* The `generateBuffers` method returns a promise resolving to WebGL buffers that are intended to be rendered by the
* same renderer.
*/
class VectorStyleRenderer extends Disposable {
/**
* @param {FlatStyleLike|StyleShaders|Array<StyleShaders>} styles Vector styles expressed as flat styles, flat style rules or style shaders
* @param {import('../../style/flat.js').StyleVariables} variables Style variables
* @param {import('../../webgl/Helper.js').default} helper Helper
* @param {boolean} [enableHitDetection] Whether to enable the hit detection (needs compatible shader)
*/
constructor(styles, variables, helper, enableHitDetection) {
super();
/**
* @private
* @type {import('../../webgl/Helper.js').default}
*/
this.helper_;
/**
* @private
*/
this.hitDetectionEnabled_ = !!enableHitDetection;
/**
* Flat style like; if shaders are given as input, will use the `sourceRule` property of the shaders
* `null` if no Flat style equivalent is available (e.g. custom-made shaders); in that case no text rendering will happen
* @type {FlatStyleLike|null}
*/
this.flatStyle = toFlatStyleLike(styles);
/**
* @type {Array<StyleShaders>}
* @private
*/
this.styleShaders = convertStyleToShaders(styles, variables);
/**
* @type {AttributeDefinitions}
* @private
*/
this.customAttributes_ = {};
/**
@type {UniformDefinitions}
* @private
*/
this.uniforms_ = {};
// add hit detection attribute if enabled
if (this.hitDetectionEnabled_) {
this.customAttributes_['hitColor'] = {
callback() {
return colorEncodeIdAndPack(this.ref, tmpColor);
},
size: 2,
};
}
// add attributes & uniforms coming from all shaders
for (const styleShader of this.styleShaders) {
for (const attributeName in styleShader.attributes) {
if (attributeName in this.customAttributes_) {
// already defined: skip
continue;
}
this.customAttributes_[attributeName] =
styleShader.attributes[attributeName];
}
for (const uniformName in styleShader.uniforms) {
if (uniformName in this.uniforms_) {
// already defined: skip
continue;
}
this.uniforms_[uniformName] = styleShader.uniforms[uniformName];
}
}
// create a render pass for each shader
/**
* @type {Array<RenderPass>}
* @private
*/
this.renderPasses_ = this.styleShaders.map((styleShader) => {
/** @type {RenderPass} */
const renderPass = {};
const customAttributesDesc = Object.entries(this.customAttributes_).map(
([name, value]) => {
const isUsed = name in styleShader.attributes || name === 'hitColor';
return {
name: isUsed ? `a_${name}` : null, // giving a null name means this is only used for "spacing" in between attributes
size: value.size || 1,
type: AttributeType.FLOAT,
};
},
);
// set up each subpass
if (styleShader.builder.getFillVertexShader()) {
renderPass.fillRenderPass = {
vertexShader: styleShader.builder.getFillVertexShader(),
fragmentShader: styleShader.builder.getFillFragmentShader(),
attributesDesc: [
{
name: Attributes.POSITION,
size: 2,
type: AttributeType.FLOAT,
},
...customAttributesDesc,
],
instancedAttributesDesc: [], // no instanced rendering for polygons
instancePrimitiveVertexCount: 3,
};
}
if (styleShader.builder.getStrokeVertexShader()) {
renderPass.strokeRenderPass = {
vertexShader: styleShader.builder.getStrokeVertexShader(),
fragmentShader: styleShader.builder.getStrokeFragmentShader(),
attributesDesc: [
{
name: Attributes.LOCAL_POSITION,
size: 2,
type: AttributeType.FLOAT,
},
],
instancedAttributesDesc: [
{
name: Attributes.SEGMENT_START,
size: 2,
type: AttributeType.FLOAT,
},
{
name: Attributes.MEASURE_START,
size: 1,
type: AttributeType.FLOAT,
},
{
name: Attributes.SEGMENT_END,
size: 2,
type: AttributeType.FLOAT,
},
{
name: Attributes.MEASURE_END,
size: 1,
type: AttributeType.FLOAT,
},
{
name: Attributes.JOIN_ANGLES,
size: 2,
type: AttributeType.FLOAT,
},
{
name: Attributes.DISTANCE_LOW,
size: 1,
type: AttributeType.FLOAT,
},
{
name: Attributes.DISTANCE_HIGH,
size: 1,
type: AttributeType.FLOAT,
},
{
name: Attributes.ANGLE_TANGENT_SUM,
size: 1,
type: AttributeType.FLOAT,
},
...customAttributesDesc,
],
instancePrimitiveVertexCount: 6,
};
}
if (styleShader.builder.getSymbolVertexShader()) {
renderPass.symbolRenderPass = {
vertexShader: styleShader.builder.getSymbolVertexShader(),
fragmentShader: styleShader.builder.getSymbolFragmentShader(),
attributesDesc: [
{
name: Attributes.LOCAL_POSITION,
size: 2,
type: AttributeType.FLOAT,
},
],
instancedAttributesDesc: [
{
name: Attributes.POSITION,
size: 2,
type: AttributeType.FLOAT,
},
...customAttributesDesc,
],
instancePrimitiveVertexCount: 6,
};
}
return renderPass;
});
this.hasFill_ = this.renderPasses_.some((pass) => pass.fillRenderPass);
this.hasStroke_ = this.renderPasses_.some((pass) => pass.strokeRenderPass);
this.hasSymbol_ = this.renderPasses_.some((pass) => pass.symbolRenderPass);
this.hasText_ = this.flatStyle && hasTextStyle(this.flatStyle);
if (this.hasText_) {
/**
* @private
*/
this.textOverlayCanvas_ = /** @type {HTMLCanvasElement} */ (
createCanvasContext2D().canvas
);
/**
* @private
*/
this.textOverlayContext_ = this.textOverlayCanvas_.getContext('2d');
/**
* @type {import("../../Map.js").FrameState}
* @private
*/
this.textOverlayRenderFrameState_ = null;
/**
* @type {Worker}
* @private
*/
this.textOverlayWorker_ = createTextOverlayWorker();
/** @type {Set<string>} */
this.textOverlayRenderList_ = new Set();
}
// this will initialize render passes with the given helper
this.setHelper(helper);
}
/**
* @param {import('./MixedGeometryBatch.js').default} geometryBatch Geometry batch
* @param {import("../../transform.js").Transform} transform Transform to apply to coordinates
* @param {number} resolution View resolution; used for text render instructions if any
* @return {Promise<WebGLBuffers>} A promise resolving to WebGL buffers; buffer sets are set to `null` if nothing to render
*/
async generateBuffers(geometryBatch, transform, resolution) {
// also return the inverse of the transform that was applied when generating buffers
const invertVerticesTransform = makeInverseTransform(
createTransform(),
transform,
);
if (geometryBatch.isEmpty()) {
return {
polygonBuffers: null,
lineStringBuffers: null,
pointBuffers: null,
invertVerticesTransform: invertVerticesTransform,
textInstructionsKey: null,
};
}
const labelsArray = new LabelsArray();
const renderInstructions = this.generateRenderInstructions_(
geometryBatch,
labelsArray,
transform,
);
const [
textInstructionsKey,
polygonBuffers,
lineStringBuffers,
pointBuffers,
] = await Promise.all([
this.hasText_
? this.generateTextInstructions_(
renderInstructions,
labelsArray,
transform,
resolution,
)
: null,
this.hasFill_
? this.generateBuffersForType_(
renderInstructions.polygonInstructions,
'Polygon',
transform,
)
: null,
this.hasStroke_
? this.generateBuffersForType_(
renderInstructions.lineStringInstructions,
'LineString',
transform,
)
: null,
this.hasSymbol_
? this.generateBuffersForType_(
renderInstructions.pointInstructions,
'Point',
transform,
)
: null,
]);
return {
polygonBuffers: polygonBuffers,
lineStringBuffers: lineStringBuffers,
pointBuffers: pointBuffers,
invertVerticesTransform: invertVerticesTransform,
textInstructionsKey,
};
}
/**
* @param {import('./MixedGeometryBatch.js').default} geometryBatch Geometry batch
* @param {LabelsArray} labelsArray Labels array
* @param {import("../../transform.js").Transform} transform Transform to apply to coordinates
* @return {RenderInstructions} Render instructions
* @private
*/
generateRenderInstructions_(geometryBatch, labelsArray, transform) {
const polygonInstructions =
this.hasFill_ || this.hasText_ // if we do text rendering we need render instructions for all geometry types
? generatePolygonRenderInstructions(
geometryBatch.polygonBatch,
new Float32Array(0),
labelsArray,
this.customAttributes_,
transform,
)
: null;
const lineStringInstructions =
this.hasStroke_ || this.hasText_
? generateLineStringRenderInstructions(
geometryBatch.lineStringBatch,
new Float32Array(0),
labelsArray,
this.customAttributes_,
transform,
)
: null;
const pointInstructions =
this.hasSymbol_ || this.hasText_
? generatePointRenderInstructions(
geometryBatch.pointBatch,
new Float32Array(0),
labelsArray,
this.customAttributes_,
transform,
)
: null;
return {
polygonInstructions,
lineStringInstructions,
pointInstructions,
};
}
/**
* @param {Float32Array|null} renderInstructions Render instructions
* @param {import("../../geom/Geometry.js").Type} geometryType Geometry type
* @param {import("../../transform.js").Transform} transform Transform to apply to coordinates
* @return {Promise<WebGLArrayBufferSet>|null} Indices buffer and vertices buffer; null if nothing to render
* @private
*/
generateBuffersForType_(renderInstructions, geometryType, transform) {
if (renderInstructions === null) {
return null;
}
let messageType;
switch (geometryType) {
case 'Polygon':
messageType = WebGLWorkerMessageType.GENERATE_POLYGON_BUFFERS;
break;
case 'LineString':
messageType = WebGLWorkerMessageType.GENERATE_LINE_STRING_BUFFERS;
break;
case 'Point':
messageType = WebGLWorkerMessageType.GENERATE_POINT_BUFFERS;
break;
default:
// pass
}
/** @type {import('./constants.js').WebGLWorkerGenerateBuffersMessage} */
const message = {
type: messageType,
renderInstructions: renderInstructions.buffer,
renderInstructionsTransform: transform,
customAttributesSize: getCustomAttributesSize(this.customAttributes_),
};
return messageWorker(getWebGLWorker(), message, [
renderInstructions.buffer,
]).then((data) => {
// the helper has disposed in the meantime; the promise will not be resolved
if (!this.helper_.getGL()) {
return;
}
const received =
/** @type {import('./constants.js').WebGLWorkerGenerateBuffersMessage} */ (
data
);
// copy & flush received buffers to GPU
const indicesBuffer = new WebGLArrayBuffer(
ELEMENT_ARRAY_BUFFER,
DYNAMIC_DRAW,
).fromArrayBuffer(received.indicesBuffer);
const vertexAttributesBuffer = new WebGLArrayBuffer(
ARRAY_BUFFER,
DYNAMIC_DRAW,
).fromArrayBuffer(received.vertexAttributesBuffer);
const instanceAttributesBuffer = new WebGLArrayBuffer(
ARRAY_BUFFER,
DYNAMIC_DRAW,
).fromArrayBuffer(received.instanceAttributesBuffer);
this.helper_.flushBufferData(indicesBuffer);
this.helper_.flushBufferData(vertexAttributesBuffer);
this.helper_.flushBufferData(instanceAttributesBuffer);
return [indicesBuffer, vertexAttributesBuffer, instanceAttributesBuffer];
});
}
/**
* @param {RenderInstructions} renderInstructions Render instructions
* @param {import('../../webgl/LabelsArray.js').default} labelsArray Labels array
* @param {import("../../transform.js").Transform} transform Transform to apply to coordinates
* @param {number} resolution View resolution to be used as a basis when computing text overflow
* @return {Promise<string>|null} Resolves to a key corresponding to the text draw instructions; null if no text to render
* @private
*/
generateTextInstructions_(
renderInstructions,
labelsArray,
transform,
resolution,
) {
const transferables = [labelsArray.getArray().buffer];
let polygonRenderInstructions = null;
let lineStringRenderInstructions = null;
let pointRenderInstructions = null;
if (renderInstructions.polygonInstructions) {
polygonRenderInstructions = new Float32Array(
renderInstructions.polygonInstructions,
).buffer;
transferables.push(polygonRenderInstructions);
}
if (renderInstructions.lineStringInstructions) {
lineStringRenderInstructions = new Float32Array(
renderInstructions.lineStringInstructions,
).buffer;
transferables.push(lineStringRenderInstructions);
}
if (renderInstructions.pointInstructions) {
pointRenderInstructions = new Float32Array(
renderInstructions.pointInstructions,
).buffer;
transferables.push(pointRenderInstructions);
}
const customAttributesSizes = Object.keys(this.customAttributes_).reduce(
(prev, curr) => ({
...prev,
[curr]: this.customAttributes_[curr].size || 1,
}),
{},
);
// load render instructions in text overlay worker
/** @type {import('./constants.js').TextOverlayWorkerMessage} */
const message = {
type: TextOverlayWorkerMessageType.BUILD_INSTRUCTIONS,
polygonRenderInstructions,
lineStringRenderInstructions,
pointRenderInstructions,
labelsArray: labelsArray.getArray(),
style: this.flatStyle,
customAttributesSizes,
renderInstructionsTransform: transform,
resolution,
};
return messageWorker(this.textOverlayWorker_, message, transferables).then(
(data) => {
const received =
/** @type {import('./constants.js').TextOverlayWorkerMessage} */ (
data
);
// we're getting a key from the worker: these will be used later on to ask for render or disposal
return received.instructionsSetKey;
},
);
}
/**
* Render the geometries in the given buffers.
* @param {WebGLBuffers} buffers WebGL Buffers to draw
* @param {import("../../Map.js").FrameState} frameState Frame state
* @param {function(): void} preRenderCallback This callback will be called right before drawing, and can be used to set uniforms
*/
render(buffers, frameState, preRenderCallback) {
for (const renderPass of this.renderPasses_) {
renderPass.fillRenderPass &&
buffers.polygonBuffers &&
this.renderInternal_(
buffers.polygonBuffers[0],
buffers.polygonBuffers[1],
buffers.polygonBuffers[2],
renderPass.fillRenderPass,
frameState,
preRenderCallback,
);
renderPass.strokeRenderPass &&
buffers.lineStringBuffers &&
this.renderInternal_(
buffers.lineStringBuffers[0],
buffers.lineStringBuffers[1],
buffers.lineStringBuffers[2],
renderPass.strokeRenderPass,
frameState,
preRenderCallback,
);
renderPass.symbolRenderPass &&
buffers.pointBuffers &&
this.renderInternal_(
buffers.pointBuffers[0],
buffers.pointBuffers[1],
buffers.pointBuffers[2],
renderPass.symbolRenderPass,
frameState,
preRenderCallback,
);
}
if (buffers.textInstructionsKey) {
this.renderText_(buffers);
}
}
/**
* @param {WebGLArrayBuffer} indicesBuffer Indices buffer
* @param {WebGLArrayBuffer} vertexAttributesBuffer Vertex attributes buffer
* @param {WebGLArrayBuffer} instanceAttributesBuffer Instance attributes buffer
* @param {SubRenderPass} subRenderPass Render pass (program, attributes, etc.) specific to one geometry type
* @param {import("../../Map.js").FrameState} frameState Frame state.
* @param {function(): void} preRenderCallback This callback will be called right before drawing, and can be used to set uniforms
* @private
*/
renderInternal_(
indicesBuffer,
vertexAttributesBuffer,
instanceAttributesBuffer,
subRenderPass,
frameState,
preRenderCallback,
) {
const renderCount = indicesBuffer.getSize();
if (renderCount === 0) {
return;
}
const usesInstancedRendering = subRenderPass.instancedAttributesDesc.length;
this.helper_.useProgram(subRenderPass.program, frameState);
this.helper_.bindBuffer(vertexAttributesBuffer);
this.helper_.bindBuffer(indicesBuffer);
this.helper_.enableAttributes(subRenderPass.attributesDesc);
this.helper_.bindBuffer(instanceAttributesBuffer);
this.helper_.enableAttributesInstanced(
subRenderPass.instancedAttributesDesc,
);
preRenderCallback();
if (usesInstancedRendering) {
const instanceAttributesStride =
subRenderPass.instancedAttributesDesc.reduce(
(prev, curr) => prev + (curr.size || 1),
0,
);
const instanceCount =
instanceAttributesBuffer.getSize() / instanceAttributesStride;
this.helper_.drawElementsInstanced(0, renderCount, instanceCount);
} else {
this.helper_.drawElements(0, renderCount);
}
}
/**
* @param {WebGLBuffers} buffers WebGL Buffers to draw
* @private
*/
renderText_(buffers) {
this.textOverlayRenderList_.add(buffers.textInstructionsKey);
}
/**
* Render the geometries in the given buffers.
* @param {import("../../Map.js").FrameState} frameState Frame state
* @return {Promise<void>} A promise resolving after the post rendering step is over
*/
finalizeTextRender(frameState) {
if (!this.hasText_) {
return Promise.resolve();
}
const message = {
type: TextOverlayWorkerMessageType.RENDER,
frameState: serializeFrameState(frameState),
batchesToRender: this.textOverlayRenderList_,
};
return messageWorker(this.textOverlayWorker_, message).then((data) => {
const received =
/** @type {import('./constants.js').TextOverlayWorkerMessage} */ (data);
// if no render data returned, do not process it
if (received.imageData) {
this.textOverlayRenderFrameState_ = received.frameState;
// the rendered image data is copied to the canvas and then given back to the worker
const imageData = received.imageData;
if (
imageData.width !== this.textOverlayCanvas_.width ||
imageData.height !== this.textOverlayCanvas_.height
) {
this.textOverlayCanvas_.width = imageData.width;
this.textOverlayCanvas_.height = imageData.height;
} else {
this.textOverlayContext_.clearRect(
0,
0,
this.textOverlayCanvas_.width,
this.textOverlayCanvas_.height,
);
}
this.textOverlayContext_.drawImage(imageData, 0, 0);
imageData.close();
}
this.textOverlayRenderList_.clear();
});
}
/**
* @param {import('../../webgl/Helper.js').default} helper Helper
* @param {WebGLBuffers} buffers WebGL Buffers to reload if any
*/
setHelper(helper, buffers = null) {
this.helper_ = helper;
for (const renderPass of this.renderPasses_) {
if (renderPass.fillRenderPass) {
renderPass.fillRenderPass.program = this.helper_.getProgram(
renderPass.fillRenderPass.fragmentShader,
renderPass.fillRenderPass.vertexShader,
);
}
if (renderPass.strokeRenderPass) {
renderPass.strokeRenderPass.program = this.helper_.getProgram(
renderPass.strokeRenderPass.fragmentShader,
renderPass.strokeRenderPass.vertexShader,
);
}
if (renderPass.symbolRenderPass) {
renderPass.symbolRenderPass.program = this.helper_.getProgram(
renderPass.symbolRenderPass.fragmentShader,
renderPass.symbolRenderPass.vertexShader,
);
}
}
this.helper_.addUniforms(this.uniforms_);
if (buffers) {
if (buffers.polygonBuffers) {
this.helper_.flushBufferData(buffers.polygonBuffers[0]);
this.helper_.flushBufferData(buffers.polygonBuffers[1]);
this.helper_.flushBufferData(buffers.polygonBuffers[2]);
}
if (buffers.lineStringBuffers) {
this.helper_.flushBufferData(buffers.lineStringBuffers[0]);
this.helper_.flushBufferData(buffers.lineStringBuffers[1]);
this.helper_.flushBufferData(buffers.lineStringBuffers[2]);
}
if (buffers.pointBuffers) {
this.helper_.flushBufferData(buffers.pointBuffers[0]);
this.helper_.flushBufferData(buffers.pointBuffers[1]);
this.helper_.flushBufferData(buffers.pointBuffers[2]);
}
}
}
getTextOverlayCanvas() {
return this.textOverlayCanvas_;
}
getTextOverlayFrameState() {
return this.textOverlayRenderFrameState_;
}
/**
* Dispose of text instructions in worker.
* @param {string} key Key corresponding to the instructions set to dispose
*/
disposeTextInstructions(key) {
this.textOverlayWorker_?.postMessage({
type: TextOverlayWorkerMessageType.DISPOSE_INSTRUCTIONS,
instructionsSetKey: key,
});
}
/**
* Clean up.
* @override
*/
disposeInternal() {
this.textOverlayWorker_?.terminate();
super.disposeInternal();
}
}
export default VectorStyleRenderer;
/**
* @param {FlatStyleLike|StyleShaders|Array<StyleShaders>} styleOrShaders Either a flat style or shaders
* @return {FlatStyleLike|null} Will return null if the original flat style could not be found
*/
export function toFlatStyleLike(styleOrShaders) {
if (Array.isArray(styleOrShaders)) {
// if it's an array of shaders but at least one has no source rule, we can't return a flat style like
if (styleOrShaders.some((s) => 'builder' in s && !('sourceRule' in s))) {
return null;
}
if (styleOrShaders.some((s) => 'builder' in s)) {
return styleOrShaders.map((style) => style.sourceRule);
}
return /** @type {FlatStyleLike} */ (styleOrShaders);
}
if ('builder' in styleOrShaders) {
if (!('sourceRule' in styleOrShaders)) {
return null;
}
return [styleOrShaders.sourceRule];
}
return styleOrShaders;
}
/**
* Breaks down a vector style into an array of prebuilt shader builders with attributes and uniforms
* @param {FlatStyleLike|StyleShaders|Array<StyleShaders>} style Vector style
* @param {import('../../style/flat.js').StyleVariables} variables Style variables
* @return {Array<StyleShaders>} Array of style shaders
*/
export function convertStyleToShaders(style, variables) {
// possible cases:
// - single shader
// - multiple shaders
// - single style
// - multiple styles
// - multiple rules
const asArray = Array.isArray(style) ? style : [style];
// if array of rules: break rules into separate styles, compute "else" filters
if ('style' in asArray[0]) {
/** @type {Array<StyleShaders>} */
const shaders = [];
const rules = /** @type {Array<FlatStyleRule>} */ (asArray);
const previousFilters = [];
for (const rule of rules) {
/** @type {Array<FlatStyle>} */
const ruleStyles = Array.isArray(rule.style) ? rule.style : [rule.style];
/** @type {import("../../expr/expression.js").EncodedExpression} */
let currentFilter = rule.filter;
if (rule.else && previousFilters.length) {
currentFilter = [
'all',
...previousFilters.map((filter) => ['!', filter]),
];
if (rule.filter) {
currentFilter.push(rule.filter);
}
if (currentFilter.length < 3) {
currentFilter = currentFilter[1];
}
}
if (rule.filter) {
previousFilters.push(rule.filter);
}
// parse each style and convert to shader
const styleShaders = ruleStyles.map((style) => ({
...parseLiteralStyle(style, variables, currentFilter),
sourceRule: rule,
}));
shaders.push(...styleShaders);
}
return shaders;
}
// if array of shaders: return as is
if ('builder' in asArray[0]) {
return /** @type {Array<StyleShaders>} */ (asArray);
}
// array of flat styles: simply convert to shaders
return /** @type {Array<FlatStyle>} */ (asArray).map((style) => ({
...parseLiteralStyle(style, variables, null),
sourceRule: {style},
}));
}