UNPKG

tangram

Version:
952 lines (816 loc) 42.1 kB
// 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); } });