ol
Version:
OpenLayers mapping library
483 lines (447 loc) • 16.3 kB
JavaScript
/**
* @module ol/render/webgl/textUtil
*/
import {ColorType, StringType} from '../../expr/expression.js';
import LineString from '../../geom/LineString.js';
import Point from '../../geom/Point.js';
import Polygon from '../../geom/Polygon.js';
import {
create as createMat4,
reset as resetMat4,
rotate as rotateMat4,
scale as scaleMat4,
translate as translateMat4,
} from '../../vec/mat4.js';
import RenderFeature from '../Feature.js';
import {unpackColor} from './compileUtil.js';
import {getCustomAttributesSize} from './renderinstructions.js';
export const TextUniforms = {
TEXT_OVERLAY_TEXTURE: 'u_textOverlay',
TEXT_OVERLAY_MATRIX: 'u_textOverlayMatrix',
};
/**
* @param {import('../../style/flat.js').FlatStyleLike} style Single flat style
* @return {boolean} Whether the style has text-related properties
*/
export function hasTextStyle(style) {
let result = false;
function check(style) {
for (const prop in style) {
if (prop === 'text-value') {
result = true;
return;
}
}
}
if (Array.isArray(style)) {
for (let i = 0, ii = style.length; i < ii; i++) {
const rule = style[i];
if ('style' in rule && Array.isArray(rule.style)) {
for (let j = 0, jj = rule.style.length; j < jj; j++) {
check(rule.style[j]);
}
} else if ('style' in rule) {
check(rule.style);
} else {
check(rule);
}
if (result) {
return result;
}
}
return result;
}
check(style);
return result;
}
/**
* @param {import('../../style/flat.js').FlatStyleLike} style Single flat style
* @return {import('../../style/flat.js').FlatStyleLike} Style with text-related properties only;
* NOTE: THIS MUTATES THE OBJECT
*/
export function stripNonTextStyleProperties(style) {
function stripStyle(style) {
for (const prop in style) {
if (!prop.startsWith('text-') && prop !== 'z-index') {
delete style[prop];
}
}
}
if (Array.isArray(style)) {
for (let i = 0, ii = style.length; i < ii; i++) {
const rule = style[i];
if ('style' in rule && Array.isArray(rule.style)) {
for (let j = 0, jj = rule.style.length; j < jj; j++) {
stripStyle(rule.style[j]);
}
} else if ('style' in rule) {
stripStyle(rule.style);
} else {
stripStyle(rule);
}
}
return style;
}
stripStyle(style);
return style;
}
/**
* @param {function(): HTMLCanvasElement} textOverlayCanvasGetter Function that returns the canvas where the text overlay was rendered
* @param {function(): import('../../Map.js').FrameState} textOverlayFrameStateGetter Function that returns the frame state used for rendering the text overlay
* @return {import("../../renderer/webgl/Layer.js").PostProcessesOptions} Post-process definition for text rendering
*/
export function createPostProcessDefinition(
textOverlayCanvasGetter,
textOverlayFrameStateGetter,
) {
const tmpMatrix = createMat4();
return {
fragmentShader: `
precision mediump float;
uniform sampler2D u_image;
uniform sampler2D ${TextUniforms.TEXT_OVERLAY_TEXTURE};
uniform mat4 ${TextUniforms.TEXT_OVERLAY_MATRIX};
varying vec2 v_texCoord;
void main() {
vec4 color = texture2D(u_image, v_texCoord);
vec2 coords = v_texCoord * 2. - vec2(1.);
coords = (${TextUniforms.TEXT_OVERLAY_MATRIX} * vec4(coords.xy, 0., 1.)).xy;
coords = coords * 0.5 + vec2(0.5);
float outOfBounds = clamp(step(1., coords.x) + step(1., coords.y) + step(0., -coords.x) + step(0., -coords.y), 0., 1.);
vec4 textColor = texture2D(${TextUniforms.TEXT_OVERLAY_TEXTURE}, vec2(coords.x, 1. - coords.y));
textColor.a *= 1. - outOfBounds; // if we're sampling out of the text overlay, make alpha 0 to avoid drawing anything
gl_FragColor = textColor.a * textColor + (1. - textColor.a) * color;
}`,
uniforms: {
[TextUniforms.TEXT_OVERLAY_TEXTURE]: textOverlayCanvasGetter,
[TextUniforms.TEXT_OVERLAY_MATRIX]: (frameState) => {
const textOverlayCanvas = textOverlayCanvasGetter();
const textOverlayFrameState = textOverlayFrameStateGetter();
if (!textOverlayCanvas || !textOverlayFrameState) {
return tmpMatrix;
}
const textOverlayViewState = textOverlayFrameState.viewState;
const viewState = frameState.viewState;
const center = viewState.center;
const resolution = viewState.resolution;
const rotation = viewState.rotation;
const size = frameState.size;
const renderedCenter = textOverlayViewState.center;
const renderedResolution = textOverlayViewState.resolution;
const renderedRotation = textOverlayViewState.rotation;
const renderedWidth = textOverlayCanvas.width;
const renderedHeight = textOverlayCanvas.height;
resetMat4(tmpMatrix);
scaleMat4(
tmpMatrix,
1 / renderedResolution / (renderedWidth / 2),
1 / renderedResolution / (renderedHeight / 2),
1,
tmpMatrix,
);
rotateMat4(tmpMatrix, renderedRotation, tmpMatrix);
translateMat4(
tmpMatrix,
center[0] - renderedCenter[0],
center[1] - renderedCenter[1],
0,
tmpMatrix,
);
rotateMat4(tmpMatrix, -rotation, tmpMatrix);
scaleMat4(
tmpMatrix,
(resolution * size[0]) / 2,
(resolution * size[1]) / 2,
1,
tmpMatrix,
);
return tmpMatrix;
},
},
};
}
const textFeatureProps = {};
const textFeature = new RenderFeature(
'Point', // the feature holds a simple placeholder geometry
[0, 0],
[],
2,
textFeatureProps,
'dummy',
);
const textDecoder = new TextDecoder();
/**
* @param {string} propertyName Name of the property (without the `prop_` prefix)
* @param {import('../../expr/expression.js').ValueType} propertyType Type of the property (e.g. `StringType`, `ColorType`)
* @param {Array<string>} customAttributesKeys Ordered keys of custom attributes
* @param {import('./VectorStyleRenderer.js').AttributeDefinitions} customAttributes Custom attributes sizes
* @param {Float32Array} customAttributesValues Flat array of custom attribute values for the feature
* @param {Uint8Array} labels Encoded label bytes used for string properties
* @return {string | Array<number> | number} Decoded custom attribute value
*/
function readCustomAttributeValue(
propertyName,
propertyType,
customAttributesKeys,
customAttributes,
customAttributesValues,
labels,
) {
const customAttrName = `prop_${propertyName}`;
const customAttrPosition = customAttributesKeys.findIndex(
(key) => key === customAttrName,
);
const customAttrOffset = customAttributesKeys
.slice(0, customAttrPosition)
.reduce((prev, curr) => prev + customAttributes[curr].size, 0);
const customAttrSize = customAttributes[customAttrName].size;
if (propertyType === StringType) {
const start = customAttributesValues[customAttrOffset + 1];
const length = customAttributesValues[customAttrOffset + 2];
const bytes = labels.slice(start, start + length);
return textDecoder.decode(bytes);
}
if (propertyType === ColorType) {
const value = unpackColor(
Array.from(
customAttributesValues.slice(customAttrOffset, customAttrOffset + 2),
),
);
value[0] *= 255;
value[1] *= 255;
value[2] *= 255;
return value;
}
if (customAttrSize > 1) {
return Array.from(
customAttributesValues.slice(
customAttrOffset,
customAttrOffset + customAttrSize,
),
);
}
return customAttributesValues[customAttrOffset];
}
/**
* Draw text for a geometry using the provided style function and text builder.
* @param {import('../../style/Style.js').StyleFunction} styleFunction Function returning style(s) for a feature
* @param {import('../canvas/TextBuilder.js').default} textBuilder Text builder instance
* @param {import('../Feature.js').default} feature Render feature used as style input
* @param {import('../../geom/SimpleGeometry.js').default} geometry Geometry to draw text for
* @param {Object} sharedData Mutable shared data object passed to the text builder
* @private
*/
function drawTextGeometry(
styleFunction,
textBuilder,
feature,
geometry,
sharedData,
) {
let styles = styleFunction(feature, 1);
if (!styles) {
return;
}
styles = Array.isArray(styles) ? styles : [styles];
for (let i = 0, ii = styles.length; i < ii; i++) {
const textStyle = styles[i].getText();
if (!textStyle) {
continue;
}
const placement = textStyle.getPlacement();
const isLine = geometry.getType() === 'LineString'; // TODO: find a better way to figure out if the line geometry is a linear ring from a polygon
if ((placement === 'line' && !isLine) || (placement !== 'line' && isLine)) {
continue;
}
textBuilder.setTextStyle(textStyle, sharedData);
textBuilder.drawText(geometry, feature);
}
}
/**
* Uses a Canvas Text Builder to render labels for polygons described in a RenderInstructions typed array
* @param {Float32Array} renderInstructions Array of render instructions for polygons.
* @param {Uint8Array} labels Integer array containing encoded labels (from LabelsArray)
* @param {Map<string,import('../../expr/expression.js').ValueType>} properties Properties
* @param {import('./VectorStyleRenderer.js').AttributeDefinitions} customAttributes Custom attributes definitions
* @param {import('../canvas/TextBuilder.js').default} textBuilder Text builder
* @param {import('../../style/Style.js').StyleFunction} styleFunction Text style
* @private
*/
export function convertPolygonRenderInstructionsToCanvasTextBuilder(
renderInstructions,
labels,
properties,
customAttributes,
textBuilder,
styleFunction,
) {
const customAttributesKeys = Object.keys(customAttributes);
const totalCustomAttributesSize = getCustomAttributesSize(customAttributes);
const instructionsPerVertex = 2; // x, y
const sharedData = {};
let instructionsIndex = 0;
while (instructionsIndex < renderInstructions.length) {
const customAttributesValues = new Float32Array(
renderInstructions.buffer,
instructionsIndex * Float32Array.BYTES_PER_ELEMENT,
totalCustomAttributesSize,
);
instructionsIndex += totalCustomAttributesSize;
const ringsCount = renderInstructions[instructionsIndex++];
let verticesCount = 0;
const ends = new Array(ringsCount);
for (let i = 0; i < ringsCount; i++) {
verticesCount += renderInstructions[instructionsIndex++];
ends[i] = verticesCount * instructionsPerVertex;
}
const newInstructionsIndex =
instructionsIndex + verticesCount * instructionsPerVertex;
const flatCoords = Array.from(
new Float32Array(
renderInstructions.buffer,
instructionsIndex * Float32Array.BYTES_PER_ELEMENT,
verticesCount * instructionsPerVertex,
),
);
const polygon = new Polygon(flatCoords, 'XY', ends);
const propEntries = Array.from(properties.entries());
for (let i = 0; i < propEntries.length; i++) {
const [propName, propType] = propEntries[i];
textFeatureProps[propName] = readCustomAttributeValue(
propName,
propType,
customAttributesKeys,
customAttributes,
customAttributesValues,
labels,
);
}
drawTextGeometry(
styleFunction,
textBuilder,
textFeature,
polygon,
sharedData,
);
instructionsIndex = newInstructionsIndex;
}
}
/**
Uses a Canvas Text Builder to render labels for line strings described in a RenderInstructions typed array
* @param {Float32Array} renderInstructions Array of render instructions for lines.
* @param {Uint8Array} labels Integer array containing encoded labels (from LabelsArray)
* @param {Map<string,import('../../expr/expression.js').ValueType>} properties Properties
* @param {import('./VectorStyleRenderer.js').AttributeDefinitions} customAttributes Custom attributes definitions
* @param {import('../canvas/TextBuilder.js').default} textBuilder Text builder
* @param {import('../../style/Style.js').StyleFunction} styleFunction Text style
* @private
*/
export function convertLineStringRenderInstructionsToCanvasTextBuilder(
renderInstructions,
labels,
properties,
customAttributes,
textBuilder,
styleFunction,
) {
const customAttributesKeys = Object.keys(customAttributes);
const totalCustomAttributesSize = getCustomAttributesSize(customAttributes);
const instructionsPerVertex = 3; // x, y
const sharedData = {};
let currentInstructionsIndex = 0;
let verticesCount;
while (currentInstructionsIndex < renderInstructions.length) {
const customAttributesValues = new Float32Array(
renderInstructions.buffer,
currentInstructionsIndex * Float32Array.BYTES_PER_ELEMENT,
totalCustomAttributesSize,
);
currentInstructionsIndex += totalCustomAttributesSize;
verticesCount = renderInstructions[currentInstructionsIndex++];
const flatCoords = Array.from(
new Float32Array(
renderInstructions.buffer,
currentInstructionsIndex * Float32Array.BYTES_PER_ELEMENT,
verticesCount * instructionsPerVertex,
),
);
const lineString = new LineString(flatCoords, 'XYM'); // render instructions always provide XYM coordinates
const propEntries = Array.from(properties.entries());
for (let i = 0; i < propEntries.length; i++) {
const [propName, propType] = propEntries[i];
textFeatureProps[propName] = readCustomAttributeValue(
propName,
propType,
customAttributesKeys,
customAttributes,
customAttributesValues,
labels,
);
}
drawTextGeometry(
styleFunction,
textBuilder,
textFeature,
lineString,
sharedData,
);
currentInstructionsIndex += verticesCount * instructionsPerVertex;
}
}
/**
Uses a Canvas Text Builder to render labels for points described in a RenderInstructions typed array
* @param {Float32Array} renderInstructions Array of render instructions for points.
* @param {Uint8Array} labels Integer array containing encoded labels (from LabelsArray)
* @param {Map<string,import('../../expr/expression.js').ValueType>} properties Properties
* @param {import('./VectorStyleRenderer.js').AttributeDefinitions} customAttributes Custom attributes definitions
* @param {import('../canvas/TextBuilder.js').default} textBuilder Text builder
* @param {import('../../style/Style.js').StyleFunction} styleFunction Text style
* @private
*/
export function convertPointRenderInstructionsToCanvasTextBuilder(
renderInstructions,
labels,
properties,
customAttributes,
textBuilder,
styleFunction,
) {
const customAttributesKeys = Object.keys(customAttributes);
const totalCustomAttributesSize = getCustomAttributesSize(customAttributes);
const instructionsPerVertex = 2; // x, y
const sharedData = {};
let currentInstructionsIndex = 0;
while (currentInstructionsIndex < renderInstructions.length) {
const flatCoords = [
renderInstructions.at(currentInstructionsIndex),
renderInstructions.at(currentInstructionsIndex + 1),
];
currentInstructionsIndex += instructionsPerVertex;
const customAttributesValues = new Float32Array(
renderInstructions.buffer,
currentInstructionsIndex * Float32Array.BYTES_PER_ELEMENT,
totalCustomAttributesSize,
);
const point = new Point(flatCoords, 'XY');
const propEntries = Array.from(properties.entries());
for (let i = 0; i < propEntries.length; i++) {
const [propName, propType] = propEntries[i];
textFeatureProps[propName] = readCustomAttributeValue(
propName,
propType,
customAttributesKeys,
customAttributes,
customAttributesValues,
labels,
);
}
drawTextGeometry(
styleFunction,
textBuilder,
textFeature,
point,
sharedData,
);
currentInstructionsIndex += totalCustomAttributesSize;
}
}