leaflet.tilelayer.glcolorscale
Version:
Custom Leaflet TileLayer using WebGL to colorize floating-point pixels according to a specified color scale
155 lines (135 loc) • 4.67 kB
text/typescript
import {
flatMap,
isEmpty,
} from 'lodash-es';
import REGL from 'regl';
import {
TextureCoordinates,
TileCoordinates,
} from './types';
import * as util from './util';
const MAX_TEXTURE_DIMENSION = 1024;
export default class TextureManager {
// width/height of a tile in pixels
readonly tileSize: number;
// dimension of the texture in number of tiles
readonly tilesAcross: number;
// dimension of the texture in number of pixels
readonly pixelsAcross: number;
// number of tiles that will fit in the texture
readonly tileCapacity: number;
// the Regl Texture2D object
readonly texture: REGL.Texture2D;
// map of tile coordinates to texture coordinates
// (key is hash string of tile coordinates: 'x:y:z')
protected contents: Map<string, TextureCoordinates>;
// texture coordinates positions that are currently available
protected available: TextureCoordinates[];
constructor(regl: REGL.Regl, tileSize: number) {
const tilesAcross = Math.floor(MAX_TEXTURE_DIMENSION / tileSize);
const pixelsAcross = tilesAcross * tileSize;
const tileCapacity = tilesAcross * tilesAcross;
const texture = regl.texture({
width: pixelsAcross,
height: pixelsAcross,
format: 'rgba',
type: 'uint8',
});
const contents = new Map<string, TextureCoordinates>();
const available = this.allTextureCoordinates(tilesAcross, tileSize);
Object.assign(this, {
tileSize,
tilesAcross,
pixelsAcross,
tileCapacity,
texture,
contents,
available,
});
}
addTile(
tileCoordinates: TileCoordinates,
data: ArrayBufferView,
): [TextureCoordinates, TextureCoordinates] {
const {
available,
contents,
texture,
tileSize,
} = this;
const hashKey = this.hashTileCoordinates(tileCoordinates);
if (contents.has(hashKey)) {
const textureCoordinates = contents.get(hashKey) as TextureCoordinates;
// We use a least-recently-used eviction policy for the tile cache. Map iterators are
// convenient for this, because they return entries in insertion order. But for this to work
// as expected, every time we access a tile, we need to reinsert it so that it moves to the
// end of that insertion-order list.
contents.delete(hashKey);
contents.set(hashKey, textureCoordinates);
return this.formatOutputTextureCoordinates(textureCoordinates);
}
if (isEmpty(available)) {
// Get the first key inserted. Map.prototype.keys() produces an iterable iterator over the keys
// in the order of insertion, so we can just use the iterator's first value.
const firstInsertedKey = contents.keys().next().value;
this.removeByHashKey(firstInsertedKey);
}
// remove from list of available positions
const textureCoordinates = available.pop() as TextureCoordinates;
// store mapping of tile to texture coordinates
contents.set(hashKey, textureCoordinates);
const { x: textureX, y: textureY } = textureCoordinates;
texture.subimage({
data,
width: tileSize,
height: tileSize,
}, textureX, textureY);
return this.formatOutputTextureCoordinates(textureCoordinates);
}
removeTile(tileCoordinates: TileCoordinates) {
this.removeByHashKey(this.hashTileCoordinates(tileCoordinates));
}
clearTiles() {
for (const hashKey of Array.from(this.contents.keys())) {
this.removeByHashKey(hashKey);
}
}
destroy() {
this.texture.destroy();
}
protected removeByHashKey(hashKey: string) {
// This method only removes the key. The pixel data remains in the texture.
if (this.contents.has(hashKey)) {
const textureCoordinates = this.contents.get(hashKey) as TextureCoordinates;
this.contents.delete(hashKey);
this.available.push(textureCoordinates);
}
}
protected formatOutputTextureCoordinates(
textureCoordinates: TextureCoordinates,
): [TextureCoordinates, TextureCoordinates] {
const { x, y } = textureCoordinates;
const { pixelsAcross, tileSize } = this;
return [
{
x: x / pixelsAcross,
y: y / pixelsAcross,
},
{
x: (x + tileSize) / pixelsAcross,
y: (y + tileSize) / pixelsAcross,
},
];
}
protected hashTileCoordinates({ x, y, z }: TileCoordinates): string {
return `${x}:${y}:${z}`;
}
protected allTextureCoordinates(tilesAcross: number, tileSize: number): TextureCoordinates[] {
return flatMap(util.range(tilesAcross), x =>
util.range(tilesAcross).map(y => ({
x: x * tileSize,
y: y * tileSize,
})),
);
}
}