mapbox-gl
Version:
A WebGL interactive maps library
774 lines (665 loc) • 35.2 kB
JavaScript
// @flow
import {FillExtrusionLayoutArray, FillExtrusionExtArray, FillExtrusionCentroidArray} from '../array_types.js';
import {members as layoutAttributes, centroidAttributes, fillExtrusionAttributesExt} from './fill_extrusion_attributes.js';
import SegmentVector from '../segment.js';
import {ProgramConfigurationSet} from '../program_configuration.js';
import {TriangleIndexArray} from '../index_array_type.js';
import EXTENT from '../extent.js';
import earcut from 'earcut';
import {VectorTileFeature} from '@mapbox/vector-tile';
const vectorTileFeatureTypes = VectorTileFeature.types;
import classifyRings from '../../util/classify_rings.js';
import assert from 'assert';
const EARCUT_MAX_RINGS = 500;
import {register} from '../../util/web_worker_transfer.js';
import {hasPattern, addPatternDependencies} from './pattern_bucket_features.js';
import loadGeometry from '../load_geometry.js';
import toEvaluationFeature from '../evaluation_feature.js';
import EvaluationParameters from '../../style/evaluation_parameters.js';
import Point from '@mapbox/point-geometry';
import {number as interpolate} from '../../style-spec/util/interpolate.js';
import {lngFromMercatorX, latFromMercatorY, mercatorYfromLat} from '../../geo/mercator_coordinate.js';
import {subdividePolygons} from '../../util/polygon_clipping.js';
import type {ClippedPolygon} from '../../util/polygon_clipping.js';
import type {Vec3} from 'gl-matrix';
import type {CanonicalTileID} from '../../source/tile_id.js';
import type {
Bucket,
BucketParameters,
BucketFeature,
IndexedFeature,
PopulateParameters
} from '../bucket.js';
import {earthRadius} from '../../geo/lng_lat.js';
import type FillExtrusionStyleLayer from '../../style/style_layer/fill_extrusion_style_layer.js';
import type Context from '../../gl/context.js';
import type IndexBuffer from '../../gl/index_buffer.js';
import type VertexBuffer from '../../gl/vertex_buffer.js';
import type {FeatureStates} from '../../source/source_state.js';
import type {SpritePositions} from '../../util/image.js';
import type {ProjectionSpecification} from '../../style-spec/types.js';
import type {TileTransform} from '../../geo/projection/tile_transform.js';
import type {IVectorTileLayer} from '@mapbox/vector-tile';
const FACTOR = Math.pow(2, 13);
// Also declared in _prelude_terrain.vertex.glsl
// Used to scale most likely elevation values to fit well in an uint16
// (Elevation of Dead Sea + ELEVATION_OFFSET) * ELEVATION_SCALE is roughly 0
// (Height of mt everest + ELEVATION_OFFSET) * ELEVATION_SCALE is roughly 64k
export const ELEVATION_SCALE = 7.0;
export const ELEVATION_OFFSET = 450;
function addVertex(vertexArray: FillExtrusionLayoutArray, x: number, y: number, nxRatio: number, nySign: number, normalUp: number, top: number, e: number) {
vertexArray.emplaceBack(
// a_pos_normal_ed:
// Encode top and side/up normal using the least significant bits
(x << 1) + top,
(y << 1) + normalUp,
// dxdy is signed, encode quadrant info using the least significant bit
(Math.floor(nxRatio * FACTOR) << 1) + nySign,
// edgedistance (used for wrapping patterns around extrusion sides)
Math.round(e)
);
}
function addGlobeExtVertex(vertexArray: FillExtrusionExtArray, pos: {x: number, y: number, z: number}, normal: Vec3) {
const encode = 1 << 14;
vertexArray.emplaceBack(
pos.x, pos.y, pos.z,
normal[0] * encode, normal[1] * encode, normal[2] * encode);
}
export class PartMetadata {
acc: Point;
min: Point;
max: Point;
polyCount: Array<{edges: number, top: number}>;
currentPolyCount: {edges: number, top: number};
borders: Array<[number, number]>; // Array<[min, max]>
vertexArrayOffset: number;
constructor() {
this.acc = new Point(0, 0);
this.polyCount = [];
}
startRing(p: Point) {
this.currentPolyCount = {edges: 0, top: 0};
this.polyCount.push(this.currentPolyCount);
if (this.min) return;
this.min = new Point(p.x, p.y);
this.max = new Point(p.x, p.y);
}
append(p: Point, prev: Point) {
this.currentPolyCount.edges++;
this.acc._add(p);
const min = this.min, max = this.max;
if (p.x < min.x) {
min.x = p.x;
} else if (p.x > max.x) {
max.x = p.x;
}
if (p.y < min.y) {
min.y = p.y;
} else if (p.y > max.y) {
max.y = p.y;
}
if (((p.x === 0 || p.x === EXTENT) && p.x === prev.x) !== ((p.y === 0 || p.y === EXTENT) && p.y === prev.y)) {
// Custom defined geojson buildings are cut on borders. Points are
// repeated when edge cuts tile corner (reason for using xor).
this.processBorderOverlap(p, prev);
}
// check border intersection
if ((prev.x < 0) !== (p.x < 0)) {
this.addBorderIntersection(0, interpolate(prev.y, p.y, (0 - prev.x) / (p.x - prev.x)));
}
if ((prev.x > EXTENT) !== (p.x > EXTENT)) {
this.addBorderIntersection(1, interpolate(prev.y, p.y, (EXTENT - prev.x) / (p.x - prev.x)));
}
if ((prev.y < 0) !== (p.y < 0)) {
this.addBorderIntersection(2, interpolate(prev.x, p.x, (0 - prev.y) / (p.y - prev.y)));
}
if ((prev.y > EXTENT) !== (p.y > EXTENT)) {
this.addBorderIntersection(3, interpolate(prev.x, p.x, (EXTENT - prev.y) / (p.y - prev.y)));
}
}
addBorderIntersection(index: 0 | 1 | 2 | 3, i: number) {
if (!this.borders) {
this.borders = [
[Number.MAX_VALUE, -Number.MAX_VALUE],
[Number.MAX_VALUE, -Number.MAX_VALUE],
[Number.MAX_VALUE, -Number.MAX_VALUE],
[Number.MAX_VALUE, -Number.MAX_VALUE]
];
}
const b = this.borders[index];
if (i < b[0]) b[0] = i;
if (i > b[1]) b[1] = i;
}
processBorderOverlap(p: Point, prev: Point) {
if (p.x === prev.x) {
if (p.y === prev.y) return; // custom defined geojson could have points repeated.
const index = p.x === 0 ? 0 : 1;
this.addBorderIntersection(index, prev.y);
this.addBorderIntersection(index, p.y);
} else {
assert(p.y === prev.y);
const index = p.y === 0 ? 2 : 3;
this.addBorderIntersection(index, prev.x);
this.addBorderIntersection(index, p.x);
}
}
centroid(): Point {
const count = this.polyCount.reduce((acc, p) => acc + p.edges, 0);
return count !== 0 ? this.acc.div(count)._round() : new Point(0, 0);
}
span(): Point {
return new Point(this.max.x - this.min.x, this.max.y - this.min.y);
}
intersectsCount(): number {
return this.borders.reduce((acc, p) => acc + +(p[0] !== Number.MAX_VALUE), 0);
}
}
class FillExtrusionBucket implements Bucket {
index: number;
zoom: number;
canonical: CanonicalTileID;
overscaling: number;
enableTerrain: boolean;
layers: Array<FillExtrusionStyleLayer>;
layerIds: Array<string>;
stateDependentLayers: Array<FillExtrusionStyleLayer>;
stateDependentLayerIds: Array<string>;
layoutVertexArray: FillExtrusionLayoutArray;
layoutVertexBuffer: VertexBuffer;
centroidVertexArray: FillExtrusionCentroidArray;
centroidVertexBuffer: VertexBuffer;
layoutVertexExtArray: ?FillExtrusionExtArray;
layoutVertexExtBuffer: ?VertexBuffer;
indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer;
hasPattern: boolean;
edgeRadius: number;
programConfigurations: ProgramConfigurationSet<FillExtrusionStyleLayer>;
segments: SegmentVector;
uploaded: boolean;
features: Array<BucketFeature>;
featuresOnBorder: Array<PartMetadata>;
// borders / borderDoneWithNeighborZ: 0 - left, 1, right, 2 - top, 3 - bottom
borders: Array<Array<number>>; // For each side, indices into featuresOnBorder array.
borderDoneWithNeighborZ: Array<number>;
needsCentroidUpdate: boolean;
tileToMeter: number; // cache conversion.
projection: ProjectionSpecification;
constructor(options: BucketParameters<FillExtrusionStyleLayer>) {
this.zoom = options.zoom;
this.canonical = options.canonical;
this.overscaling = options.overscaling;
this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index;
this.hasPattern = false;
this.edgeRadius = 0;
this.projection = options.projection;
this.layoutVertexArray = new FillExtrusionLayoutArray();
this.centroidVertexArray = new FillExtrusionCentroidArray();
this.indexArray = new TriangleIndexArray();
this.programConfigurations = new ProgramConfigurationSet(options.layers, options.zoom);
this.segments = new SegmentVector();
this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id);
this.enableTerrain = options.enableTerrain;
}
populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID, tileTransform: TileTransform) {
this.features = [];
this.hasPattern = hasPattern('fill-extrusion', this.layers, options);
this.featuresOnBorder = [];
this.borders = [[], [], [], []];
this.borderDoneWithNeighborZ = [-1, -1, -1, -1];
this.tileToMeter = tileToMeter(canonical);
this.edgeRadius = this.layers[0].layout.get('fill-extrusion-edge-radius') / this.tileToMeter;
for (const {feature, id, index, sourceLayerIndex} of features) {
const needGeometry = this.layers[0]._featureFilter.needGeometry;
const evaluationFeature = toEvaluationFeature(feature, needGeometry);
// $FlowFixMe[method-unbinding]
if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue;
const bucketFeature: BucketFeature = {
id,
sourceLayerIndex,
index,
geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature, canonical, tileTransform),
properties: feature.properties,
type: feature.type,
patterns: {}
};
const vertexArrayOffset = this.layoutVertexArray.length;
if (this.hasPattern) {
this.features.push(addPatternDependencies('fill-extrusion', this.layers, bucketFeature, this.zoom, options));
} else {
this.addFeature(bucketFeature, bucketFeature.geometry, index, canonical, {}, options.availableImages, tileTransform);
}
options.featureIndex.insert(feature, bucketFeature.geometry, index, sourceLayerIndex, this.index, vertexArrayOffset);
}
this.sortBorders();
}
addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: SpritePositions, availableImages: Array<string>, tileTransform: TileTransform) {
for (const feature of this.features) {
const {geometry} = feature;
this.addFeature(feature, geometry, feature.index, canonical, imagePositions, availableImages, tileTransform);
}
this.sortBorders();
}
update(states: FeatureStates, vtLayer: IVectorTileLayer, availableImages: Array<string>, imagePositions: SpritePositions) {
if (!this.stateDependentLayers.length) return;
this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, availableImages, imagePositions);
}
isEmpty(): boolean {
return this.layoutVertexArray.length === 0;
}
uploadPending(): boolean {
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);
if (this.layoutVertexExtArray) {
this.layoutVertexExtBuffer = context.createVertexBuffer(this.layoutVertexExtArray, fillExtrusionAttributesExt.members, true);
}
}
this.programConfigurations.upload(context);
this.uploaded = true;
}
uploadCentroid(context: Context) {
if (this.centroidVertexArray.length === 0) return;
if (!this.centroidVertexBuffer) {
this.centroidVertexBuffer = context.createVertexBuffer(this.centroidVertexArray, centroidAttributes.members, true);
} else if (this.needsCentroidUpdate) {
this.centroidVertexBuffer.updateData(this.centroidVertexArray);
}
this.needsCentroidUpdate = false;
}
destroy() {
if (!this.layoutVertexBuffer) return;
this.layoutVertexBuffer.destroy();
if (this.centroidVertexBuffer) {
this.centroidVertexBuffer.destroy();
}
if (this.layoutVertexExtBuffer) {
this.layoutVertexExtBuffer.destroy();
}
this.indexBuffer.destroy();
this.programConfigurations.destroy();
this.segments.destroy();
}
addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, canonical: CanonicalTileID, imagePositions: SpritePositions, availableImages: Array<string>, tileTransform: TileTransform) {
const tileBounds = [new Point(0, 0), new Point(EXTENT, EXTENT)];
const projection = tileTransform.projection;
const isGlobe = projection.name === 'globe';
const metadata = this.enableTerrain && !isGlobe ? new PartMetadata() : null;
const isPolygon = vectorTileFeatureTypes[feature.type] === 'Polygon';
if (isGlobe && !this.layoutVertexExtArray) {
this.layoutVertexExtArray = new FillExtrusionExtArray();
}
const polygons = classifyRings(geometry, EARCUT_MAX_RINGS);
for (let i = polygons.length - 1; i >= 0; i--) {
const polygon = polygons[i];
if (polygon.length === 0 || isEntirelyOutside(polygon[0])) {
polygons.splice(i, 1);
}
}
let clippedPolygons: ClippedPolygon[];
if (isGlobe) {
// Perform tesselation for polygons of tiles in order to support long planar
// triangles on the curved surface of the globe. This is done for all polygons
// regardless of their size in order guarantee identical results on all sides of
// tile boundaries.
//
// The globe is subdivided into a 32x16 grid. The number of subdivisions done
// for a tile depends on the zoom level. For example tile with z=0 requires 2⁴
// subdivisions, tile with z=1 2³ etc. The subdivision is done in polar coordinates
// instead of tile coordinates.
clippedPolygons = resampleFillExtrusionPolygonsForGlobe(polygons, tileBounds, canonical);
} else {
clippedPolygons = [];
for (const polygon of polygons) {
clippedPolygons.push({polygon, bounds: tileBounds});
}
}
const edgeRadius = isPolygon ? this.edgeRadius : 0;
for (const {polygon, bounds} of clippedPolygons) {
// Only triangulate and draw the area of the feature if it is a polygon
// Other feature types (e.g. LineString) do not have area, so triangulation is pointless / undefined
let topIndex = 0;
let numVertices = 0;
for (const ring of polygon) {
// make sure the ring closes
if (isPolygon && !ring[0].equals(ring[ring.length - 1])) ring.push(ring[0]);
numVertices += (isPolygon ? (ring.length - 1) : ring.length);
}
// We use "(isPolygon ? 5 : 4) * numVertices" as an estimate to ensure whether additional segments are needed or not (see SegmentVector.MAX_VERTEX_ARRAY_LENGTH).
const segment = this.segments.prepareSegment((isPolygon ? 5 : 4) * numVertices, this.layoutVertexArray, this.indexArray);
if (isPolygon) {
const flattened = [];
const holeIndices = [];
topIndex = segment.vertexLength;
// First we offset (inset) the top vertices (i.e the vertices that make up the roof).
for (const ring of polygon) {
if (ring.length && ring !== polygon[0]) {
holeIndices.push(flattened.length / 2);
}
// The following vectors are used to avoid duplicate normal calculations when going over the vertices.
let na, nb;
{
const p0 = ring[0];
const p1 = ring[1];
na = p1.sub(p0)._perp()._unit();
}
for (let i = 1; i < ring.length; i++) {
const p1 = ring[i];
const p2 = ring[i === ring.length - 1 ? 1 : i + 1];
let {x, y} = p1;
if (edgeRadius) {
nb = p2.sub(p1)._perp()._unit();
const nm = na.add(nb)._unit();
const cosHalfAngle = na.x * nm.x + na.y * nm.y;
const offset = edgeRadius * Math.min(4, 1 / cosHalfAngle);
x += offset * nm.x;
y += offset * nm.y;
na = nb;
}
addVertex(this.layoutVertexArray, x, y, 0, 0, 1, 1, 0);
segment.vertexLength++;
// triangulate as if vertices were not offset to ensure correct triangulation
flattened.push(p1.x, p1.y);
if (isGlobe) {
const array: any = this.layoutVertexExtArray;
const projectedP = projection.projectTilePoint(x, y, canonical);
const n = projection.upVector(canonical, x, y);
addGlobeExtVertex(array, projectedP, n);
}
}
}
const indices = earcut(flattened, holeIndices);
assert(indices.length % 3 === 0);
for (let j = 0; j < indices.length; j += 3) {
// clockwise winding order.
this.indexArray.emplaceBack(
topIndex + indices[j],
topIndex + indices[j + 2],
topIndex + indices[j + 1]);
segment.primitiveLength++;
}
}
for (const ring of polygon) {
if (metadata && ring.length) metadata.startRing(ring[0]);
let isPrevCornerConcave = ring.length > 4 && isAOConcaveAngle(ring[ring.length - 2], ring[0], ring[1]);
let offsetPrev = edgeRadius ? getRoundedEdgeOffset(ring[ring.length - 2], ring[0], ring[1], edgeRadius) : 0;
let kFirst;
// The following vectors are used to avoid duplicate normal calculations when going over the vertices.
let na, nb;
{
const p0 = ring[0];
const p1 = ring[1];
na = p1.sub(p0)._perp()._unit();
}
let cap = true;
for (let i = 1, edgeDistance = 0; i < ring.length; i++) {
let p0 = ring[i - 1];
let p1 = ring[i];
const p2 = ring[i === ring.length - 1 ? 1 : i + 1];
if (metadata && isPolygon) metadata.currentPolyCount.top++;
if (isEdgeOutsideBounds(p1, p0, bounds)) {
if (edgeRadius) {
na = p2.sub(p1)._perp()._unit();
cap = !cap;
}
continue;
}
if (metadata) metadata.append(p1, p0);
const d = p1.sub(p0)._perp();
// Given that nz === 0, encode nx / (abs(nx) + abs(ny)) and signs.
// This information is sufficient to reconstruct normal vector in vertex shader.
const nxRatio = d.x / (Math.abs(d.x) + Math.abs(d.y));
const nySign = d.y > 0 ? 1 : 0;
const dist = p0.dist(p1);
if (edgeDistance + dist > 32768) edgeDistance = 0;
// Next offset the vertices along the edges and create a chamfer space between them:
// So if we have the following (where 'x' denotes a vertex)
// x──────x
// | |
// | |
// | |
// | |
// x──────x
// we end up with:
// x────x
// x x
// | |
// | |
// x x
// x────x
// (drawing isn't exact but hopefully gets the point across).
if (edgeRadius) {
nb = p2.sub(p1)._perp()._unit();
const cosHalfAngle = getCosHalfAngle(na, nb);
let offsetNext = _getRoundedEdgeOffset(p0, p1, p2, cosHalfAngle, edgeRadius);
if (isNaN(offsetNext)) offsetNext = 0;
const nEdge = p1.sub(p0)._unit();
p0 = p0.add(nEdge.mult(offsetPrev))._round();
p1 = p1.add(nEdge.mult(-offsetNext))._round();
offsetPrev = offsetNext;
na = nb;
}
const k = segment.vertexLength;
const isConcaveCorner = ring.length > 4 && isAOConcaveAngle(p0, p1, p2);
let encodedEdgeDistance = encodeAOToEdgeDistance(edgeDistance, isPrevCornerConcave, cap);
addVertex(this.layoutVertexArray, p0.x, p0.y, nxRatio, nySign, 0, 0, encodedEdgeDistance);
addVertex(this.layoutVertexArray, p0.x, p0.y, nxRatio, nySign, 0, 1, encodedEdgeDistance);
edgeDistance += dist;
encodedEdgeDistance = encodeAOToEdgeDistance(edgeDistance, isConcaveCorner, !cap);
isPrevCornerConcave = isConcaveCorner;
addVertex(this.layoutVertexArray, p1.x, p1.y, nxRatio, nySign, 0, 0, encodedEdgeDistance);
addVertex(this.layoutVertexArray, p1.x, p1.y, nxRatio, nySign, 0, 1, encodedEdgeDistance);
segment.vertexLength += 4;
// ┌──────┐
// │ 1 3 │ clockwise winding order.
// │ │ Triangle 1: 0 => 1 => 2
// │ 0 2 │ Triangle 2: 1 => 3 => 2
// └──────┘
this.indexArray.emplaceBack(k + 0, k + 1, k + 2);
this.indexArray.emplaceBack(k + 1, k + 3, k + 2);
segment.primitiveLength += 2;
if (edgeRadius) {
// Note that in the previous for-loop we start from index 1 to add the top vertices which explains the next line.
const t0 = topIndex + (i === 1 ? ring.length - 2 : i - 2);
const t1 = i === 1 ? topIndex : t0 + 1;
// top chamfer along the side (i.e. the space between the wall and the roof).
this.indexArray.emplaceBack(k + 1, t0, k + 3);
this.indexArray.emplaceBack(t0, t1, k + 3);
segment.primitiveLength += 2;
if (kFirst === undefined) {
kFirst = k;
}
// Make sure to fill in the gap in the corner only when both corresponding edges are in tile bounds.
if (!isEdgeOutsideBounds(p2, ring[i], bounds)) {
const l = i === ring.length - 1 ? kFirst : segment.vertexLength;
// vertical side chamfer i.e. the space between consecutive walls.
this.indexArray.emplaceBack(k + 2, k + 3, l);
this.indexArray.emplaceBack(k + 3, l + 1, l);
// top corner where the top(roof) and two sides(walls) meet.
this.indexArray.emplaceBack(k + 3, t1, l + 1);
segment.primitiveLength += 3;
}
cap = !cap;
}
if (isGlobe) {
const array: any = this.layoutVertexExtArray;
const projectedP0 = projection.projectTilePoint(p0.x, p0.y, canonical);
const projectedP1 = projection.projectTilePoint(p1.x, p1.y, canonical);
const n0 = projection.upVector(canonical, p0.x, p0.y);
const n1 = projection.upVector(canonical, p1.x, p1.y);
addGlobeExtVertex(array, projectedP0, n0);
addGlobeExtVertex(array, projectedP0, n0);
addGlobeExtVertex(array, projectedP1, n1);
addGlobeExtVertex(array, projectedP1, n1);
}
}
if (isPolygon) topIndex += (ring.length - 1);
}
}
assert(!isGlobe || (this.layoutVertexExtArray && this.layoutVertexExtArray.length === this.layoutVertexArray.length));
if (metadata && metadata.polyCount.length > 0) {
// When building is split between tiles, don't handle flat roofs here.
if (metadata.borders) {
// Store to the bucket. Flat roofs are handled in flatRoofsUpdate,
// after joining parts that lay in different buckets.
metadata.vertexArrayOffset = this.centroidVertexArray.length;
const borders = metadata.borders;
const index = this.featuresOnBorder.push(metadata) - 1;
for (let i = 0; i < 4; i++) {
if (borders[i][0] !== Number.MAX_VALUE) { this.borders[i].push(index); }
}
}
this.encodeCentroid(metadata.borders ? undefined : metadata.centroid(), metadata);
assert(!this.centroidVertexArray.length || this.centroidVertexArray.length === this.layoutVertexArray.length);
}
this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, imagePositions, availableImages, canonical);
}
sortBorders() {
for (let i = 0; i < 4; i++) {
// Sort by border intersection area minimums, ascending.
this.borders[i].sort((a, b) => this.featuresOnBorder[a].borders[i][0] - this.featuresOnBorder[b].borders[i][0]);
}
}
encodeCentroid(c: ?Point, metadata: PartMetadata, append: boolean = true) {
let x, y;
// Encoded centroid x and y:
// x y
// ---------------------------------------------
// 0 0 Default, no flat roof.
// 0 1 Hide, used to hide parts of buildings on border while expecting the other side to get loaded
// >0 0 Elevation encoded to uint16 word
// >0 >0 Encoded centroid position and x & y span
if (c) {
if (c.y !== 0) {
const span = metadata.span()._mult(this.tileToMeter);
x = (Math.max(c.x, 1) << 3) + Math.min(7, Math.round(span.x / 10));
y = (Math.max(c.y, 1) << 3) + Math.min(7, Math.round(span.y / 10));
} else { // encode height:
x = Math.ceil((c.x + ELEVATION_OFFSET) * ELEVATION_SCALE);
y = 0;
}
} else {
// Use the impossible situation (building that has width and doesn't cross border cannot have centroid
// at border) to encode unprocessed border building: it is initially (append === true) hidden until
// computing centroid for joined building parts in rendering thread (flatRoofsUpdate). If it intersects more than
// two borders, flat roof approach is not applied.
x = 0;
y = +append; // Hide (1) initially when creating - visibility is changed in draw_fill_extrusion as soon as neighbor tile gets loaded.
}
assert(append || metadata.vertexArrayOffset !== undefined);
let offset = append ? this.centroidVertexArray.length : metadata.vertexArrayOffset;
for (const polyInfo of metadata.polyCount) {
if (append) {
this.centroidVertexArray.resize(this.centroidVertexArray.length + polyInfo.edges * 4 + polyInfo.top);
}
for (let i = 0; i < polyInfo.top; i++) {
this.centroidVertexArray.emplace(offset++, x, y);
}
for (let i = 0; i < polyInfo.edges * 2; i++) {
this.centroidVertexArray.emplace(offset++, 0, y);
this.centroidVertexArray.emplace(offset++, x, y);
}
}
}
}
function getCosHalfAngle(na: Point, nb: Point) {
const nm = na.add(nb)._unit();
const cosHalfAngle = na.x * nm.x + na.y * nm.y;
return cosHalfAngle;
}
function getRoundedEdgeOffset(p0: Point, p1: Point, p2: Point, edgeRadius: number) {
const na = p1.sub(p0)._perp()._unit();
const nb = p2.sub(p1)._perp()._unit();
const cosHalfAngle = getCosHalfAngle(na, nb);
return _getRoundedEdgeOffset(p0, p1, p2, cosHalfAngle, edgeRadius);
}
function _getRoundedEdgeOffset(p0: Point, p1: Point, p2: Point, cosHalfAngle: number, edgeRadius: number) {
const sinHalfAngle = Math.sqrt(1 - cosHalfAngle * cosHalfAngle);
return Math.min(p0.dist(p1) / 3, p1.dist(p2) / 3, edgeRadius * sinHalfAngle / cosHalfAngle);
}
register(FillExtrusionBucket, 'FillExtrusionBucket', {omit: ['layers', 'features']});
register(PartMetadata, 'PartMetadata');
export default FillExtrusionBucket;
// Edges that are outside tile bounds are defined in tile across the border.
// Rendering them twice often results with Z-fighting.
// In case of globe and axis aligned bounds, it is also useful to
// discard edges that have the both endpoints outside the same bound.
function isEdgeOutsideBounds(p1: Point, p2: Point, bounds: [Point, Point]) {
return (p1.x < bounds[0].x && p2.x < bounds[0].x) ||
(p1.x > bounds[1].x && p2.x > bounds[1].x) ||
(p1.y < bounds[0].y && p2.y < bounds[0].y) ||
(p1.y > bounds[1].y && p2.y > bounds[1].y);
}
function isEntirelyOutside(ring: Array<Point>) {
// Discard rings with corners on border if all other vertices are outside: they get defined
// also in the tile across the border. Eventual zero area rings at border are discarded by classifyRings
// and there is no need to handle that case here.
return ring.every(p => p.x <= 0) ||
ring.every(p => p.x >= EXTENT) ||
ring.every(p => p.y <= 0) ||
ring.every(p => p.y >= EXTENT);
}
function tileToMeter(canonical: CanonicalTileID) {
const circumferenceAtEquator = 40075017;
const mercatorY = canonical.y / (1 << canonical.z);
const exp = Math.exp(Math.PI * (1 - 2 * mercatorY));
// simplify cos(2 * atan(e) - PI/2) from mercator_coordinate.js, remove trigonometrics.
return circumferenceAtEquator * 2 * exp / (exp * exp + 1) / EXTENT / (1 << canonical.z);
}
function isAOConcaveAngle(p2: Point, p1: Point, p3: Point) {
if (p2.x < 0 || p2.x >= EXTENT || p1.x < 0 || p1.x >= EXTENT || p3.x < 0 || p3.x >= EXTENT) {
return false; // angles are not processed for edges that extend over tile borders
}
const a = p3.sub(p1);
const an = a.perp();
const b = p2.sub(p1);
const ab = a.x * b.x + a.y * b.y;
const cosAB = ab / Math.sqrt(((a.x * a.x + a.y * a.y) * (b.x * b.x + b.y * b.y)));
const dotProductWithNormal = an.x * b.x + an.y * b.y;
// Heuristics: don't shade concave angles above 150° (arccos(-0.866)).
return cosAB > -0.866 && dotProductWithNormal < 0;
}
function encodeAOToEdgeDistance(edgeDistance: number, isConcaveCorner: boolean, edgeStart: boolean) {
// Encode concavity and edge start/end using the least significant bits.
// Second least significant bit 1 encodes concavity.
// The least significant bit 1 marks the edge start, 0 for edge end.
const encodedEdgeDistance = isConcaveCorner ? (edgeDistance | 2) : (edgeDistance & ~2);
return edgeStart ? (encodedEdgeDistance | 1) : (encodedEdgeDistance & ~1);
}
export function fillExtrusionHeightLift(): number {
// A rectangle covering globe is subdivided into a grid of 32 cells
// This information can be used to deduce a minimum lift value so that
// fill extrusions with 0 height will never go below the ground.
const angle = Math.PI / 32.0;
const tanAngle = Math.tan(angle);
const r = earthRadius;
return r * Math.sqrt(1.0 + 2.0 * tanAngle * tanAngle) - r;
}
// Resamples fill extrusion polygons by subdividing them into 32x16 cells in mercator space.
// The idea is to allow reprojection of large continuous planar shapes on the surface of the globe
export function resampleFillExtrusionPolygonsForGlobe(polygons: Point[][][], tileBounds: [Point, Point], tileID: CanonicalTileID): ClippedPolygon[] {
const cellCount = 360.0 / 32.0;
const tiles = 1 << tileID.z;
const leftLng = lngFromMercatorX(tileID.x / tiles);
const rightLng = lngFromMercatorX((tileID.x + 1) / tiles);
const topLat = latFromMercatorY(tileID.y / tiles);
const bottomLat = latFromMercatorY((tileID.y + 1) / tiles);
const cellCountOnXAxis = Math.ceil((rightLng - leftLng) / cellCount);
const cellCountOnYAxis = Math.ceil((topLat - bottomLat) / cellCount);
const splitFn = (axis: number, min: number, max: number) => {
if (axis === 0) {
return 0.5 * (min + max);
} else {
const maxLat = latFromMercatorY((tileID.y + min / EXTENT) / tiles);
const minLat = latFromMercatorY((tileID.y + max / EXTENT) / tiles);
const midLat = 0.5 * (minLat + maxLat);
return (mercatorYfromLat(midLat) * tiles - tileID.y) * EXTENT;
}
};
return subdividePolygons(polygons, tileBounds, cellCountOnXAxis, cellCountOnYAxis, 1.0, splitFn);
}