tangram
Version:
WebGL Maps for Vector Tiles
600 lines (518 loc) • 27.7 kB
JavaScript
// Line 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 Texture from '../../gl/texture';
import VertexLayout from '../../gl/vertex_layout';
import {buildPolylines} from '../../builders/polylines';
import renderDashArray from './dasharray';
import Geo from '../../utils/geo';
import WorkerBroker from '../../utils/worker_broker';
import hashString from '../../utils/hash';
import polygons_vs from '../polygons/polygons_vertex.glsl';
import polygons_fs from '../polygons/polygons_fragment.glsl';
export const Lines = Object.create(Style);
const DASH_SCALE = 20; // adjustment factor for UV scale to for line dash patterns w/fractional pixel width
Object.assign(Lines, {
name: 'lines',
built_in: true,
vertex_shader_src: polygons_vs,
fragment_shader_src: polygons_fs,
selection: true, // enable feature selection
init() {
Style.init.apply(this, arguments);
// Tell the shader we want a order in vertex attributes, and to extrude lines
this.defines.TANGRAM_EXTRUDE_LINES = true;
this.defines.TANGRAM_TEXTURE_COORDS = true; // texcoords attribute is set to static when not needed
// Additional single-allocated object used for holding outline style as it is processed
// Separate from this.feature_style so that outline properties do not overwrite calculated
// inline properties (outline call is made *within* the inline call)
this.outline_feature_style = {};
this.inline_feature_style = this.feature_style; // save reference to main computed style object
this.dash_textures = {}; // cache previously rendered line dash pattern textures
},
// Calculate width or offset at zoom given in `context`
calcDistance (prop, context) {
return StyleParser.evalCachedDistanceProperty(prop, context) || 0;
},
// Calculate width or offset at next zoom (used for zoom-based interpolation in shader)
calcDistanceNextZoom (prop, context) {
context.zoom++;
let val = this.calcDistance(prop, context);
context.zoom--;
return val;
},
// Calculate width at current and next zoom, and scaling factor between
calcWidth (draw, style, context) {
// line width in meters
let width = this.calcDistance(draw.width, context);
if (width < 0) {
return; // skip lines with negative width
}
let next_width;
if (draw.next_width) {
next_width = this.calcDistanceNextZoom(draw.next_width, context);
}
else {
next_width = width / 2; // when width is static, width at next zoom is just half as many tile units
}
if ((width === 0 && next_width === 0) || next_width < 0) {
return false; // skip lines that don't interpolate to a positive value at next zoom
}
// these values are saved for later calculating the outline width, which needs to add the base line's width
style.width_unscaled = width;
style.next_width_unscaled = next_width;
// calculate relative change in line width between zooms
// interpolate from the line width at the zoom mid-point, towards/away from the previous/next integer zoom
if (draw.next_width) {
next_width *= 2; // NB: a given width is twice as big in screen space at the next zoom
let mid_width = (width + next_width) * 0.5;
style.width = mid_width * context.units_per_meter_overzoom; // width at zoom mid-point
style.width_scale = 1 - (next_width / mid_width);
}
else {
style.width = width * context.units_per_meter_overzoom;
style.width_scale = 0;
}
// optional adjustment to texcoord width based on scale
if (draw.texcoords) {
// when drawing an outline, use the inline's texture scale
// (e.g. keeps dashed outline pattern locked to inline pattern)
if (draw.inline_texcoord_width) {
style.texcoord_width = draw.inline_texcoord_width;
}
// when drawing an inline, calculate UVs based on line width
else {
// UVs can't calc for zero-width, use next zoom width in that case
style.texcoord_width = (style.width_unscaled || style.next_width_unscaled) * context.units_per_meter_overzoom / context.tile.overzoom2; // shorten calcs
}
}
return true;
},
// Calculate offset at current and next zoom, and scaling factor between
calcOffset (draw, style, context) {
// Pre-calculated offset passed
// This happens when a line passes pre-computed offset values to its outline
if (draw.offset_precalc) {
style.offset = draw.offset_precalc;
style.offset_scale = draw.offset_scale_precalc;
}
// Offset to calculate
else if (draw.offset) {
let offset = this.calcDistance(draw.offset, context);
if (draw.next_offset) {
let next_offset = this.calcDistanceNextZoom(draw.next_offset, context) * 2;
if (Math.abs(offset) >= Math.abs(next_offset)) {
style.offset = offset * context.units_per_meter_overzoom;
if (offset !== 0) {
style.offset_scale = 1 - (next_offset / offset);
}
else {
style.offset_scale = 0;
}
}
else {
style.offset = next_offset * context.units_per_meter_overzoom;
if (next_offset !== 0) {
style.offset_scale = (1 - (offset / next_offset)) * -1;
}
else {
style.offset_scale = 0;
}
}
}
else {
style.offset = offset * context.units_per_meter_overzoom;
style.offset_scale = 0;
}
}
// No offset
else {
style.offset = 0;
style.offset_scale = 0;
}
},
_parseFeature (feature, draw, context) {
var style = this.feature_style;
// calculate line width at current and next zoom
if (this.calcWidth(draw, style, context) === false) {
return; // missing or zero width
}
// calculate line offset at current and next zoom
this.calcOffset(draw, style, context);
style.color = this.parseColor(draw.color, context);
if (!style.color) {
return;
}
style.alpha = StyleParser.evalCachedProperty(draw.alpha, context); // optional alpha override
style.variant = draw.variant; // pre-calculated mesh variant
// height defaults to feature height, but extrude style can dynamically adjust height by returning a number or array (instead of a boolean)
style.z = StyleParser.evalCachedDistanceProperty(draw.z, context) || StyleParser.defaults.z;
style.height = feature.properties.height || StyleParser.defaults.height;
style.extrude = StyleParser.evalProperty(draw.extrude, context);
if (style.extrude) {
if (typeof style.extrude === 'number') {
style.height = style.extrude;
}
else if (Array.isArray(style.extrude)) {
style.height = style.extrude[1];
}
}
// Raise line height if extruded
if (style.extrude && style.height) {
style.z += style.height;
}
style.z *= Geo.height_scale; // provide sub-meter precision of height values
style.cap = draw.cap;
style.join = draw.join;
style.miter_limit = draw.miter_limit;
style.tile_edges = draw.tile_edges; // usually activated for debugging, or rare visualization needs
// Construct an outline style
// Reusable outline style object, marked as already wrapped in cache objects (preprocessed = true)
style.outline = style.outline || {
width: {}, next_width: {},
preprocessed: true
};
if (draw.outline && draw.outline.visible !== false && draw.outline.color && draw.outline.width) {
// outline width in meters
// NB: multiply by 2 because outline is applied on both sides of line
let outline_width = this.calcDistance(draw.outline.width, context) * 2;
let outline_next_width = this.calcDistanceNextZoom(draw.outline.next_width, context) * 2;
if ((outline_width === 0 && outline_next_width === 0) || outline_width < 0 || outline_next_width < 0) {
// skip lines that don't interpolate between zero or greater width
style.outline.width.value = null;
style.outline.next_width.value = null;
style.outline.color = null;
style.outline.inline_texcoord_width = null;
style.outline.texcoords = false;
}
else {
// Maintain consistent outline width around the line fill
style.outline.width.value = outline_width + style.width_unscaled;
style.outline.next_width.value = outline_next_width + style.next_width_unscaled;
style.outline.inline_texcoord_width = style.texcoord_width;
// Offset is directly copied from fill to outline, no need to re-calculate it
style.outline.offset_precalc = style.offset;
style.outline.offset_scale_precalc = style.offset_scale;
style.outline.color = draw.outline.color;
style.outline.alpha = draw.outline.alpha;
style.outline.interactive = draw.outline.interactive;
style.outline.cap = draw.outline.cap;
style.outline.join = draw.outline.join;
style.outline.miter_limit = draw.outline.miter_limit;
style.outline.texcoords = draw.outline.texcoords;
style.outline.extrude = draw.outline.extrude;
style.outline.z = draw.outline.z;
style.outline.style = draw.outline.style;
style.outline.variant = draw.outline.variant;
// Explicitly defined outline order, or inherited from inner line
if (draw.outline.order) {
style.outline.order = this.parseOrder(draw.outline.order, context);
}
else {
style.outline.order = style.order;
}
// Don't let outline be above inner line
if (style.outline.order > style.order) {
style.outline.order = style.order;
}
// Outlines are always at half-layer intervals to avoid conflicting with inner lines
style.outline.order -= 0.5;
}
}
else {
style.outline.width.value = null;
style.outline.next_width.value = null;
style.outline.color = null;
style.outline.inline_texcoord_width = null;
}
return style;
},
_preprocess (draw) {
draw.color = StyleParser.createColorPropertyCache(draw.color);
draw.alpha = StyleParser.createPropertyCache(draw.alpha);
draw.width = StyleParser.createPropertyCache(draw.width, StyleParser.parseUnits);
if (draw.width && draw.width.type !== StyleParser.CACHE_TYPE.STATIC) {
draw.next_width = StyleParser.createPropertyCache(draw.width, StyleParser.parseUnits);
}
draw.offset = draw.offset && StyleParser.createPropertyCache(draw.offset, StyleParser.parseUnits);
if (draw.offset && draw.offset.type !== StyleParser.CACHE_TYPE.STATIC) {
draw.next_offset = StyleParser.createPropertyCache(draw.offset, StyleParser.parseUnits);
}
draw.z = StyleParser.createPropertyCache(draw.z, StyleParser.parseUnits);
draw.dash = (draw.dash !== undefined ? draw.dash : this.dash);
draw.dash_key = draw.dash && this.dashTextureKey(draw.dash);
draw.dash_background_color = (draw.dash_background_color !== undefined ? draw.dash_background_color : this.dash_background_color);
draw.dash_background_color = draw.dash_background_color && StyleParser.parseColor(draw.dash_background_color);
draw.texture_merged = draw.dash_key || ((draw.texture !== undefined ? draw.texture : this.texture));
draw.texcoords = ((this.texcoords || draw.texture_merged) ? 1 : 0);
this.computeVariant(draw);
if (draw.outline) {
draw.outline.is_outline = true; // mark as outline (so mesh variant can be adjusted for render order, etc.)
draw.outline.style = draw.outline.style || this.name;
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.parseUnits);
draw.outline.next_width = StyleParser.createPropertyCache(draw.outline.width, StyleParser.parseUnits); // width re-computed for next zoom
draw.outline.interactive = (draw.outline.interactive != null) ? draw.outline.interactive : draw.interactive;
draw.outline.cap = draw.outline.cap || draw.cap;
draw.outline.join = draw.outline.join || draw.join;
draw.outline.miter_limit = (draw.outline.miter_limit != null) ? draw.outline.miter_limit : draw.miter_limit;
// always apply inline values for offset and extrusion/height to outline
draw.outline.offset = draw.offset;
draw.outline.extrude = draw.extrude;
draw.outline.z = draw.z;
// outline inherits dash pattern, but NOT explicit texture
let outline_style = this.styles[draw.outline.style];
if (outline_style) {
draw.outline.dash = (draw.outline.dash !== undefined ? draw.outline.dash : outline_style.dash);
draw.outline.texture = (draw.outline.texture !== undefined ? draw.outline.texture : outline_style.texture);
if (draw.outline.dash != null) { // dash was defined by outline draw or style
draw.outline.dash_key = draw.outline.dash && this.dashTextureKey(draw.outline.dash);
draw.outline.texture_merged = draw.outline.dash_key;
}
else if (draw.outline.dash === null) { // dash explicitly disabled by outline draw or style
draw.outline.dash_key = null;
draw.outline.texture_merged = draw.outline.texture;
}
else if (draw.outline.texture != null) { // texture was defined by outline draw or style
draw.outline.dash_key = null; // outline explicitly turning off dash
draw.outline.texture_merged = draw.outline.texture;
}
else { // no dash or texture defined for outline, inherit parent dash
draw.outline.dash = draw.dash;
draw.outline.dash_key = draw.outline.dash && this.dashTextureKey(draw.outline.dash);
draw.outline.texture_merged = draw.outline.dash_key;
}
draw.outline.dash_background_color = (draw.outline.dash_background_color !== undefined ? draw.outline.dash_background_color : outline_style.dash_background_color);
draw.outline.dash_background_color = (draw.outline.dash_background_color !== undefined ? draw.outline.dash_background_color : draw.dash_background_color);
draw.outline.dash_background_color = draw.outline.dash_background_color && StyleParser.parseColor(draw.outline.dash_background_color);
draw.outline.texcoords = ((outline_style.texcoords || draw.outline.texture_merged) ? 1 : 0);
// outline inherits draw blend order from parent inline, unless explicitly turned off with null
if (draw.outline.blend_order === undefined && draw.blend_order != null) {
draw.outline.blend_order = draw.blend_order;
}
outline_style.computeVariant(draw.outline);
}
else {
log({ level: 'warn', once: true }, `Layer group '${draw.layers.join(', ')}': ` +
`line 'outline' specifies non-existent draw style '${draw.outline.style}' (or maybe the style is ` +
'defined but is missing a \'base\' or has another error), skipping outlines for features matching this layer group');
draw.outline = null;
}
}
return draw;
},
// Unique string key for a dash pattern (used as texture name)
dashTextureKey (dash) {
return '__dash_' + JSON.stringify(dash);
},
// Return or render a dash pattern texture
getDashTexture (dash) {
let dash_key = this.dashTextureKey(dash);
if (this.dash_textures[dash_key] == null) {
this.dash_textures[dash_key] = true;
// Render line pattern
const dash_texture = renderDashArray(dash, { scale: DASH_SCALE });
Texture.create(this.gl, dash_key, {
data: dash_texture.pixels,
height: dash_texture.length,
width: 1,
filtering: 'nearest'
});
}
},
// Override
async endData (tile) {
const tile_data = await Style.endData.call(this, tile);
if (tile_data) {
tile_data.uniforms.u_has_line_texture = false;
tile_data.uniforms.u_texture = Texture.default;
tile_data.uniforms.u_v_scale_adjust = Geo.tile_scale;
let pending = [];
for (let m in tile_data.meshes) {
let variant = tile_data.meshes[m].variant;
if (variant.texture) {
let uniforms = tile_data.meshes[m].uniforms = tile_data.meshes[m].uniforms || {};
uniforms.u_has_line_texture = true;
uniforms.u_texture = variant.texture;
uniforms.u_texture_ratio = 1;
if (variant.dash) {
uniforms.u_v_scale_adjust = Geo.tile_scale * DASH_SCALE;
uniforms.u_has_dash = (variant.dash_background_color != null ? 1 : 0);
uniforms.u_dash_background_color = variant.dash_background_color || [0, 0, 0, 0];
}
if (variant.dash_key && this.dash_textures[variant.dash_key] == null) {
this.dash_textures[variant.dash_key] = true;
try {
await WorkerBroker.postMessage(this.main_thread_target+'.getDashTexture', variant.dash);
}
catch (e) {
log('trace', `${this.name}: line dash texture create failed because style no longer on main thread`);
}
}
if (Texture.textures[variant.texture] == null) {
pending.push(
Texture.syncTexturesToWorker([variant.texture]).then(textures => {
let texture = textures[variant.texture];
if (texture) {
uniforms.u_texture_ratio = texture.height / texture.width;
}
})
);
}
else {
let texture = Texture.textures[variant.texture];
uniforms.u_texture_ratio = texture.height / texture.width;
}
}
}
await Promise.all(pending);
}
return tile_data;
},
// Calculate and store mesh variant (unique by draw group but not feature)
computeVariant (draw) {
// Factors that determine a unique mesh rendering variant
let key = (draw.offset ? 1 : 0); // whether feature has a line offset
key += '/' + draw.texcoords; // whether feature has texture UVs
key += '/' + (draw.interactive ? 1 : 0); // whether feature has interactivity
key += '/' + ((draw.extrude || draw.z) ? 1 : 0); // whether feature has a z coordinate
key += '/' + draw.is_outline; // whether this is an outline of a line feature
if (draw.dash_key) { // whether feature has a line dash pattern
key += draw.dash_key;
if (draw.dash_background_color) {
key += draw.dash_background_color;
}
}
if (draw.texture_merged) { // whether feature has a line texture
key += draw.texture_merged;
}
const blend_order = this.getBlendOrderForDraw(draw);
key += '/' + blend_order;
// Create unique key
key = hashString(key);
draw.variant = key;
if (this.variants[key] == null) {
this.variants[key] = {
key,
blend_order,
mesh_order: (draw.is_outline ? 0 : 1), // outlines should be drawn first, so inline is on top
selection: (draw.interactive ? 1 : 0),
offset: (draw.offset ? 1 : 0),
z_or_offset: ((draw.offset || draw.extrude || draw.z) ? 1 : 0),
texcoords: draw.texcoords,
texture: draw.texture_merged,
dash: draw.dash,
dash_key: draw.dash_key,
dash_background_color: draw.dash_background_color
};
}
},
// Override
// Create or return desired vertex layout permutation based on flags
vertexLayoutForMeshVariant (variant) {
if (this.vertex_layouts[variant.key] == null) {
// Attributes for this mesh variant
// Optional attributes have placeholder values assigned with `static` parameter
const attribs = [
{ name: 'a_position', size: 4, type: gl.SHORT, normalized: false },
{ name: 'a_extrude', size: 2, type: gl.SHORT, normalized: false },
{ name: 'a_offset', size: 2, type: gl.SHORT, normalized: false, static: (variant.offset ? null : [0, 0]) },
{ name: 'a_z_and_offset_scale', size: 2, type: gl.SHORT, normalized: false, static: (variant.z_or_offset ? null : [0, 0]) },
{ name: 'a_texcoord', size: 2, type: gl.UNSIGNED_SHORT, normalized: true, static: (variant.texcoords ? null : [0, 0]) },
{ 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]) }
];
this.addCustomAttributesToAttributeList(attribs);
this.vertex_layouts[variant.key] = new VertexLayout(attribs);
}
return this.vertex_layouts[variant.key];
},
// Override
meshVariantTypeForDraw (draw) {
return this.variants[draw.variant]; // return pre-calculated mesh variant
},
/**
* 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) {
let i = 0;
// a_position.xy - vertex position
// a_position.z - line width scaling factor
// a_position.w - layer order
this.vertex_template[i++] = 0;
this.vertex_template[i++] = 0;
this.vertex_template[i++] = style.width_scale * 1024;
this.vertex_template[i++] = this.scaleOrder(style.order);
// a_extrude.xy - extrusion vector (vertex extrusion away from center of line)
this.vertex_template[i++] = 0;
this.vertex_template[i++] = 0;
// a_offset.xy - normal vector
// offset can be static or dynamic depending on style
if (mesh.variant.offset) {
this.vertex_template[i++] = 0;
this.vertex_template[i++] = 0;
}
// a_z_and_offset_scale.xy
if (mesh.variant.z_or_offset) {
this.vertex_template[i++] = style.z || 0; // feature z position
this.vertex_template[i++] = style.offset_scale * 1024; // line offset scaling factor
}
// a_texcoord.uv - texture coordinates
if (mesh.variant.texcoords) {
this.vertex_template[i++] = 0;
this.vertex_template[i++] = 0;
}
// a_color.rgba - feature color
this.vertex_template[i++] = style.color[0] * 255;
this.vertex_template[i++] = style.color[1] * 255;
this.vertex_template[i++] = style.color[2] * 255;
this.vertex_template[i++] = (style.alpha != null ? style.alpha : style.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;
}
this.addCustomAttributesToVertexTemplate(style, i);
return this.vertex_template;
},
buildLines(lines, style, context, options) {
// Outline (build first so that blended geometry without a depth test is drawn first/under the inner line)
this.feature_style = this.outline_feature_style; // swap in outline-specific style holder
if (style.outline && style.outline.color != null && style.outline.width.value != null) {
var outline_style = this.styles[style.outline.style];
if (outline_style) {
outline_style.addFeature(context.feature, style.outline, context);
}
}
// Main line
this.feature_style = this.inline_feature_style; // restore calculated style for inline
let mesh = this.getTileMesh(context.tile, this.meshVariantTypeForDraw(style));
let vertex_data = mesh.vertex_data;
let vertex_layout = vertex_data.vertex_layout;
let vertex_template = this.makeVertexTemplate(style, mesh);
return buildPolylines(
lines,
style,
vertex_data,
vertex_template,
vertex_layout.index,
(options && options.closed_polygon), // closed_polygon
(!style.tile_edges && options && options.remove_tile_edges), // remove_tile_edges
(Geo.tile_scale * context.tile.pad_scale * 2) // tile_edge_tolerance
);
},
buildPolygons(polygons, style, context) {
// Render polygons as individual lines
let geom_count = 0;
for (let p=0; p < polygons.length; p++) {
geom_count += this.buildLines(polygons[p], style, context, { closed_polygon: true, remove_tile_edges: true });
}
return geom_count;
}
});