UNPKG

ol

Version:

OpenLayers mapping library

483 lines (447 loc) • 16.3 kB
/** * @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; } }