UNPKG

mapbox-gl

Version:
647 lines (541 loc) 28.3 kB
// @flow import { LineLayoutArray } from '../array_types'; import { members as layoutAttributes } from './line_attributes'; import SegmentVector from '../segment'; import { ProgramConfigurationSet } from '../program_configuration'; import { TriangleIndexArray } from '../index_array_type'; import EXTENT from '../extent'; import mvt from '@mapbox/vector-tile'; const vectorTileFeatureTypes = mvt.VectorTileFeature.types; import { register } from '../../util/web_worker_transfer'; import {hasPattern, addPatternDependencies} from './pattern_bucket_features'; import loadGeometry from '../load_geometry'; import EvaluationParameters from '../../style/evaluation_parameters'; import type { Bucket, BucketParameters, BucketFeature, IndexedFeature, PopulateParameters } from '../bucket'; import type LineStyleLayer from '../../style/style_layer/line_style_layer'; import type Point from '@mapbox/point-geometry'; import type {Segment} from '../segment'; import type Context from '../../gl/context'; import type IndexBuffer from '../../gl/index_buffer'; import type VertexBuffer from '../../gl/vertex_buffer'; import type {FeatureStates} from '../../source/source_state'; import type {ImagePosition} from '../../render/image_atlas'; // NOTE ON EXTRUDE SCALE: // scale the extrusion vector so that the normal length is this value. // contains the "texture" normals (-1..1). this is distinct from the extrude // normals for line joins, because the x-value remains 0 for the texture // normal array, while the extrude normal actually moves the vertex to create // the acute/bevelled line join. const EXTRUDE_SCALE = 63; /* * Sharp corners cause dashed lines to tilt because the distance along the line * is the same at both the inner and outer corners. To improve the appearance of * dashed lines we add extra points near sharp corners so that a smaller part * of the line is tilted. * * COS_HALF_SHARP_CORNER controls how sharp a corner has to be for us to add an * extra vertex. The default is 75 degrees. * * The newly created vertices are placed SHARP_CORNER_OFFSET pixels from the corner. */ const COS_HALF_SHARP_CORNER = Math.cos(75 / 2 * (Math.PI / 180)); const SHARP_CORNER_OFFSET = 15; // The number of bits that is used to store the line distance in the buffer. const LINE_DISTANCE_BUFFER_BITS = 15; // We don't have enough bits for the line distance as we'd like to have, so // use this value to scale the line distance (in tile units) down to a smaller // value. This lets us store longer distances while sacrificing precision. const LINE_DISTANCE_SCALE = 1 / 2; // The maximum line distance, in tile units, that fits in the buffer. const MAX_LINE_DISTANCE = Math.pow(2, LINE_DISTANCE_BUFFER_BITS - 1) / LINE_DISTANCE_SCALE; function addLineVertex(layoutVertexBuffer, point: Point, extrude: Point, round: boolean, up: boolean, dir: number, linesofar: number) { layoutVertexBuffer.emplaceBack( // a_pos_normal // Encode round/up the least significant bits (point.x << 1) + (round ? 1 : 0), (point.y << 1) + (up ? 1 : 0), // a_data // add 128 to store a byte in an unsigned byte Math.round(EXTRUDE_SCALE * extrude.x) + 128, Math.round(EXTRUDE_SCALE * extrude.y) + 128, // Encode the -1/0/1 direction value into the first two bits of .z of a_data. // Combine it with the lower 6 bits of `linesofar` (shifted by 2 bites to make // room for the direction value). The upper 8 bits of `linesofar` are placed in // the `w` component. `linesofar` is scaled down by `LINE_DISTANCE_SCALE` so that // we can store longer distances while sacrificing precision. ((dir === 0 ? 0 : (dir < 0 ? -1 : 1)) + 1) | (((linesofar * LINE_DISTANCE_SCALE) & 0x3F) << 2), (linesofar * LINE_DISTANCE_SCALE) >> 6); } /** * @private */ class LineBucket implements Bucket { distance: number; e1: number; e2: number; e3: number; index: number; zoom: number; overscaling: number; layers: Array<LineStyleLayer>; layerIds: Array<string>; stateDependentLayers: Array<any>; stateDependentLayerIds: Array<string>; patternFeatures: Array<BucketFeature>; layoutVertexArray: LineLayoutArray; layoutVertexBuffer: VertexBuffer; indexArray: TriangleIndexArray; indexBuffer: IndexBuffer; hasPattern: boolean; programConfigurations: ProgramConfigurationSet<LineStyleLayer>; segments: SegmentVector; uploaded: boolean; constructor(options: BucketParameters<LineStyleLayer>) { this.zoom = options.zoom; this.overscaling = options.overscaling; this.layers = options.layers; this.layerIds = this.layers.map(layer => layer.id); this.index = options.index; this.hasPattern = false; this.patternFeatures = []; this.layoutVertexArray = new LineLayoutArray(); this.indexArray = new TriangleIndexArray(); this.programConfigurations = new ProgramConfigurationSet(layoutAttributes, options.layers, options.zoom); this.segments = new SegmentVector(); this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); } populate(features: Array<IndexedFeature>, options: PopulateParameters) { this.hasPattern = hasPattern('line', this.layers, options); const lineSortKey = this.layers[0].layout.get('line-sort-key'); const bucketFeatures = []; for (const {feature, index, sourceLayerIndex} of features) { if (!this.layers[0]._featureFilter(new EvaluationParameters(this.zoom), feature)) continue; const geometry = loadGeometry(feature); const sortKey = lineSortKey ? lineSortKey.evaluate(feature, {}) : undefined; const bucketFeature: BucketFeature = { id: feature.id, properties: feature.properties, type: feature.type, sourceLayerIndex, index, geometry, patterns: {}, sortKey }; bucketFeatures.push(bucketFeature); } if (lineSortKey) { bucketFeatures.sort((a, b) => { // a.sortKey is always a number when in use return ((a.sortKey: any): number) - ((b.sortKey: any): number); }); } for (const bucketFeature of bucketFeatures) { const {geometry, index, sourceLayerIndex} = bucketFeature; if (this.hasPattern) { const patternBucketFeature = addPatternDependencies('line', this.layers, bucketFeature, this.zoom, options); // pattern features are added only once the pattern is loaded into the image atlas // so are stored during populate until later updated with positions by tile worker in addFeatures this.patternFeatures.push(patternBucketFeature); } else { this.addFeature(bucketFeature, geometry, index, {}); } const feature = features[index].feature; options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index); } } update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[string]: ImagePosition}) { if (!this.stateDependentLayers.length) return; this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, imagePositions); } addFeatures(options: PopulateParameters, imagePositions: {[string]: ImagePosition}) { for (const feature of this.patternFeatures) { this.addFeature(feature, feature.geometry, feature.index, imagePositions); } } isEmpty() { return this.layoutVertexArray.length === 0; } uploadPending() { return !this.uploaded || this.programConfigurations.needsUpload; } upload(context: Context) { if (!this.uploaded) { this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, layoutAttributes); this.indexBuffer = context.createIndexBuffer(this.indexArray); } this.programConfigurations.upload(context); this.uploaded = true; } destroy() { if (!this.layoutVertexBuffer) return; this.layoutVertexBuffer.destroy(); this.indexBuffer.destroy(); this.programConfigurations.destroy(); this.segments.destroy(); } addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, imagePositions: {[string]: ImagePosition}) { const layout = this.layers[0].layout; const join = layout.get('line-join').evaluate(feature, {}); const cap = layout.get('line-cap'); const miterLimit = layout.get('line-miter-limit'); const roundLimit = layout.get('line-round-limit'); for (const line of geometry) { this.addLine(line, feature, join, cap, miterLimit, roundLimit, index, imagePositions); } } addLine(vertices: Array<Point>, feature: BucketFeature, join: string, cap: string, miterLimit: number, roundLimit: number, index: number, imagePositions: {[string]: ImagePosition}) { let lineDistances = null; if (!!feature.properties && feature.properties.hasOwnProperty('mapbox_clip_start') && feature.properties.hasOwnProperty('mapbox_clip_end')) { lineDistances = { start: feature.properties.mapbox_clip_start, end: feature.properties.mapbox_clip_end, tileTotal: undefined }; } const isPolygon = vectorTileFeatureTypes[feature.type] === 'Polygon'; // If the line has duplicate vertices at the ends, adjust start/length to remove them. let len = vertices.length; while (len >= 2 && vertices[len - 1].equals(vertices[len - 2])) { len--; } let first = 0; while (first < len - 1 && vertices[first].equals(vertices[first + 1])) { first++; } // Ignore invalid geometry. if (len < (isPolygon ? 3 : 2)) return; if (lineDistances) { lineDistances.tileTotal = calculateFullDistance(vertices, first, len); } if (join === 'bevel') miterLimit = 1.05; const sharpCornerOffset = SHARP_CORNER_OFFSET * (EXTENT / (512 * this.overscaling)); const firstVertex = vertices[first]; // we could be more precise, but it would only save a negligible amount of space const segment = this.segments.prepareSegment(len * 10, this.layoutVertexArray, this.indexArray); this.distance = 0; const beginCap = cap, endCap = isPolygon ? 'butt' : cap; let startOfLine = true; let currentVertex; let prevVertex = ((undefined: any): Point); let nextVertex = ((undefined: any): Point); let prevNormal = ((undefined: any): Point); let nextNormal = ((undefined: any): Point); let offsetA; let offsetB; // the last three vertices added this.e1 = this.e2 = this.e3 = -1; if (isPolygon) { currentVertex = vertices[len - 2]; nextNormal = firstVertex.sub(currentVertex)._unit()._perp(); } for (let i = first; i < len; i++) { nextVertex = isPolygon && i === len - 1 ? vertices[first + 1] : // if the line is closed, we treat the last vertex like the first vertices[i + 1]; // just the next vertex // if two consecutive vertices exist, skip the current one if (nextVertex && vertices[i].equals(nextVertex)) continue; if (nextNormal) prevNormal = nextNormal; if (currentVertex) prevVertex = currentVertex; currentVertex = vertices[i]; // Calculate the normal towards the next vertex in this line. In case // there is no next vertex, pretend that the line is continuing straight, // meaning that we are just using the previous normal. nextNormal = nextVertex ? nextVertex.sub(currentVertex)._unit()._perp() : prevNormal; // If we still don't have a previous normal, this is the beginning of a // non-closed line, so we're doing a straight "join". prevNormal = prevNormal || nextNormal; // Determine the normal of the join extrusion. It is the angle bisector // of the segments between the previous line and the next line. // In the case of 180° angles, the prev and next normals cancel each other out: // prevNormal + nextNormal = (0, 0), its magnitude is 0, so the unit vector would be // undefined. In that case, we're keeping the joinNormal at (0, 0), so that the cosHalfAngle // below will also become 0 and miterLength will become Infinity. let joinNormal = prevNormal.add(nextNormal); if (joinNormal.x !== 0 || joinNormal.y !== 0) { joinNormal._unit(); } /* joinNormal prevNormal * ↖ ↑ * .________. prevVertex * | * nextNormal ← | currentVertex * | * nextVertex ! * */ // Calculate the length of the miter (the ratio of the miter to the width). // Find the cosine of the angle between the next and join normals // using dot product. The inverse of that is the miter length. const cosHalfAngle = joinNormal.x * nextNormal.x + joinNormal.y * nextNormal.y; const miterLength = cosHalfAngle !== 0 ? 1 / cosHalfAngle : Infinity; const isSharpCorner = cosHalfAngle < COS_HALF_SHARP_CORNER && prevVertex && nextVertex; if (isSharpCorner && i > first) { const prevSegmentLength = currentVertex.dist(prevVertex); if (prevSegmentLength > 2 * sharpCornerOffset) { const newPrevVertex = currentVertex.sub(currentVertex.sub(prevVertex)._mult(sharpCornerOffset / prevSegmentLength)._round()); this.distance += newPrevVertex.dist(prevVertex); this.addCurrentVertex(newPrevVertex, this.distance, prevNormal.mult(1), 0, 0, false, segment, lineDistances); prevVertex = newPrevVertex; } } // The join if a middle vertex, otherwise the cap. const middleVertex = prevVertex && nextVertex; let currentJoin = middleVertex ? join : nextVertex ? beginCap : endCap; if (middleVertex && currentJoin === 'round') { if (miterLength < roundLimit) { currentJoin = 'miter'; } else if (miterLength <= 2) { currentJoin = 'fakeround'; } } if (currentJoin === 'miter' && miterLength > miterLimit) { currentJoin = 'bevel'; } if (currentJoin === 'bevel') { // The maximum extrude length is 128 / 63 = 2 times the width of the line // so if miterLength >= 2 we need to draw a different type of bevel here. if (miterLength > 2) currentJoin = 'flipbevel'; // If the miterLength is really small and the line bevel wouldn't be visible, // just draw a miter join to save a triangle. if (miterLength < miterLimit) currentJoin = 'miter'; } // Calculate how far along the line the currentVertex is if (prevVertex) this.distance += currentVertex.dist(prevVertex); if (currentJoin === 'miter') { joinNormal._mult(miterLength); this.addCurrentVertex(currentVertex, this.distance, joinNormal, 0, 0, false, segment, lineDistances); } else if (currentJoin === 'flipbevel') { // miter is too big, flip the direction to make a beveled join if (miterLength > 100) { // Almost parallel lines joinNormal = nextNormal.clone().mult(-1); } else { const direction = prevNormal.x * nextNormal.y - prevNormal.y * nextNormal.x > 0 ? -1 : 1; const bevelLength = miterLength * prevNormal.add(nextNormal).mag() / prevNormal.sub(nextNormal).mag(); joinNormal._perp()._mult(bevelLength * direction); } this.addCurrentVertex(currentVertex, this.distance, joinNormal, 0, 0, false, segment, lineDistances); this.addCurrentVertex(currentVertex, this.distance, joinNormal.mult(-1), 0, 0, false, segment, lineDistances); } else if (currentJoin === 'bevel' || currentJoin === 'fakeround') { const lineTurnsLeft = (prevNormal.x * nextNormal.y - prevNormal.y * nextNormal.x) > 0; const offset = -Math.sqrt(miterLength * miterLength - 1); if (lineTurnsLeft) { offsetB = 0; offsetA = offset; } else { offsetA = 0; offsetB = offset; } // Close previous segment with a bevel if (!startOfLine) { this.addCurrentVertex(currentVertex, this.distance, prevNormal, offsetA, offsetB, false, segment, lineDistances); } if (currentJoin === 'fakeround') { // The join angle is sharp enough that a round join would be visible. // Bevel joins fill the gap between segments with a single pie slice triangle. // Create a round join by adding multiple pie slices. The join isn't actually round, but // it looks like it is at the sizes we render lines at. // Add more triangles for sharper angles. // This math is just a good enough approximation. It isn't "correct". const n = Math.floor((0.5 - (cosHalfAngle - 0.5)) * 8); let approxFractionalJoinNormal; for (let m = 0; m < n; m++) { approxFractionalJoinNormal = nextNormal.mult((m + 1) / (n + 1))._add(prevNormal)._unit(); this.addPieSliceVertex(currentVertex, this.distance, approxFractionalJoinNormal, lineTurnsLeft, segment, lineDistances); } this.addPieSliceVertex(currentVertex, this.distance, joinNormal, lineTurnsLeft, segment, lineDistances); for (let k = n - 1; k >= 0; k--) { approxFractionalJoinNormal = prevNormal.mult((k + 1) / (n + 1))._add(nextNormal)._unit(); this.addPieSliceVertex(currentVertex, this.distance, approxFractionalJoinNormal, lineTurnsLeft, segment, lineDistances); } } // Start next segment if (nextVertex) { this.addCurrentVertex(currentVertex, this.distance, nextNormal, -offsetA, -offsetB, false, segment, lineDistances); } } else if (currentJoin === 'butt') { if (!startOfLine) { // Close previous segment with a butt this.addCurrentVertex(currentVertex, this.distance, prevNormal, 0, 0, false, segment, lineDistances); } // Start next segment with a butt if (nextVertex) { this.addCurrentVertex(currentVertex, this.distance, nextNormal, 0, 0, false, segment, lineDistances); } } else if (currentJoin === 'square') { if (!startOfLine) { // Close previous segment with a square cap this.addCurrentVertex(currentVertex, this.distance, prevNormal, 1, 1, false, segment, lineDistances); // The segment is done. Unset vertices to disconnect segments. this.e1 = this.e2 = -1; } // Start next segment if (nextVertex) { this.addCurrentVertex(currentVertex, this.distance, nextNormal, -1, -1, false, segment, lineDistances); } } else if (currentJoin === 'round') { if (!startOfLine) { // Close previous segment with butt this.addCurrentVertex(currentVertex, this.distance, prevNormal, 0, 0, false, segment, lineDistances); // Add round cap or linejoin at end of segment this.addCurrentVertex(currentVertex, this.distance, prevNormal, 1, 1, true, segment, lineDistances); // The segment is done. Unset vertices to disconnect segments. this.e1 = this.e2 = -1; } // Start next segment with a butt if (nextVertex) { // Add round cap before first segment this.addCurrentVertex(currentVertex, this.distance, nextNormal, -1, -1, true, segment, lineDistances); this.addCurrentVertex(currentVertex, this.distance, nextNormal, 0, 0, false, segment, lineDistances); } } if (isSharpCorner && i < len - 1) { const nextSegmentLength = currentVertex.dist(nextVertex); if (nextSegmentLength > 2 * sharpCornerOffset) { const newCurrentVertex = currentVertex.add(nextVertex.sub(currentVertex)._mult(sharpCornerOffset / nextSegmentLength)._round()); this.distance += newCurrentVertex.dist(currentVertex); this.addCurrentVertex(newCurrentVertex, this.distance, nextNormal.mult(1), 0, 0, false, segment, lineDistances); currentVertex = newCurrentVertex; } } startOfLine = false; } this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions); } /** * Add two vertices to the buffers. * * @param {Object} currentVertex the line vertex to add buffer vertices for * @param {number} distance the distance from the beginning of the line to the vertex * @param {number} endLeft extrude to shift the left vertex along the line * @param {number} endRight extrude to shift the left vertex along the line * @param {boolean} round whether this is a round cap * @private */ addCurrentVertex(currentVertex: Point, distance: number, normal: Point, endLeft: number, endRight: number, round: boolean, segment: Segment, distancesForScaling: ?Object) { let extrude; const layoutVertexArray = this.layoutVertexArray; const indexArray = this.indexArray; if (distancesForScaling) { // For gradient lines, scale distance from tile units to [0, 2^15) distance = scaleDistance(distance, distancesForScaling); } extrude = normal.clone(); if (endLeft) extrude._sub(normal.perp()._mult(endLeft)); addLineVertex(layoutVertexArray, currentVertex, extrude, round, false, endLeft, distance); this.e3 = segment.vertexLength++; if (this.e1 >= 0 && this.e2 >= 0) { indexArray.emplaceBack(this.e1, this.e2, this.e3); segment.primitiveLength++; } this.e1 = this.e2; this.e2 = this.e3; extrude = normal.mult(-1); if (endRight) extrude._sub(normal.perp()._mult(endRight)); addLineVertex(layoutVertexArray, currentVertex, extrude, round, true, -endRight, distance); this.e3 = segment.vertexLength++; if (this.e1 >= 0 && this.e2 >= 0) { indexArray.emplaceBack(this.e1, this.e2, this.e3); segment.primitiveLength++; } this.e1 = this.e2; this.e2 = this.e3; // There is a maximum "distance along the line" that we can store in the buffers. // When we get close to the distance, reset it to zero and add the vertex again with // a distance of zero. The max distance is determined by the number of bits we allocate // to `linesofar`. if (distance > MAX_LINE_DISTANCE / 2 && !distancesForScaling) { this.distance = 0; this.addCurrentVertex(currentVertex, this.distance, normal, endLeft, endRight, round, segment); } } /** * Add a single new vertex and a triangle using two previous vertices. * This adds a pie slice triangle near a join to simulate round joins * * @param currentVertex the line vertex to add buffer vertices for * @param distance the distance from the beginning of the line to the vertex * @param extrude the offset of the new vertex from the currentVertex * @param lineTurnsLeft whether the line is turning left or right at this angle * @private */ addPieSliceVertex(currentVertex: Point, distance: number, extrude: Point, lineTurnsLeft: boolean, segment: Segment, distancesForScaling: ?Object) { extrude = extrude.mult(lineTurnsLeft ? -1 : 1); const layoutVertexArray = this.layoutVertexArray; const indexArray = this.indexArray; if (distancesForScaling) distance = scaleDistance(distance, distancesForScaling); addLineVertex(layoutVertexArray, currentVertex, extrude, false, lineTurnsLeft, 0, distance); this.e3 = segment.vertexLength++; if (this.e1 >= 0 && this.e2 >= 0) { indexArray.emplaceBack(this.e1, this.e2, this.e3); segment.primitiveLength++; } if (lineTurnsLeft) { this.e2 = this.e3; } else { this.e1 = this.e3; } } } /** * Knowing the ratio of the full linestring covered by this tiled feature, as well * as the total distance (in tile units) of this tiled feature, and the distance * (in tile units) of the current vertex, we can determine the relative distance * of this vertex along the full linestring feature and scale it to [0, 2^15) * * @param {number} tileDistance the distance from the beginning of the tiled line to this vertex * @param {Object} stats * @param {number} stats.start the ratio (0-1) along a full original linestring feature of the start of this tiled line feature * @param {number} stats.end the ratio (0-1) along a full original linestring feature of the end of this tiled line feature * @param {number} stats.tileTotal the total distance, in tile units, of this tiled line feature * * @private */ function scaleDistance(tileDistance: number, stats: Object) { return ((tileDistance / stats.tileTotal) * (stats.end - stats.start) + stats.start) * (MAX_LINE_DISTANCE - 1); } /** * Calculate the total distance, in tile units, of this tiled line feature * * @param {Array<Point>} vertices the full geometry of this tiled line feature * @param {number} first the index in the vertices array representing the first vertex we should consider * @param {number} len the count of vertices we should consider from `first` * * @private */ function calculateFullDistance(vertices: Array<Point>, first: number, len: number) { let currentVertex, nextVertex; let total = 0; for (let i = first; i < len - 1; i++) { currentVertex = vertices[i]; nextVertex = vertices[i + 1]; total += currentVertex.dist(nextVertex); } return total; } register('LineBucket', LineBucket, {omit: ['layers', 'patternFeatures']}); export default LineBucket;