mapbox-gl
Version:
A WebGL interactive maps library
221 lines (178 loc) • 7.04 kB
JavaScript
// @flow
import {warnOnce, nextPowerOfTwo} from '../util/util.js';
import {AlphaImage} from '../util/image.js';
import {register} from '../util/web_worker_transfer.js';
import type {SpritePosition, SpritePositions} from '../util/image.js';
type DashRange = {|
isDash: boolean,
left: number,
right: number,
zeroLength: boolean
|};
/**
* A LineAtlas lets us reuse rendered dashed lines
* by writing many of them to a texture and then fetching their positions
* using .getDash.
*
* @param {number} width
* @param {number} height
* @private
*/
class LineAtlas {
width: number;
height: number;
nextRow: number;
image: AlphaImage;
positions: SpritePositions;
uploaded: boolean;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
this.nextRow = 0;
this.image = new AlphaImage({width, height});
this.positions = {};
this.uploaded = false;
}
/**
* Get a dash line pattern.
*
* @param {Array<number>} dasharray
* @param {string} lineCap the type of line caps to be added to dashes
* @returns {Object} position of dash texture in { y, height, width }
* @private
*/
getDash(dasharray: Array<number>, lineCap: string): SpritePosition {
const key = this.getKey(dasharray, lineCap);
return this.positions[key];
}
trim() {
const width = this.width;
const height = this.height = nextPowerOfTwo(this.nextRow);
this.image.resize({width, height});
}
getKey(dasharray: Array<number>, lineCap: string): string {
return dasharray.join(',') + lineCap;
}
getDashRanges(dasharray: Array<number>, lineAtlasWidth: number, stretch: number): Array<DashRange> {
// If dasharray has an odd length, both the first and last parts
// are dashes and should be joined seamlessly.
const oddDashArray = dasharray.length % 2 === 1;
const ranges = [];
let left = oddDashArray ? -dasharray[dasharray.length - 1] * stretch : 0;
let right = dasharray[0] * stretch;
let isDash = true;
ranges.push({left, right, isDash, zeroLength: dasharray[0] === 0});
let currentDashLength = dasharray[0];
for (let i = 1; i < dasharray.length; i++) {
isDash = !isDash;
const dashLength = dasharray[i];
left = currentDashLength * stretch;
currentDashLength += dashLength;
right = currentDashLength * stretch;
ranges.push({left, right, isDash, zeroLength: dashLength === 0});
}
return ranges;
}
addRoundDash(ranges: Array<DashRange>, stretch: number, n: number) {
const halfStretch = stretch / 2;
for (let y = -n; y <= n; y++) {
const row = this.nextRow + n + y;
const index = this.width * row;
let currIndex = 0;
let range = ranges[currIndex];
for (let x = 0; x < this.width; x++) {
if (x / range.right > 1) { range = ranges[++currIndex]; }
const distLeft = Math.abs(x - range.left);
const distRight = Math.abs(x - range.right);
const minDist = Math.min(distLeft, distRight);
let signedDistance;
const distMiddle = y / n * (halfStretch + 1);
if (range.isDash) {
const distEdge = halfStretch - Math.abs(distMiddle);
signedDistance = Math.sqrt(minDist * minDist + distEdge * distEdge);
} else {
signedDistance = halfStretch - Math.sqrt(minDist * minDist + distMiddle * distMiddle);
}
this.image.data[index + x] = Math.max(0, Math.min(255, signedDistance + 128));
}
}
}
addRegularDash(ranges: Array<DashRange>, capLength: number) {
// Collapse any zero-length range
// Collapse neighbouring same-type parts into a single part
for (let i = ranges.length - 1; i >= 0; --i) {
const part = ranges[i];
const next = ranges[i + 1];
if (part.zeroLength) {
ranges.splice(i, 1);
} else if (next && next.isDash === part.isDash) {
next.left = part.left;
ranges.splice(i, 1);
}
}
// Combine the first and last parts if possible
const first = ranges[0];
const last = ranges[ranges.length - 1];
if (first.isDash === last.isDash) {
first.left = last.left - this.width;
last.right = first.right + this.width;
}
const index = this.width * this.nextRow;
let currIndex = 0;
let range = ranges[currIndex];
for (let x = 0; x < this.width; x++) {
if (x / range.right > 1) {
range = ranges[++currIndex];
}
const distLeft = Math.abs(x - range.left);
const distRight = Math.abs(x - range.right);
const minDist = Math.min(distLeft, distRight);
const signedDistance = (range.isDash ? minDist : -minDist) + capLength;
this.image.data[index + x] = Math.max(0, Math.min(255, signedDistance + 128));
}
}
addDash(dasharray: Array<number>, lineCap: string): null | SpritePosition {
const key = this.getKey(dasharray, lineCap);
if (this.positions[key]) return this.positions[key];
const round = lineCap === 'round';
const n = round ? 7 : 0;
const height = 2 * n + 1;
if (this.nextRow + height > this.height) {
warnOnce('LineAtlas out of space');
return null;
}
// dasharray is empty, draws a full line (no dash or no gap length represented, default behavior)
if (dasharray.length === 0) {
// insert a single dash range in order to draw a full line
dasharray.push(1);
}
let length = 0;
for (let i = 0; i < dasharray.length; i++) {
if (dasharray[i] < 0) {
warnOnce('Negative value is found in line dasharray, replacing values with 0');
dasharray[i] = 0;
}
length += dasharray[i];
}
if (length !== 0) {
const stretch = this.width / length;
const ranges = this.getDashRanges(dasharray, this.width, stretch);
if (round) {
this.addRoundDash(ranges, stretch, n);
} else {
const capLength = lineCap === 'square' ? 0.5 * stretch : 0;
this.addRegularDash(ranges, capLength);
}
}
const y = this.nextRow + n;
this.nextRow += height;
const pos = {
tl: [y, n],
br: [length, 0]
};
this.positions[key] = pos;
return pos;
}
}
register(LineAtlas, 'LineAtlas');
export default LineAtlas;