UNPKG

@deck.gl/carto

Version:

CARTO official integration with Deck.gl. Build geospatial applications using CARTO and Deck.gl.

288 lines (253 loc) 8.44 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { Accessor, Color, CompositeLayer, CompositeLayerProps, DefaultProps, Layer, LayersList, log } from '@deck.gl/core'; import { TextLayer, TextLayerProps, _TextBackgroundLayer as TextBackgroundLayer } from '@deck.gl/layers'; import VectorTileLayer from './vector-tile-layer'; const [LEFT, TOP, RIGHT, BOTTOM] = [0, 1, 2, 3]; class EnhancedTextBackgroundLayer extends TextBackgroundLayer { static layerName = 'EnhancedTextBackgroundLayer'; getShaders() { const shaders = super.getShaders(); let vs = shaders.vs; // Modify shader so that the padding is offset by the pixel offset to ensure the padding // always captures the anchor point. As padding is uniform we cannot pass it a per-label value vs = vs.replaceAll('textBackground.padding.', '_padding.'); vs = vs.replace( 'void main(void) {', 'void main(void) {\n vec4 _padding = textBackground.padding + instancePixelOffsets.xyxy * vec4(1.0, 1.0, -1.0, -1.0);' ); return {...shaders, vs}; } } // TextLayer which includes modified text-background-layer-vertex shader and only renders the // primary background layer in the collision pass class EnhancedTextLayer extends TextLayer { static layerName = 'EnhancedTextLayer'; filterSubLayer({layer, renderPass}) { const background = layer.id.includes('primary-background'); if (renderPass === 'collision') { return background; // Only draw primary background layer in collision pass } return !background; // Do not draw background layer in other passes } } const defaultProps: DefaultProps<PointLabelLayerProps> = { ...TextLayer.defaultProps, getRadius: {type: 'accessor', value: 1}, radiusScale: {type: 'number', min: 0, value: 1} }; /** All properties supported by PointLabelLayer. */ export type PointLabelLayerProps<DataT = unknown> = _PointLabelLayerProps<DataT> & TextLayerProps & CompositeLayerProps; /** Properties added by PointLabelLayer. */ type _PointLabelLayerProps<DataT> = TextLayerProps<DataT> & { /** * Radius multiplier. * @default 1 */ radiusScale?: number; /** * Radius accessor. * @default 1 */ getRadius?: Accessor<DataT, number>; /** * Secondary label text accessor */ getSecondaryText?: Accessor<DataT, string>; /** * Secondary label color accessor * @default [0, 0, 0, 255] */ getSecondaryColor?: Accessor<DataT, Color>; /** * Secondary label color of outline around the text, in `[r, g, b, [a]]`. Each channel is a number between 0-255 and `a` is 255 if not supplied. * @default [0, 0, 0, 255] */ secondaryOutlineColor?: Color; /** * Secondary label text size multiplier. * @default 1 */ secondarySizeScale?: number; }; /** * PointLabelLayer is a layer that renders point labels. * It is a composite layer that renders a primary and secondary label. * It behaves like a TextLayer except that getTextSize is **not supported** * and the text size for the primary label must be set with **sizeScale**. */ export default class PointLabelLayer< DataT = any, ExtraProps extends {} = {} > extends CompositeLayer<ExtraProps & Required<_PointLabelLayerProps<DataT>>> { static layerName = 'PointLabelLayer'; static defaultProps = defaultProps; calculatePixelOffset(secondary) { const { getTextAnchor: anchor, getAlignmentBaseline: alignment, getRadius, getSecondaryText, radiusScale, secondarySizeScale, sizeScale } = this.props; const xMult = anchor === 'middle' ? 0 : anchor === 'start' ? 1 : -1; const yMult = alignment === 'center' ? 0 : alignment === 'bottom' ? 1 : -1; // Padding based on font size (font size / 4) const xPadding = sizeScale / 4; const yPadding = sizeScale * (1 + 1 / 4); // Place secondary label under main label (secondary label always 'top' baseline aligned) const secondaryOffset = 0.6 * (1 - yMult) * sizeScale; let yOffset = secondary ? secondaryOffset : 0; // Special case, position relative to secondary label if (anchor === 'middle' && alignment === 'top' && getSecondaryText) { yOffset -= secondaryOffset; yOffset -= secondarySizeScale; yOffset += sizeScale; } // Padding based on point radius (radius/ 4) const radiusPadding = 1 + 1 / 4; return typeof getRadius === 'function' ? (d, info) => { const r = (info ? getRadius(d, info) : 1) * radiusScale * radiusPadding; return [xMult * (r + xPadding), yMult * (r + yPadding) + yOffset]; } : [ xMult * (getRadius * radiusScale * radiusPadding + xPadding), yMult * (getRadius * radiusScale * radiusPadding + yPadding) + yOffset ]; } calculateBackgroundPadding() { const {getTextAnchor: anchor, getAlignmentBaseline: alignment, sizeScale} = this.props; // Heuristics to avoid label overlap const paddingX = 12 * sizeScale; const paddingY = 3 * sizeScale; const backgroundPadding = [0, 0, 0, 0]; if (alignment === 'top') { backgroundPadding[TOP] = paddingY; } else if (alignment === 'bottom') { backgroundPadding[BOTTOM] = paddingY; } else { backgroundPadding[TOP] = 0.5 * paddingY; backgroundPadding[BOTTOM] = 0.5 * paddingY; } if (anchor === 'start') { backgroundPadding[LEFT] = paddingX; } else if (anchor === 'end') { backgroundPadding[RIGHT] = paddingX; } else { backgroundPadding[LEFT] = 0.5 * paddingX; backgroundPadding[RIGHT] = 0.5 * paddingX; } return backgroundPadding; } renderTextLayer(id, {updateTriggers: updateTriggersOverride = {}, ...props}): EnhancedTextLayer { const { data, characterSet, fontFamily, fontSettings, fontWeight, outlineColor, outlineWidth, sizeScale, radiusScale, getAlignmentBaseline, getColor, getPosition, getTextAnchor, updateTriggers } = this.props; if (sizeScale < 2) { const propName = (this.parent as VectorTileLayer)?.props?.textSizeScale ? 'textSizeScale' : 'sizeScale'; log.warn( `${propName} has small value (${sizeScale}). Note getTextSize is not supported on PointLabelLayer` )(); } return new EnhancedTextLayer( this.getSubLayerProps({ id, data, characterSet, fontFamily, fontSettings, fontWeight, outlineColor, outlineWidth, sizeScale, getAlignmentBaseline, getColor, getPosition, getTextAnchor, updateTriggers: { ...updateTriggers, ...updateTriggersOverride, getPixelOffset: [ updateTriggers.getRadius, updateTriggers.getTextAnchor, updateTriggers.getAlignmentBaseline, radiusScale, sizeScale ] } }), { getSize: 1, _subLayerProps: {background: {type: EnhancedTextBackgroundLayer}} }, props ); } renderLayers(): Layer | null | LayersList { const { getText, getSecondaryColor, getSecondaryText, secondaryOutlineColor, secondarySizeScale, updateTriggers } = this.props; const getPixelOffset = this.calculatePixelOffset(false); const backgroundPadding = this.calculateBackgroundPadding(); const out = [ // Text doesn't update via updateTrigger for some reason this.renderTextLayer(`${updateTriggers.getText}-primary`, { backgroundPadding, getText, getPixelOffset, background: true // Only use background for primary label for faster collisions }), Boolean(getSecondaryText) && this.renderTextLayer(`${updateTriggers.getSecondaryText}-secondary`, { getText: getSecondaryText, getPixelOffset: this.calculatePixelOffset(true), getAlignmentBaseline: 'top', // updateTriggers: {getText: updateTriggers.getSecondaryText}, // Optional overrides ...(getSecondaryColor && {getColor: getSecondaryColor}), ...(secondarySizeScale && {sizeScale: secondarySizeScale}), ...(secondaryOutlineColor && {outlineColor: secondaryOutlineColor}) }) ]; return out; } }