tangram
Version:
WebGL Maps for Vector Tiles
214 lines (184 loc) • 9.76 kB
JavaScript
import Label from './label';
import LabelPoint from './label_point';
import {LabelLineStraight} from './label_line';
import Collision from './collision';
import OBB from '../utils/obb';
import Geo from '../utils/geo';
let visible = {}; // currently visible labels
let prev_visible = {}; // previously visible labels (in last collision run)
export default async function mainThreadLabelCollisionPass (tiles, view_zoom, hide_breach = false) {
// Swap/reset visible label set
prev_visible = visible; // save last visible label set
visible = {}; // initialize new visible label set
// Build label containers from tiles
let containers = buildLabels(tiles, view_zoom);
// Collide all labels in a single group
// TODO: maybe rename tile and style to group/subgroup?
Collision.startTile('main', { apply_repeat_groups: true, return_hidden: true });
Collision.addStyle('main', 'main');
// Adaptive collision grid, using a heuristic based on the tile with the most labels
const max_tile_label_count =
Math.max(0, ...Object.values(tiles)
.map(t => Object.values(t.meshes))
.flat()
.map(meshes => Math.max(0, ...meshes.map(mesh => mesh.labels ? Object.keys(mesh.labels).length : 0)))
);
const grid_divs = Math.floor(max_tile_label_count / Geo.tile_size); // heuristic of label density to tile size
if (grid_divs > 0) {
Collision.initGrid({
anchor: { x: Math.min(...tiles.map(t => t.min.x)), y: Math.min(...tiles.map(t => t.min.y)) },
span: tiles[0].span.x / grid_divs
});
}
else {
Collision.initGrid();
}
const labels = await Collision.collide(containers, 'main', 'main');
// Update label visiblity
let meshes = [];
labels.forEach(container => {
// Hide breach labels (those that cross tile boundaries) while tiles are loading, unless they
// were previously visible (otherwise fully loaded/collided breach labels will flicker in and out
// when new tiles load, even if they aren't adjacent)
let show = 0;
if (container.show === true &&
(!hide_breach || !container.label.breach || prev_visible[container.label.id])) {
show = 1;
}
if (show) {
visible[container.label.id] = true; // track visible labels
}
let changed = true; // check if label visibility changed on this collision pass
container.ranges.forEach(r => {
if (!changed) {
return; // skip rest of label if state hasn't changed
}
let mesh = container.mesh;
if (!mesh.valid) {
return;
}
let off = mesh.vertex_layout.offset.a_shape; // byte offset (within each vertex) of attribute
let stride = mesh.vertex_layout.stride; // byte stride per vertex
for (let i=0; i < r[1]; i++) {
// NB: +6 is because attribute is a short int (2 bytes each), and we're skipping to 3rd element, 6=3*2
if (mesh.vertex_data[r[0] + i * stride + off + 6] === show) {
changed = false;
return; // label hasn't changed states, skip further updates
}
mesh.vertex_data[r[0] + i * stride + off + 6] = show;
}
if (meshes.indexOf(mesh) === -1) {
meshes.push(mesh);
}
});
});
// Upload updated meshes and make them visible
meshes.forEach(mesh => mesh.upload());
tiles.forEach(t => t.swapPendingLabels());
return { labels, containers }; // currently returned for debugging
}
function buildLabels (tiles, view_zoom) {
const labels = {};
let containers = {};
// Collect labels from each tile and turn into new label instances
tiles.forEach(tile => {
const units_per_meter = Geo.unitsPerMeter(tile.coords.z); // scale from tile units to mercator meters
const zoom_scale = Math.pow(2, view_zoom - tile.style_z); // adjust label size by view zoom
const size_scale = units_per_meter * zoom_scale; // scale from tile units to zoom-adjusted meters
const meters_per_pixel = Geo.metersPerPixel(view_zoom);
// First pass: create label instances and centralize collision containers
// Combine existing (previously collided) and pending (waiting to be collided for first time) meshes
const tile_meshes = Object.assign({}, tile.meshes, tile.pending_label_meshes);
for (let style in tile_meshes) {
const meshes = tile_meshes[style];
meshes.forEach(mesh => {
if (mesh.labels) {
for (let label_id in mesh.labels) {
// For proxy tiles, only allow visible labels to be *hidden* by further collisions,
// don't allow new ones to appear. Promotes label stability and prevents thrash
// from different labels (often not thematically relevant given the different zoom level of
// the proxy tile content, e.g. random POIs popping in/out when zooming out to city-wide view).
if (tile.isProxy() && !prev_visible[label_id]) {
continue;
}
const params = mesh.labels[label_id].container.label;
const linked = mesh.labels[label_id].container.linked;
const ranges = mesh.labels[label_id].ranges;
// const debug = Object.assign({}, mesh.labels[label_id].debug, { tile, params, label_id });
let label = labels[label_id] = {};
label.discard = discard.bind(label);
label.build_id = tile.build_id; // original order in which tiles were built
Object.assign(label, params);
label.layout = Object.assign({}, params.layout); // TODO: ideally remove need to copy props here
label.layout.repeat_scale = 0.75; // looser second pass on repeat groups, to weed out repeats near tile edges
label.layout.repeat_distance = label.layout.repeat_distance || 0;
label.layout.repeat_distance /= size_scale; // TODO: where should this be scaled?
label.position = [ // don't overwrite referenced values
label.position[0] / units_per_meter + tile.min.x,
label.position[1] / units_per_meter + tile.min.y
];
label.unit_scale = meters_per_pixel;
if (label.type === 'point') { // TODO: move to integer constants to avoid excess string copies
LabelPoint.prototype.updateBBoxes.call(label);
}
else if (label.type === 'straight') {
LabelLineStraight.prototype.updateBBoxes.call(label, label.position, label.size, label.angle, label.angle, label.offset);
}
else if (params.obbs) {
// NB: this is a very rough approximation of curved label collision at intermediate zooms,
// because the position/scale of each collision box isn't correctly updated; however,
// it's good enough to provide some additional label coverage, with less overhead
const obbs = params.obbs.map(o => {
let { x, y, a, w, h } = o;
x = x / units_per_meter + tile.min.x;
y = y / units_per_meter + tile.min.y;
w /= size_scale;
h /= size_scale;
return new OBB(x, y, a, w, h);
});
label.obbs = obbs;
label.aabbs = obbs.map(o => o.getExtent());
}
containers[label_id] = {
label,
linked,
ranges,
mesh,
// debug
};
}
}
});
}
});
// Resolve links between label containers
for (let c in containers) {
const container = containers[c];
if (container.linked) {
container.linked = containers[container.linked];
}
// NB: if linked label not found, it was discarded in initial tile collision pass
}
// Convert container map to array
containers = Object.keys(containers).map(k => containers[k]);
return containers;
}
// Generic discard function for labels, does simple occlusion with one or more bounding boxes
// (no additional logic to try alternate anchors or other layout options, etc.)
function discard (bboxes, exclude = null) {
if (this.obb) { // single collision box
return Label.prototype.occluded.call(this, bboxes, exclude);
}
else if (this.obbs) { // mutliple collision boxes
for (let i = 0; i < this.obbs.length; i++){
let aabb = this.aabbs[i];
let obb = this.obbs[i];
let obj = { aabb, obb };
let should_discard = Label.prototype.occluded.call(obj, bboxes, exclude);
if (should_discard) {
return true;
}
}
}
return false;
}