tangram
Version:
WebGL Maps for Vector Tiles
952 lines (816 loc) • 42.1 kB
JavaScript
// Point + text label rendering style
import log from '../../utils/log';
import {Style} from '../style';
import StyleParser from '../style_parser';
import gl from '../../gl/constants'; // web workers don't have access to GL context, so import all GL constants
import VertexLayout from '../../gl/vertex_layout';
import { buildQuadForPoint } from '../../builders/points';
import Texture from '../../gl/texture';
import Geo from '../../utils/geo';
import Collision from '../../labels/collision';
import LabelPoint from '../../labels/label_point';
import placePointsOnLine from '../../labels/point_placement';
import {TextLabels} from '../text/text_labels';
import {VIEW_PAN_SNAP_TIME} from '../../scene/view';
import debugSettings from '../../utils/debug_settings';
import points_vs from './points_vertex.glsl';
import points_fs from './points_fragment.glsl';
const PLACEMENT = LabelPoint.PLACEMENT;
export const Points = Object.create(Style);
const SHADER_POINT_VARIANT = '__shader_point';
// texture types
const TANGRAM_POINT_TYPE_TEXTURE = 1; // style texture/sprites (assigned by user)
const TANGRAM_POINT_TYPE_LABEL = 2; // labels (generated by rendering labels to canvas)
const TANGRAM_POINT_TYPE_SHADER = 3; // point (drawn in shader)
// default point size in pixels
const DEFAULT_POINT_SIZE = 16;
// Mixin text label methods
Object.assign(Points, TextLabels);
Object.assign(Points, {
name: 'points',
built_in: true,
vertex_shader_src: points_vs,
fragment_shader_src: points_fs,
selection: true, // enable feature selection
collision: true, // style includes a collision pass
blend: 'overlay', // overlays drawn on top of all other styles, with blending
init(options = {}) {
Style.init.call(this, options);
// Shader defines
this.setupDefines();
// Include code for SDF-drawn shader points
this.defines.TANGRAM_HAS_SHADER_POINTS = true;
// texture types
this.defines.TANGRAM_POINT_TYPE_TEXTURE = TANGRAM_POINT_TYPE_TEXTURE;
this.defines.TANGRAM_POINT_TYPE_LABEL = TANGRAM_POINT_TYPE_LABEL;
this.defines.TANGRAM_POINT_TYPE_SHADER = TANGRAM_POINT_TYPE_SHADER;
this.collision_group_points = this.name+'-points';
this.collision_group_text = this.name+'-text';
// Stenciling proxy tiles (to avoid compounding alpha artifacts) doesn't work well with
// points/text labels, which have pure transparent pixels that interfere with the stencil buffer,
// causing a "cut-out"/"x-ray" effect (preventing pixels that would usually be covered by proxy tiles
// underneath from being rendered).
this.stencil_proxy_tiles = false;
this.reset();
},
// Setup defines common to points base and child (text) styles
setupDefines () {
// If we're not rendering as overlay, we need a layer attribute
if (this.blend !== 'overlay') {
this.defines.TANGRAM_LAYER_ORDER = true;
}
// Fade in labels
if (debugSettings.suppress_label_fade_in === true) {
this.fade_in_time = 0;
this.defines.TANGRAM_FADE_IN_RATE = null;
}
else {
this.fade_in_time = 0.15; // time in seconds
this.defines.TANGRAM_FADE_IN_RATE = 1 / this.fade_in_time;
}
// Snap points to pixel grid after panning stop
if (debugSettings.suppress_label_snap_animation !== true) {
this.defines.TANGRAM_VIEW_PAN_SNAP_RATE = 1 / VIEW_PAN_SNAP_TIME; // inverse time in seconds
}
// Show hidden labels for debugging
if (debugSettings.show_hidden_labels === true) {
this.defines.TANGRAM_SHOW_HIDDEN_LABELS = true;
}
// Enable wireframe for debugging
if (debugSettings.wireframe === true) {
this.defines.TANGRAM_WIREFRAME = true;
}
},
reset () {
this.queues = {};
this.resetText();
this.texture_missing_sprites = {}; // track which missing sprites we've found (reduce dupe log messages)
},
// Override to queue features instead of processing immediately
addFeature (feature, draw, context) {
let tile = context.tile;
if (tile.generation !== this.generation) {
return;
}
// Point styling
let style = {};
style.color = this.parseColor(draw.color, context);
style.texture = draw.texture; // optional point texture, specified in `draw` or at style level
style.label_texture = null; // assigned by labelling code if needed
style.blend_order = draw.blend_order; // copy pre-computed blend order
// require color or texture
if (!style.color && !style.texture) {
return;
}
style.alpha = StyleParser.evalCachedProperty(draw.alpha, context); // optional alpha override
// optional sprite and texture
let sprite_info;
if (this.hasSprites(style)) {
// populate sprite_info object with used sprites
sprite_info = this.parseSprite(style, draw, context);
if (sprite_info) {
style.texcoords = sprite_info.texcoords;
}
else {
// sprites are defined in the style's texture, but none are used in the current layer
log({ level: 'debug', once: true }, `Layer group '${draw.layers.join(', ')}' ` +
`uses a texture '${style.texture}', but doesn't specify which sprite to draw. ` +
'Features that match this layer group won\'t be drawn without specifying the sprite with the ' +
'\'sprite\' or \'sprite_default\' properties. The merged draw parameters for this layer group are:', draw).then(logged => {
if (logged) {
log('debug', `Example feature for layer group '${draw.layers.join(', ')}'`, feature);
}
});
return;
}
} else if (draw.sprite) {
// sprite specified in the draw layer but no sprites defined in the texture
log({ level: 'warn', once: true }, `Layer group '${draw.layers.join(', ')}' ` +
`specifies sprite '${draw.sprite}', but the texture '${draw.texture}' doesn't define any sprites. ` +
'Features that match this layer group won\'t be drawn. The merged draw parameters for this layer group are:', draw);
return;
}
this.calcSize(draw, style, sprite_info, context);
// incorporate outline into size
if (draw.outline) {
style.outline_width = StyleParser.evalCachedProperty(draw.outline.width, context) || StyleParser.defaults.outline.width;
style.outline_color = this.parseColor(draw.outline.color, context);
}
style.outline_edge_pct = 0;
if (style.outline_width && style.outline_color) {
// adjust size and UVs for outline
let outline_width = style.outline_width;
style.size[0] += outline_width;
style.size[1] += outline_width;
style.outline_edge_pct = outline_width / Math.min(style.size[0], style.size[1]) * 2; // UV distance at which outline starts
style.outline_alpha = StyleParser.evalCachedProperty(draw.outline.alpha, context); // optional alpha override
}
// size will be scaled to 16-bit signed int, so max allowed width + height of 256 pixels
style.size[0] = Math.min(style.size[0], 256);
style.size[1] = Math.min(style.size[1], 256);
// Placement strategy
style.placement = draw.placement;
style.placement_min_length_ratio = StyleParser.evalCachedProperty(draw.placement_min_length_ratio, context);
// Spacing parameter (in pixels) to equally space points along a line
if (style.placement === PLACEMENT.SPACED && draw.placement_spacing) {
style.placement_spacing = StyleParser.evalCachedProperty(draw.placement_spacing, context);
}
// Angle parameter (can be a number or the string "auto")
style.angle = StyleParser.evalProperty(draw.angle, context) || 0;
// points can be placed off the ground
style.z = StyleParser.evalCachedDistanceProperty(draw.z, context) || StyleParser.defaults.z;
style.tile_edges = draw.tile_edges; // usually activated for debugging, or rare visualization needs
this.computeLayout(style, feature, draw, context, tile);
// Text styling
let tf =
draw.text &&
draw.text.visible !== false && // explicitly handle `visible` property for nested text
this.parseTextFeature(feature, draw.text, context, tile);
if (Array.isArray(tf)) {
tf = null; // NB: boundary labels not supported for point label attachments, should log warning
log({ level: 'warn', once: true }, `Layer group '${draw.layers.join(', ')}': ` +
'cannot use boundary labels (e.g. \'text_source: { left: ..., right: ... }\') for \'text\' labels attached to \'points\'; ' +
`provided 'text_source' value was ${JSON.stringify(draw.text.text_source)}`);
}
if (tf) {
tf.layout.parent = style; // parent point will apply additional anchor/offset to text
// Text labels have a default priority of 0.5 below their parent point (+0.5, priority is lower-is-better)
// This can be overriden, as long as it is less than or equal to the default
tf.layout.priority = draw.text.priority ? Math.max(tf.layout.priority, style.priority + 0.5) : (style.priority + 0.5);
Collision.addStyle(this.collision_group_text, tile.id);
}
this.queueFeature({ feature, draw, context, style, text_feature: tf }, tile); // queue the feature for later processing
// Register with collision manager
Collision.addStyle(this.collision_group_points, tile.id);
},
// Calcuate the size for the current point feature
calcSize (draw, style, sprite_info, context) {
// point size defined explicitly, or defaults to sprite size, or generic fallback
style.size = draw.size;
if (!style.size) {
// a 'size' property has not been set in the draw layer -
// use the sprite size if it exists and a generic fallback if it doesn't
style.size = (sprite_info && sprite_info.css_size) || [DEFAULT_POINT_SIZE, DEFAULT_POINT_SIZE];
}
else {
// check for a cached size, passing the texture and any sprite references
style.size = StyleParser.evalCachedPointSizeProperty(draw.size, sprite_info, Texture.textures[style.texture], context);
if (style.size == null) {
// the StyleParser couldn't evaluate a sprite size
log({ level: 'warn', once: true }, `Layer group '${draw.layers.join(', ')}': ` +
`'size' (${JSON.stringify(draw.size.value)}) couldn't be interpreted, features that match ` +
'this layer group won\'t be drawn');
return;
}
else if (typeof style.size === 'number') {
style.size = [style.size, style.size]; // convert 1d size to 2d
}
}
},
hasSprites (style) {
return style.texture && Texture.textures[style.texture] && Texture.textures[style.texture].sprites;
},
// Generate a sprite_info object
getSpriteInfo (style, sprite) {
let info = Texture.textures[style.texture].sprites[sprite] && Texture.getSpriteInfo(style.texture, sprite);
if (sprite && !info) {
// track missing sprites (per texture)
this.texture_missing_sprites[style.texture] = this.texture_missing_sprites[style.texture] || {};
if (!this.texture_missing_sprites[style.texture][sprite]) { // only log each missing sprite once
log('debug', `Style: in style '${this.name}', could not find sprite '${sprite}' for texture '${style.texture}'`);
this.texture_missing_sprites[style.texture][sprite] = true;
}
}
else if (info) {
info.sprite = sprite;
}
return info;
},
// Check a sprite name against available sprites and return a sprite_info object
parseSprite (style, draw, context) {
// check for functions
let sprite = StyleParser.evalProperty(draw.sprite, context);
let sprite_info = this.getSpriteInfo(style, sprite) || this.getSpriteInfo(style, draw.sprite_default);
return sprite_info;
},
// Queue features for deferred processing (collect all features first so we can do collision on the whole group)
queueFeature (q, tile) {
if (!this.tile_data[tile.id] || !this.queues[tile.id]) {
this.startData(tile);
}
this.queues[tile.id] = this.queues[tile.id] || [];
this.queues[tile.id].push(q);
},
// Override
async endData (tile) {
if (tile.canceled) {
log('trace', `Style ${this.name}: stop tile build because tile was canceled: ${tile.key}`);
return null;
}
let queue = this.queues[tile.id];
delete this.queues[tile.id];
// For each point feature, create one or more labels
let text_objs = [];
let point_objs = [];
queue.forEach(q => {
let style = q.style;
let feature = q.feature;
let geometry = feature.geometry;
let feature_labels = this.buildLabels(style.size, geometry, style);
for (let i = 0; i < feature_labels.length; i++) {
let label = feature_labels[i];
let point_obj = {
feature,
draw: q.draw,
context: q.context,
style,
label
};
point_objs.push(point_obj);
if (q.text_feature) {
let text_obj = {
feature,
draw: q.text_feature.draw,
context: q.context,
text: q.text_feature.text,
text_settings_key: q.text_feature.text_settings_key,
layout: q.text_feature.layout,
point_label: label,
linked: point_obj // link so text only renders when parent point is placed
};
text_objs.push(text_obj);
// Unless text feature is optional, create a two-way link so that parent
// point will only render when text is also placed
if (!q.draw.text.optional) {
point_obj.linked = text_obj; // two-way link
}
}
}
});
// Collide both points and text, then build features
const [, { labels, texts, textures }] = await Promise.all([
// Points
Collision.collide(point_objs, this.collision_group_points, tile.id).then(point_objs => {
point_objs.forEach(q => {
this.feature_style = q.style;
this.feature_style.label = q.label;
this.feature_style.linked = q.linked; // TODO: move linked into label to avoid extra prop tracking?
Style.addFeature.call(this, q.feature, q.draw, q.context);
});
}),
// Labels
this.collideAndRenderTextLabels(tile, this.collision_group_text, text_objs)
]);
// Process labels
if (labels && texts) {
// Build queued features
labels.forEach(q => {
let text_settings_key = q.text_settings_key;
let text_info = texts[text_settings_key] && texts[text_settings_key][q.text];
// setup styling object expected by Style class
let style = this.feature_style;
style.label = q.label;
style.linked = q.linked; // TODO: move linked into label to avoid extra prop tracking?
style.size = text_info.size.logical_size;
style.texcoords = text_info.align[q.label.align].texcoords;
style.label_texture = textures[text_info.align[q.label.align].texture_id];
style.blend_order = q.draw.blend_order; // copy blend order from parent point
Style.addFeature.call(this, q.feature, q.draw, q.context);
});
}
this.freeText(tile);
// Finish tile mesh
const tile_data = await Style.endData.call(this, tile);
// Attach tile-specific label atlas to mesh as a texture uniform
if (tile_data && textures && textures.length) {
tile_data.textures = tile_data.textures || [];
tile_data.textures.push(...textures); // assign texture ownership to tile
}
return tile_data;
},
_preprocess (draw) {
draw.color = StyleParser.createColorPropertyCache(draw.color);
draw.alpha = StyleParser.createPropertyCache(draw.alpha);
draw.texture = (draw.texture !== undefined ? draw.texture : this.texture); // optional or default texture
draw.blend_order = this.getBlendOrderForDraw(draw); // from draw block, or fall back on default style blend order
if (draw.outline) {
draw.outline.color = StyleParser.createColorPropertyCache(draw.outline.color);
draw.outline.alpha = StyleParser.createPropertyCache(draw.outline.alpha);
draw.outline.width = StyleParser.createPropertyCache(draw.outline.width, StyleParser.parsePositiveNumber);
}
draw.z = StyleParser.createPropertyCache(draw.z, StyleParser.parseUnits);
// Size (1d value or 2d array)
try {
draw.size = StyleParser.createPointSizePropertyCache(draw.size, draw.texture);
}
catch(e) {
log({ level: 'warn', once: true }, `Layer group '${draw.layers.join(', ')}': ` +
`${e} (${JSON.stringify(draw.size)}), features that match this layer group won't be drawn.`);
return null;
}
// Offset (2d array)
draw.offset = StyleParser.createPropertyCache(draw.offset,
v => Array.isArray(v) && v.map(StyleParser.parseNumber)
);
// Buffer (1d value or or 2d array) - must be >= 0
draw.buffer = StyleParser.createPropertyCache(draw.buffer,
v => (Array.isArray(v) ? v : [v, v]).map(StyleParser.parsePositiveNumber)
);
// Repeat rules - no repeat limitation for points by default
draw.repeat_distance = StyleParser.createPropertyCache(draw.repeat_distance, StyleParser.parseNumber);
// Placement strategies
draw.placement = PLACEMENT[draw.placement && draw.placement.toUpperCase()];
if (draw.placement == null) {
draw.placement = PLACEMENT.VERTEX;
}
draw.placement_spacing = draw.placement_spacing != null ? draw.placement_spacing : 80; // default spacing
draw.placement_spacing = StyleParser.createPropertyCache(draw.placement_spacing, StyleParser.parsePositiveNumber);
draw.placement_min_length_ratio = draw.placement_min_length_ratio != null ? draw.placement_min_length_ratio : 1;
draw.placement_min_length_ratio = StyleParser.createPropertyCache(draw.placement_min_length_ratio, StyleParser.parsePositiveNumber);
if (typeof draw.angle === 'number') {
draw.angle = draw.angle * Math.PI / 180; // convert static value to radians
}
else if (typeof draw.angle === 'function') {
// convert function return value to radians
const angle_func = draw.angle;
draw.angle = context => angle_func(context) * Math.PI / 180;
}
else {
draw.angle = draw.angle || 0; // angle can be a string like "auto" (use angle of geometry)
}
// Optional text styling
draw.text = this.preprocessText(draw.text); // will return null if valid text styling wasn't provided
if (draw.text) {
draw.text.key = draw.key; // inherits parent properties
draw.text.group = draw.group;
draw.text.layers = draw.layers;
draw.text.order = draw.order;
draw.text.blend_order = draw.blend_order;
draw.text.repeat_group = (draw.text.repeat_group != null ? draw.text.repeat_group : draw.repeat_group);
draw.text.anchor = draw.text.anchor || this.default_anchor;
draw.text.optional = (typeof draw.text.optional === 'boolean') ? draw.text.optional : false; // default text to required
draw.text.interactive = draw.text.interactive || draw.interactive; // inherits from point
}
return draw;
},
// Default to trying all anchor placements
default_anchor: ['bottom', 'top', 'right', 'left'],
// Compute label layout-related properties
computeLayout (target, feature, draw, context, tile) {
let layout = target || {};
layout.id = feature;
layout.units_per_pixel = tile.units_per_pixel || 1;
// collision flag
layout.collide = (draw.collide === false) ? false : true;
// label anchors (point labels only)
// label position will be adjusted in the given direction, relative to its original point
// one of: left, right, top, bottom, top-left, top-right, bottom-left, bottom-right
layout.anchor = draw.anchor;
// label offset and buffer in pixel (applied in screen space)
layout.offset = StyleParser.evalCachedProperty(draw.offset, context) || StyleParser.zeroPair;
layout.buffer = StyleParser.evalCachedProperty(draw.buffer, context) || StyleParser.zeroPair;
// repeat rules
layout.repeat_distance = StyleParser.evalCachedProperty(draw.repeat_distance, context);
if (layout.repeat_distance) {
layout.repeat_distance *= layout.units_per_pixel;
layout.repeat_scale = 1; // initial repeat pass in tile with full scale
if (typeof draw.repeat_group === 'function') {
layout.repeat_group = draw.repeat_group(context); // dynamic repeat group
}
else {
// default to top-level layer name
// (e.g. all labels under `roads` layer, including sub-layers, are in one repeat group)
layout.repeat_group = draw.repeat_group || context.layer;
}
}
// label priority (lower is higher)
let priority = draw.priority;
if (priority != null) {
if (typeof priority === 'function') {
priority = priority(context);
}
}
else {
priority = -1 >>> 0; // default to max priority value if none set
}
layout.priority = priority;
return layout;
},
// Implements label building for TextLabels mixin
buildTextLabels (tile, feature_queue) {
let labels = [];
for (let f=0; f < feature_queue.length; f++) {
let fq = feature_queue[f];
let text_info = this.texts[tile.id][fq.text_settings_key][fq.text];
let size = text_info.size.collision_size;
fq.label = new LabelPoint(fq.point_label.position, size, fq.layout);
labels.push(fq);
}
return labels;
},
// Builds one or more point labels for a geometry
buildLabels (size, geometry, layout) {
let labels = [];
if (geometry.type === 'Point') {
labels.push(new LabelPoint(geometry.coordinates, size, layout, layout.angle));
}
else if (geometry.type === 'MultiPoint') {
let points = geometry.coordinates;
for (let i = 0; i < points.length; ++i) {
let point = points[i];
labels.push(new LabelPoint(point, size, layout, layout.angle));
}
}
else if (geometry.type === 'LineString') {
let line = geometry.coordinates;
let point_labels = placePointsOnLine(line, size, layout);
for (let i = 0; i < point_labels.length; ++i) {
labels.push(point_labels[i]);
}
}
else if (geometry.type === 'MultiLineString') {
let lines = geometry.coordinates;
for (let ln = 0; ln < lines.length; ln++) {
let line = lines[ln];
let point_labels = placePointsOnLine(line, size, layout);
for (let i = 0; i < point_labels.length; ++i) {
labels.push(point_labels[i]);
}
}
}
else if (geometry.type === 'Polygon') {
// Point at polygon centroid (of outer ring)
if (layout.placement === PLACEMENT.CENTROID) {
let centroid = Geo.centroid(geometry.coordinates);
if (centroid) { // skip degenerate polygons
labels.push(new LabelPoint(centroid, size, layout, layout.angle));
}
}
// Point at each polygon vertex (all rings)
else {
let rings = geometry.coordinates;
for (let ln = 0; ln < rings.length; ln++) {
let point_labels = placePointsOnLine(rings[ln], size, layout);
for (let i = 0; i < point_labels.length; ++i) {
labels.push(point_labels[i]);
}
}
}
}
else if (geometry.type === 'MultiPolygon') {
if (layout.placement === PLACEMENT.CENTROID) {
let centroid = Geo.multiCentroid(geometry.coordinates);
if (centroid) { // skip degenerate polygons
labels.push(new LabelPoint(centroid, size, layout, layout.angle));
}
}
else {
let polys = geometry.coordinates;
for (let p = 0; p < polys.length; p++) {
let rings = polys[p];
for (let ln = 0; ln < rings.length; ln++) {
let point_labels = placePointsOnLine(rings[ln], size, layout);
for (let i = 0; i < point_labels.length; ++i) {
labels.push(point_labels[i]);
}
}
}
}
}
return labels;
},
/**
* A "template" that sets constant attibutes for each vertex, which is then modified per vertex or per feature.
* A plain JS array matching the order of the vertex layout.
*/
makeVertexTemplate(style, mesh, add_custom_attribs = true) {
let i = 0;
// a_position.xyz - vertex position
// a_position.w - layer order
this.vertex_template[i++] = 0;
this.vertex_template[i++] = 0;
this.vertex_template[i++] = style.z || 0;
this.vertex_template[i++] = this.scaleOrder(style.order);
// a_shape.xy - size of point in pixels (scaling vector)
// a_shape.z - angle of point
// a_shape.w - show/hide flag
this.vertex_template[i++] = 0;
this.vertex_template[i++] = 0;
this.vertex_template[i++] = 0;
this.vertex_template[i++] = style.label.layout.collide ? 0 : 1; // set initial label hide/show state
// a_texcoord.xy - texture coords
if (!mesh.variant.shader_point) {
this.vertex_template[i++] = 0;
this.vertex_template[i++] = 0;
}
// a_offset.xy - offset of point from center, in pixels
this.vertex_template[i++] = 0;
this.vertex_template[i++] = 0;
// a_color.rgba - feature color
const color = style.color || StyleParser.defaults.color;
this.vertex_template[i++] = color[0] * 255;
this.vertex_template[i++] = color[1] * 255;
this.vertex_template[i++] = color[2] * 255;
this.vertex_template[i++] = (style.alpha != null ? style.alpha : color[3]) * 255;
// a_selection_color.rgba - selection color
if (mesh.variant.selection) {
this.vertex_template[i++] = style.selection_color[0] * 255;
this.vertex_template[i++] = style.selection_color[1] * 255;
this.vertex_template[i++] = style.selection_color[2] * 255;
this.vertex_template[i++] = style.selection_color[3] * 255;
}
// point outline
if (mesh.variant.shader_point) {
// a_outline_color.rgba - outline color
const outline_color = style.outline_color || StyleParser.defaults.outline.color;
this.vertex_template[i++] = outline_color[0] * 255;
this.vertex_template[i++] = outline_color[1] * 255;
this.vertex_template[i++] = outline_color[2] * 255;
this.vertex_template[i++] = (style.outline_alpha != null ? style.outline_alpha : outline_color[3]) * 255;
// a_outline_edge - point outline edge (as % of point size where outline begins)
this.vertex_template[i++] = style.outline_edge_pct || StyleParser.defaults.outline.width;
}
if (add_custom_attribs) {
this.addCustomAttributesToVertexTemplate(style, i);
}
return this.vertex_template;
},
buildQuad(point, size, angle, angles, pre_angles, offset, offsets, texcoords, curve, vertex_data, vertex_template) {
if (size[0] <= 0 || size[1] <= 0) {
return 0; // size must be positive
}
return buildQuadForPoint(
point,
vertex_data,
vertex_template,
vertex_data.vertex_layout.index,
size,
offset,
offsets,
pre_angles,
angle * 4096, // angle values have a 12-bit fraction
angles,
texcoords,
curve
);
},
// Build quad for point sprite
build (style, context) {
let label = style.label;
if (label.type === 'curved') {
return this.buildCurvedLabel(label, style, context);
}
else {
return this.buildStraightLabel(label, style, context);
}
},
buildStraightLabel (label, style, context) {
let mesh = this.getTileMesh(context.tile, this.meshVariantTypeForDraw(style));
let vertex_template = this.makeVertexTemplate(style, mesh);
let size, texcoords;
if (label.type !== 'point') {
size = style.size[label.type];
texcoords = style.texcoords[label.type].texcoord;
}
else {
size = style.size;
texcoords = style.texcoords;
}
// setup style or label texture if applicable
mesh.uniforms = mesh.uniforms || {};
if (style.label_texture) {
mesh.uniforms.u_texture = style.label_texture;
mesh.uniforms.u_point_type = TANGRAM_POINT_TYPE_LABEL;
mesh.uniforms.u_apply_color_blocks = false;
}
else if (style.texture) {
mesh.uniforms.u_texture = style.texture;
mesh.uniforms.u_point_type = TANGRAM_POINT_TYPE_TEXTURE;
mesh.uniforms.u_apply_color_blocks = true;
}
else {
mesh.uniforms.u_texture = Texture.default; // ensure a tetxure is always bound to avoid GL warnings ('no texture bound to unit' in Chrome)
mesh.uniforms.u_point_type = TANGRAM_POINT_TYPE_SHADER;
mesh.uniforms.u_apply_color_blocks = true;
}
let offset = label.offset;
// TODO: instead of passing null, pass arrays with fingerprintable values
// This value is checked in the shader to determine whether to apply curving logic
let geom_count = this.buildQuad(
label.position, // position
size, // size in pixels
label.angle, // angle in radians
null, // placeholder for multiple angles
null, // placeholder for multiple pre_angles
offset, // offset from center in pixels
null, // placeholder for multiple offsets
texcoords, // texture UVs
false, // if curved boolean
mesh.vertex_data, vertex_template // VBO and data for current vertex
);
// track label mesh buffer data
const linked = (style.linked && style.linked.label.id);
this.trackLabel(label, linked, mesh, geom_count, context);
return geom_count;
},
buildCurvedLabel (label, style, context) {
let mesh, vertex_template;
let geom_count = 0;
// two passes for stroke and fill, where stroke needs to be drawn first (painter's algorithm)
// this ensures strokes don't overlap on other fills
// pass for stroke
for (let i = 0; i < label.num_segments; i++){
let size = style.size[label.type][i];
let texcoord_stroke = style.texcoords_stroke[i];
// re-point to correct label texture
style.label_texture = style.label_textures[i];
mesh = this.getTileMesh(context.tile, this.meshVariantTypeForDraw(style));
vertex_template = this.makeVertexTemplate(style, mesh);
// add label texture uniform if needed
mesh.uniforms = mesh.uniforms || {};
mesh.uniforms.u_texture = style.label_texture;
mesh.uniforms.u_point_type = TANGRAM_POINT_TYPE_LABEL;
mesh.uniforms.u_apply_color_blocks = false;
let offset = label.offset || [0,0];
let position = label.position;
let angles = label.angles[i];
let offsets = label.offsets[i];
let pre_angles = label.pre_angles[i];
let seg_count = this.buildQuad(
position, // position
size, // size in pixels
label.angle, // angle in degrees
angles, // angles per segment
pre_angles, // pre_angle array (rotation applied before offseting)
offset, // offset from center in pixels
offsets, // offsets per segment
texcoord_stroke, // texture UVs for stroked text
true, // if curved
mesh.vertex_data, vertex_template // VBO and data for current vertex
);
geom_count += seg_count;
// track label mesh buffer data
const linked = (style.linked && style.linked.label.id);
this.trackLabel(label, linked, mesh, seg_count, context);
}
// pass for fill
for (let i = 0; i < label.num_segments; i++){
let size = style.size[label.type][i];
let texcoord = style.texcoords[label.type][i].texcoord;
// re-point to correct label texture
style.label_texture = style.label_textures[i];
mesh = this.getTileMesh(context.tile, this.meshVariantTypeForDraw(style));
vertex_template = this.makeVertexTemplate(style, mesh);
// add label texture uniform if needed
mesh.uniforms = mesh.uniforms || {};
mesh.uniforms.u_texture = style.label_texture;
mesh.uniforms.u_point_type = TANGRAM_POINT_TYPE_LABEL;
mesh.uniforms.u_apply_color_blocks = false;
let offset = label.offset || [0,0];
let position = label.position;
let angles = label.angles[i];
let offsets = label.offsets[i];
let pre_angles = label.pre_angles[i];
let seg_count = this.buildQuad(
position, // position
size, // size in pixels
label.angle, // angle in degrees
angles, // angles per segment
pre_angles, // pre_angle array (rotation applied before offseting)
offset, // offset from center in pixels
offsets, // offsets per segment
texcoord, // texture UVs for fill text
true, // if curved
mesh.vertex_data, vertex_template // VBO and data for current vertex
);
geom_count += seg_count;
// track label mesh buffer data
const linked = (style.linked && style.linked.label.id);
this.trackLabel(label, linked, mesh, seg_count, context);
}
return geom_count;
},
// track mesh data for label on main thread, for additional cross-tile collision/repeat passes
trackLabel (label, linked, mesh, geom_count/*, context*/) {
// track if collision is enabled, or if the label is near enough to the tile edge to
// necessitate further repeat checking
if (label.layout.collide || label.may_repeat_across_tiles) {
mesh.labels = mesh.labels || {};
mesh.labels[label.id] = mesh.labels[label.id] || {
container: {
label: label.toJSON(),
linked,
},
ranges: [],
// debug: { // uncomment and pass in context for debugging
// id: context.feature.properties.id,
// name: context.feature.properties.name,
// props: JSON.stringify(context.feature.properties),
// point_type: mesh.uniforms.u_point_type
// }
};
// store byte ranges occupied by label in VBO, so they can be updated on main thread
const vertex_count = geom_count * 2; // geom count is triangles: 2 triangles = 1 quad = 4 vertices
const start = mesh.vertex_data.offset - mesh.vertex_data.stride * vertex_count; // start offset of byte range
mesh.labels[label.id].ranges.push([
start,
vertex_count
]);
}
},
// Override to pass-through to generic point builder
buildLines (lines, style, context) {
return this.build(style, context);
},
buildPoints (points, style, context) {
return this.build(style, context);
},
buildPolygons (points, style, context) {
return this.build(style, context);
},
// Override
// Create or return desired vertex layout permutation based on flags
vertexLayoutForMeshVariant (variant) {
// Vertex layout only depends on shader point flag, so using it as layout key to avoid duplicate layouts
if (this.vertex_layouts[variant.shader_point] == null) {
// Attributes for this mesh variant
// Optional attributes have placeholder values assigned with `static` parameter
// TODO: could support optional attributes for selection and offset, but may not be worth it
// since points generally don't consume much memory anyway
const attribs = [
{ name: 'a_position', size: 4, type: gl.SHORT, normalized: false },
{ name: 'a_shape', size: 4, type: gl.SHORT, normalized: false },
{ name: 'a_texcoord', size: 2, type: gl.UNSIGNED_SHORT, normalized: true, static: (variant.shader_point ? [0, 0] : null) },
{ name: 'a_offset', size: 2, type: gl.SHORT, normalized: false },
{ name: 'a_color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true },
{ name: 'a_selection_color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true, static: (variant.selection ? null : [0, 0, 0, 0]) },
{ name: 'a_outline_color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true, static: (variant.shader_point ? null : [0, 0, 0, 0]) },
{ name: 'a_outline_edge', size: 1, type: gl.FLOAT, normalized: false, static: (variant.shader_point ? null : 0) }
];
this.addCustomAttributesToAttributeList(attribs);
this.vertex_layouts[variant.shader_point] = new VertexLayout(attribs);
}
return this.vertex_layouts[variant.shader_point];
},
// Override
meshVariantTypeForDraw (draw) {
const texture = draw.label_texture || draw.texture || SHADER_POINT_VARIANT; // unique key by texture name
const key = texture + '/' + draw.blend_order;
if (this.variants[key] == null) {
this.variants[key] = {
key,
selection: 1, // TODO: make this vary by draw params
shader_point: (texture === SHADER_POINT_VARIANT), // is shader point
blend_order: draw.blend_order,
mesh_order: (draw.label_texture ? 1 : 0) // put text on top of points (e.g. for highway shields, etc.)
};
}
return this.variants[key]; // return pre-calculated mesh variant
},
// Override
makeMesh (vertex_data, vertex_elements, options = {}) {
// Add label fade time
options = Object.assign({}, options, { fade_in_time: this.fade_in_time });
return Style.makeMesh.call(this, vertex_data, vertex_elements, options);
}
});