@deck.gl/layers
Version:
deck.gl core layers
327 lines (326 loc) • 15.3 kB
JavaScript
// Copyright (c) 2015 - 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import { CompositeLayer, createIterable } from '@deck.gl/core';
import MultiIconLayer from './multi-icon-layer/multi-icon-layer';
import FontAtlasManager, { DEFAULT_FONT_SETTINGS, setFontAtlasCacheLimit } from './font-atlas-manager';
import { transformParagraph, getTextFromBuffer } from './utils';
import TextBackgroundLayer from './text-background-layer/text-background-layer';
const TEXT_ANCHOR = {
start: 1,
middle: 0,
end: -1
};
const ALIGNMENT_BASELINE = {
top: 1,
center: 0,
bottom: -1
};
const DEFAULT_COLOR = [0, 0, 0, 255];
const DEFAULT_LINE_HEIGHT = 1.0;
const defaultProps = {
billboard: true,
sizeScale: 1,
sizeUnits: 'pixels',
sizeMinPixels: 0,
sizeMaxPixels: Number.MAX_SAFE_INTEGER,
background: false,
getBackgroundColor: { type: 'accessor', value: [255, 255, 255, 255] },
getBorderColor: { type: 'accessor', value: DEFAULT_COLOR },
getBorderWidth: { type: 'accessor', value: 0 },
backgroundPadding: { type: 'array', value: [0, 0, 0, 0] },
characterSet: { type: 'object', value: DEFAULT_FONT_SETTINGS.characterSet },
fontFamily: DEFAULT_FONT_SETTINGS.fontFamily,
fontWeight: DEFAULT_FONT_SETTINGS.fontWeight,
lineHeight: DEFAULT_LINE_HEIGHT,
outlineWidth: { type: 'number', value: 0, min: 0 },
outlineColor: { type: 'color', value: DEFAULT_COLOR },
fontSettings: {},
// auto wrapping options
wordBreak: 'break-word',
maxWidth: { type: 'number', value: -1 },
getText: { type: 'accessor', value: x => x.text },
getPosition: { type: 'accessor', value: x => x.position },
getColor: { type: 'accessor', value: DEFAULT_COLOR },
getSize: { type: 'accessor', value: 32 },
getAngle: { type: 'accessor', value: 0 },
getTextAnchor: { type: 'accessor', value: 'middle' },
getAlignmentBaseline: { type: 'accessor', value: 'center' },
getPixelOffset: { type: 'accessor', value: [0, 0] },
// deprecated
backgroundColor: { deprecatedFor: ['background', 'getBackgroundColor'] }
};
export default class TextLayer extends CompositeLayer {
constructor() {
super(...arguments);
// Returns the x, y offsets of each character in a text string
this.getBoundingRect = (object, objectInfo) => {
const iconMapping = this.state.fontAtlasManager.mapping;
const getText = this.state.getText;
const { wordBreak, maxWidth, lineHeight, getTextAnchor, getAlignmentBaseline } = this.props;
const paragraph = getText(object, objectInfo) || '';
const { size: [width, height] } = transformParagraph(paragraph, lineHeight, wordBreak, maxWidth, iconMapping);
const anchorX = TEXT_ANCHOR[typeof getTextAnchor === 'function' ? getTextAnchor(object, objectInfo) : getTextAnchor];
const anchorY = ALIGNMENT_BASELINE[typeof getAlignmentBaseline === 'function'
? getAlignmentBaseline(object, objectInfo)
: getAlignmentBaseline];
return [((anchorX - 1) * width) / 2, ((anchorY - 1) * height) / 2, width, height];
};
// Returns the x, y, w, h of each text object
this.getIconOffsets = (object, objectInfo) => {
const iconMapping = this.state.fontAtlasManager.mapping;
const getText = this.state.getText;
const { wordBreak, maxWidth, lineHeight, getTextAnchor, getAlignmentBaseline } = this.props;
const paragraph = getText(object, objectInfo) || '';
const { x, y, rowWidth, size: [width, height] } = transformParagraph(paragraph, lineHeight, wordBreak, maxWidth, iconMapping);
const anchorX = TEXT_ANCHOR[typeof getTextAnchor === 'function' ? getTextAnchor(object, objectInfo) : getTextAnchor];
const anchorY = ALIGNMENT_BASELINE[typeof getAlignmentBaseline === 'function'
? getAlignmentBaseline(object, objectInfo)
: getAlignmentBaseline];
const numCharacters = x.length;
const offsets = new Array(numCharacters * 2);
let index = 0;
for (let i = 0; i < numCharacters; i++) {
// For a multi-line object, offset in x-direction needs consider
// the row offset in the paragraph and the object offset in the row
const rowOffset = ((1 - anchorX) * (width - rowWidth[i])) / 2;
offsets[index++] = ((anchorX - 1) * width) / 2 + rowOffset + x[i];
offsets[index++] = ((anchorY - 1) * height) / 2 + y[i];
}
return offsets;
};
}
initializeState() {
this.state = {
styleVersion: 0,
fontAtlasManager: new FontAtlasManager()
};
}
// eslint-disable-next-line complexity
updateState(params) {
const { props, oldProps, changeFlags } = params;
const textChanged = changeFlags.dataChanged ||
(changeFlags.updateTriggersChanged &&
(changeFlags.updateTriggersChanged.all || changeFlags.updateTriggersChanged.getText));
if (textChanged) {
this._updateText();
}
const fontChanged = this._updateFontAtlas();
const styleChanged = fontChanged ||
props.lineHeight !== oldProps.lineHeight ||
props.wordBreak !== oldProps.wordBreak ||
props.maxWidth !== oldProps.maxWidth;
if (styleChanged) {
this.setState({
styleVersion: this.state.styleVersion + 1
});
}
}
getPickingInfo({ info }) {
// because `TextLayer` assign the same pickingInfoIndex for one text label,
// here info.index refers the index of text label in props.data
info.object = info.index >= 0 ? this.props.data[info.index] : null;
return info;
}
/** Returns true if font has changed */
_updateFontAtlas() {
const { fontSettings, fontFamily, fontWeight } = this.props;
const { fontAtlasManager, characterSet } = this.state;
const fontProps = {
...fontSettings,
characterSet,
fontFamily,
fontWeight
};
if (!fontAtlasManager.mapping) {
// This is the first update
fontAtlasManager.setProps(fontProps);
return true;
}
for (const key in fontProps) {
if (fontProps[key] !== fontAtlasManager.props[key]) {
fontAtlasManager.setProps(fontProps);
return true;
}
}
return false;
}
// Text strings are variable width objects
// Count characters and start offsets
_updateText() {
const { data, characterSet } = this.props;
const textBuffer = data.attributes?.getText;
let { getText } = this.props;
let startIndices = data.startIndices;
let numInstances;
const autoCharacterSet = characterSet === 'auto' && new Set();
if (textBuffer && startIndices) {
const { texts, characterCount } = getTextFromBuffer({
...(ArrayBuffer.isView(textBuffer) ? { value: textBuffer } : textBuffer),
// @ts-ignore if data.attribute is defined then length is expected
length: data.length,
startIndices,
characterSet: autoCharacterSet
});
numInstances = characterCount;
getText = (_, { index }) => texts[index];
}
else {
const { iterable, objectInfo } = createIterable(data);
startIndices = [0];
numInstances = 0;
for (const object of iterable) {
objectInfo.index++;
// Break into an array of characters
// When dealing with double-length unicode characters, `str.length` or `str[i]` do not work
const text = Array.from(getText(object, objectInfo) || '');
if (autoCharacterSet) {
// eslint-disable-next-line @typescript-eslint/unbound-method
text.forEach(autoCharacterSet.add, autoCharacterSet);
}
numInstances += text.length;
startIndices.push(numInstances);
}
}
this.setState({
getText,
startIndices,
numInstances,
characterSet: autoCharacterSet || characterSet
});
}
renderLayers() {
const { startIndices, numInstances, getText, fontAtlasManager: { scale, texture, mapping }, styleVersion } = this.state;
const { data, _dataDiff, getPosition, getColor, getSize, getAngle, getPixelOffset, getBackgroundColor, getBorderColor, getBorderWidth, backgroundPadding, background, billboard, fontSettings, outlineWidth, outlineColor, sizeScale, sizeUnits, sizeMinPixels, sizeMaxPixels, transitions, updateTriggers } = this.props;
const CharactersLayerClass = this.getSubLayerClass('characters', MultiIconLayer);
const BackgroundLayerClass = this.getSubLayerClass('background', TextBackgroundLayer);
return [
background &&
new BackgroundLayerClass({
// background props
getFillColor: getBackgroundColor,
getLineColor: getBorderColor,
getLineWidth: getBorderWidth,
padding: backgroundPadding,
// props shared with characters layer
getPosition,
getSize,
getAngle,
getPixelOffset,
billboard,
sizeScale: sizeScale / this.state.fontAtlasManager.props.fontSize,
sizeUnits,
sizeMinPixels,
sizeMaxPixels,
transitions: transitions && {
getPosition: transitions.getPosition,
getAngle: transitions.getAngle,
getSize: transitions.getSize,
getFillColor: transitions.getBackgroundColor,
getLineColor: transitions.getBorderColor,
getLineWidth: transitions.getBorderWidth,
getPixelOffset: transitions.getPixelOffset
}
}, this.getSubLayerProps({
id: 'background',
updateTriggers: {
getPosition: updateTriggers.getPosition,
getAngle: updateTriggers.getAngle,
getSize: updateTriggers.getSize,
getFillColor: updateTriggers.getBackgroundColor,
getLineColor: updateTriggers.getBorderColor,
getLineWidth: updateTriggers.getBorderWidth,
getPixelOffset: updateTriggers.getPixelOffset,
getBoundingRect: {
getText: updateTriggers.getText,
getTextAnchor: updateTriggers.getTextAnchor,
getAlignmentBaseline: updateTriggers.getAlignmentBaseline,
styleVersion
}
}
}), {
data:
// @ts-ignore (2339) attribute is not defined on all data types
data.attributes && data.attributes.background
? // @ts-ignore (2339) attribute is not defined on all data types
{ length: data.length, attributes: data.attributes.background }
: data,
_dataDiff,
// Maintain the same background behavior as <=8.3. Remove in v9?
autoHighlight: false,
getBoundingRect: this.getBoundingRect
}),
new CharactersLayerClass({
sdf: fontSettings.sdf,
smoothing: Number.isFinite(fontSettings.smoothing)
? fontSettings.smoothing
: DEFAULT_FONT_SETTINGS.smoothing,
outlineWidth,
outlineColor,
iconAtlas: texture,
iconMapping: mapping,
getPosition,
getColor,
getSize,
getAngle,
getPixelOffset,
billboard,
sizeScale: sizeScale * scale,
sizeUnits,
sizeMinPixels: sizeMinPixels * scale,
sizeMaxPixels: sizeMaxPixels * scale,
transitions: transitions && {
getPosition: transitions.getPosition,
getAngle: transitions.getAngle,
getColor: transitions.getColor,
getSize: transitions.getSize,
getPixelOffset: transitions.getPixelOffset
}
}, this.getSubLayerProps({
id: 'characters',
updateTriggers: {
getIcon: updateTriggers.getText,
getPosition: updateTriggers.getPosition,
getAngle: updateTriggers.getAngle,
getColor: updateTriggers.getColor,
getSize: updateTriggers.getSize,
getPixelOffset: updateTriggers.getPixelOffset,
getIconOffsets: {
getText: updateTriggers.getText,
getTextAnchor: updateTriggers.getTextAnchor,
getAlignmentBaseline: updateTriggers.getAlignmentBaseline,
styleVersion
}
}
}), {
data,
_dataDiff,
startIndices,
numInstances,
getIconOffsets: this.getIconOffsets,
getIcon: getText
})
];
}
static set fontAtlasCacheLimit(limit) {
setFontAtlasCacheLimit(limit);
}
}
TextLayer.defaultProps = defaultProps;
TextLayer.layerName = 'TextLayer';