@deck.gl/layers
Version:
deck.gl core layers
256 lines (255 loc) • 9.09 kB
JavaScript
/* global document */
import GL from '@luma.gl/constants';
import { Texture2D, copyToTexture, cloneTextureFrom } from '@luma.gl/core';
import { ImageLoader } from '@loaders.gl/images';
import { load } from '@loaders.gl/core';
import { createIterable } from '@deck.gl/core';
const DEFAULT_CANVAS_WIDTH = 1024;
const DEFAULT_BUFFER = 4;
const noop = () => { };
const DEFAULT_TEXTURE_PARAMETERS = {
[GL.TEXTURE_MIN_FILTER]: GL.LINEAR_MIPMAP_LINEAR,
// GL.LINEAR is the default value but explicitly set it here
[GL.TEXTURE_MAG_FILTER]: GL.LINEAR,
// for texture boundary artifact
[GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE,
[GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE
};
function nextPowOfTwo(number) {
return Math.pow(2, Math.ceil(Math.log2(number)));
}
// update comment to create a new texture and copy original data.
function resizeImage(ctx, imageData, width, height) {
if (width === imageData.width && height === imageData.height) {
return imageData;
}
ctx.canvas.height = height;
ctx.canvas.width = width;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight
ctx.drawImage(imageData, 0, 0, imageData.width, imageData.height, 0, 0, width, height);
return ctx.canvas;
}
function getIconId(icon) {
return icon && (icon.id || icon.url);
}
// resize texture without losing original data
function resizeTexture(texture, width, height) {
const oldWidth = texture.width;
const oldHeight = texture.height;
const newTexture = cloneTextureFrom(texture, { width, height });
copyToTexture(texture, newTexture, {
targetY: 0,
width: oldWidth,
height: oldHeight
});
texture.delete();
return newTexture;
}
// traverse icons in a row of icon atlas
// extend each icon with left-top coordinates
function buildRowMapping(mapping, columns, yOffset) {
for (let i = 0; i < columns.length; i++) {
const { icon, xOffset } = columns[i];
const id = getIconId(icon);
mapping[id] = {
...icon,
x: xOffset,
y: yOffset
};
}
}
/**
* Generate coordinate mapping to retrieve icon left-top position from an icon atlas
*/
export function buildMapping({ icons, buffer, mapping = {}, xOffset = 0, yOffset = 0, rowHeight = 0, canvasWidth }) {
let columns = [];
// Strategy to layout all the icons into a texture:
// traverse the icons sequentially, layout the icons from left to right, top to bottom
// when the sum of the icons width is equal or larger than canvasWidth,
// move to next row starting from total height so far plus max height of the icons in previous row
// row width is equal to canvasWidth
// row height is decided by the max height of the icons in that row
// mapping coordinates of each icon is its left-top position in the texture
for (let i = 0; i < icons.length; i++) {
const icon = icons[i];
const id = getIconId(icon);
if (!mapping[id]) {
const { height, width } = icon;
// fill one row
if (xOffset + width + buffer > canvasWidth) {
buildRowMapping(mapping, columns, yOffset);
xOffset = 0;
yOffset = rowHeight + yOffset + buffer;
rowHeight = 0;
columns = [];
}
columns.push({
icon,
xOffset
});
xOffset = xOffset + width + buffer;
rowHeight = Math.max(rowHeight, height);
}
}
if (columns.length > 0) {
buildRowMapping(mapping, columns, yOffset);
}
return {
mapping,
rowHeight,
xOffset,
yOffset,
canvasWidth,
canvasHeight: nextPowOfTwo(rowHeight + yOffset + buffer)
};
}
// extract icons from data
// return icons should be unique, and not cached or cached but url changed
export function getDiffIcons(data, getIcon, cachedIcons) {
if (!data || !getIcon) {
return null;
}
cachedIcons = cachedIcons || {};
const icons = {};
const { iterable, objectInfo } = createIterable(data);
for (const object of iterable) {
objectInfo.index++;
const icon = getIcon(object, objectInfo);
const id = getIconId(icon);
if (!icon) {
throw new Error('Icon is missing.');
}
if (!icon.url) {
throw new Error('Icon url is missing.');
}
if (!icons[id] && (!cachedIcons[id] || icon.url !== cachedIcons[id].url)) {
icons[id] = { ...icon, source: object, sourceIndex: objectInfo.index };
}
}
return icons;
}
export default class IconManager {
constructor(gl, { onUpdate = noop, onError = noop }) {
this._loadOptions = null;
this._texture = null;
this._externalTexture = null;
this._mapping = {};
/** count of pending requests to fetch icons */
this._pendingCount = 0;
this._autoPacking = false;
// / internal state used for autoPacking
this._xOffset = 0;
this._yOffset = 0;
this._rowHeight = 0;
this._buffer = DEFAULT_BUFFER;
this._canvasWidth = DEFAULT_CANVAS_WIDTH;
this._canvasHeight = 0;
this._canvas = null;
this.gl = gl;
this.onUpdate = onUpdate;
this.onError = onError;
}
finalize() {
this._texture?.delete();
}
getTexture() {
return this._texture || this._externalTexture;
}
getIconMapping(icon) {
const id = this._autoPacking ? getIconId(icon) : icon;
return this._mapping[id] || {};
}
setProps({ loadOptions, autoPacking, iconAtlas, iconMapping }) {
if (loadOptions) {
this._loadOptions = loadOptions;
}
if (autoPacking !== undefined) {
this._autoPacking = autoPacking;
}
if (iconMapping) {
this._mapping = iconMapping;
}
if (iconAtlas) {
this._texture?.delete();
this._texture = null;
this._externalTexture = iconAtlas;
}
}
get isLoaded() {
return this._pendingCount === 0;
}
packIcons(data, getIcon) {
if (!this._autoPacking || typeof document === 'undefined') {
return;
}
const icons = Object.values(getDiffIcons(data, getIcon, this._mapping) || {});
if (icons.length > 0) {
// generate icon mapping
const { mapping, xOffset, yOffset, rowHeight, canvasHeight } = buildMapping({
icons,
buffer: this._buffer,
canvasWidth: this._canvasWidth,
mapping: this._mapping,
rowHeight: this._rowHeight,
xOffset: this._xOffset,
yOffset: this._yOffset
});
this._rowHeight = rowHeight;
this._mapping = mapping;
this._xOffset = xOffset;
this._yOffset = yOffset;
this._canvasHeight = canvasHeight;
// create new texture
if (!this._texture) {
this._texture = new Texture2D(this.gl, {
width: this._canvasWidth,
height: this._canvasHeight,
parameters: DEFAULT_TEXTURE_PARAMETERS
});
}
if (this._texture.height !== this._canvasHeight) {
this._texture = resizeTexture(this._texture, this._canvasWidth, this._canvasHeight);
}
this.onUpdate();
// load images
this._canvas = this._canvas || document.createElement('canvas');
this._loadIcons(icons);
}
}
_loadIcons(icons) {
// This method is only called in the auto packing case, where _canvas is defined
const ctx = this._canvas.getContext('2d');
for (const icon of icons) {
this._pendingCount++;
load(icon.url, ImageLoader, this._loadOptions)
.then(imageData => {
const id = getIconId(icon);
const { x, y, width, height } = this._mapping[id];
const data = resizeImage(ctx, imageData, width, height);
this._texture.setSubImageData({
data,
x,
y,
width,
height
});
// Call to regenerate mipmaps after modifying texture(s)
this._texture.generateMipmap();
this.onUpdate();
})
.catch(error => {
this.onError({
url: icon.url,
source: icon.source,
sourceIndex: icon.sourceIndex,
loadOptions: this._loadOptions,
error
});
})
.finally(() => {
this._pendingCount--;
});
}
}
}