diff --git a/package.json b/package.json index 6c4372d249e..930b1794d31 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mapbox-gl", "description": "A WebGL interactive maps library", - "version": "1.7.0-dev", + "version": "1.8.0-dev", "main": "dist/mapbox-gl.js", "style": "dist/mapbox-gl.css", "license": "SEE LICENSE IN LICENSE.txt", @@ -23,7 +23,7 @@ "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", "csscolorparser": "~1.0.2", - "earcut": "^2.2.0", + "earcut": "^2.2.2", "geojson-vt": "^3.2.1", "gl-matrix": "^3.0.0", "grid-index": "^1.1.0", diff --git a/src/css/mapbox-gl.css b/src/css/mapbox-gl.css index 92a1731546a..e0c16e6e568 100644 --- a/src/css/mapbox-gl.css +++ b/src/css/mapbox-gl.css @@ -761,6 +761,13 @@ a.mapboxgl-ctrl-logo.mapboxgl-compact { display: none; } +.mapboxgl-user-location-accuracy-circle { + background-color: #1da1f233; + width: 1px; + height: 1px; + border-radius: 100%; +} + .mapboxgl-crosshair, .mapboxgl-crosshair .mapboxgl-interactive, .mapboxgl-crosshair .mapboxgl-interactive:active { diff --git a/src/data/bucket/line_bucket.js b/src/data/bucket/line_bucket.js index 7e11a4fdae4..bb63e4d3a4e 100644 --- a/src/data/bucket/line_bucket.js +++ b/src/data/bucket/line_bucket.js @@ -121,7 +121,7 @@ class LineBucket implements Bucket { const lineSortKey = this.layers[0].layout.get('line-sort-key'); const bucketFeatures = []; - for (const {feature, index, sourceLayerIndex} of features) { + for (const {feature, id, index, sourceLayerIndex} of features) { if (!this.layers[0]._featureFilter(new EvaluationParameters(this.zoom), feature)) continue; const geometry = loadGeometry(feature); @@ -130,7 +130,7 @@ class LineBucket implements Bucket { undefined; const bucketFeature: BucketFeature = { - id: feature.id, + id, properties: feature.properties, type: feature.type, sourceLayerIndex, @@ -231,6 +231,7 @@ class LineBucket implements Bucket { for (let i = 0; i < vertices.length - 1; i++) { this.totalDistance += vertices[i].dist(vertices[i + 1]); } + this.updateScaledDistance(); } const isPolygon = vectorTileFeatureTypes[feature.type] === 'Polygon'; @@ -273,8 +274,8 @@ class LineBucket implements Bucket { 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 + nextVertex = i === len - 1 ? + (isPolygon ? vertices[first + 1] : (undefined: any)) : // if it's a polygon, treat the last vertex like the first vertices[i + 1]; // just the next vertex // if two consecutive vertices exist, skip the current one @@ -526,9 +527,7 @@ class LineBucket implements Bucket { } } - updateDistance(prev: Point, next: Point) { - this.distance += prev.dist(next); - + updateScaledDistance() { // 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 @@ -537,6 +536,11 @@ class LineBucket implements Bucket { (this.clipStart + (this.clipEnd - this.clipStart) * this.distance / this.totalDistance) * (MAX_LINE_DISTANCE - 1) : this.distance; } + + updateDistance(prev: Point, next: Point) { + this.distance += prev.dist(next); + this.updateScaledDistance(); + } } register('LineBucket', LineBucket, {omit: ['layers', 'patternFeatures']}); diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 8b8def60f32..fac4120b762 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -177,7 +177,18 @@ export class SymbolBuffers { this.placedSymbolArray = new PlacedSymbolArray(); } + isEmpty() { + return this.layoutVertexArray.length === 0 && + this.indexArray.length === 0 && + this.dynamicLayoutVertexArray.length === 0 && + this.opacityVertexArray.length === 0; + } + upload(context: Context, dynamicIndexBuffer: boolean, upload?: boolean, update?: boolean) { + if (this.isEmpty()) { + return; + } + if (upload) { this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, symbolLayoutAttributes.members); this.indexBuffer = context.createIndexBuffer(this.indexArray, dynamicIndexBuffer); @@ -376,11 +387,6 @@ class SymbolBucket implements Bucket { this.text = new SymbolBuffers(new ProgramConfigurationSet(symbolLayoutAttributes.members, this.layers, this.zoom, property => /^text/.test(property))); this.icon = new SymbolBuffers(new ProgramConfigurationSet(symbolLayoutAttributes.members, this.layers, this.zoom, property => /^icon/.test(property))); - this.textCollisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray); - this.iconCollisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray); - this.textCollisionCircle = new CollisionBuffers(CollisionCircleLayoutArray, collisionCircleLayout.members, TriangleIndexArray); - this.iconCollisionCircle = new CollisionBuffers(CollisionCircleLayoutArray, collisionCircleLayout.members, TriangleIndexArray); - this.glyphOffsetArray = new GlyphOffsetArray(); this.lineVertexArray = new SymbolLineVertexArray(); this.symbolInstances = new SymbolInstanceArray(); @@ -539,7 +545,7 @@ class SymbolBucket implements Bucket { } upload(context: Context) { - if (!this.uploaded) { + if (!this.uploaded && this.hasDebugData()) { this.textCollisionBox.upload(context); this.iconCollisionBox.upload(context); this.textCollisionCircle.upload(context); @@ -550,15 +556,22 @@ class SymbolBucket implements Bucket { this.uploaded = true; } - destroy() { - this.text.destroy(); - this.icon.destroy(); + destroyDebugData() { this.textCollisionBox.destroy(); this.iconCollisionBox.destroy(); this.textCollisionCircle.destroy(); this.iconCollisionCircle.destroy(); } + destroy() { + this.text.destroy(); + this.icon.destroy(); + + if (this.hasDebugData()) { + this.destroyDebugData(); + } + } + addToLineVertexArray(anchor: Anchor, line: any) { const lineStartIndex = this.lineVertexArray.length; if (anchor.segment !== undefined) { @@ -752,6 +765,15 @@ class SymbolBucket implements Bucket { } generateCollisionDebugBuffers() { + if (this.hasDebugData()) { + this.destroyDebugData(); + } + + this.textCollisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray); + this.iconCollisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray); + this.textCollisionCircle = new CollisionBuffers(CollisionCircleLayoutArray, collisionCircleLayout.members, TriangleIndexArray); + this.iconCollisionCircle = new CollisionBuffers(CollisionCircleLayoutArray, collisionCircleLayout.members, TriangleIndexArray); + for (let i = 0; i < this.symbolInstances.length; i++) { const symbolInstance = this.symbolInstances.get(i); this.addDebugCollisionBoxes(symbolInstance.textBoxStartIndex, symbolInstance.textBoxEndIndex, symbolInstance, true); @@ -840,20 +862,24 @@ class SymbolBucket implements Bucket { return this.icon.segments.get().length > 0; } + hasDebugData() { + return this.textCollisionBox && this.iconCollisionBox && this.textCollisionCircle && this.iconCollisionCircle; + } + hasTextCollisionBoxData() { - return this.textCollisionBox.segments.get().length > 0; + return this.hasDebugData() && this.textCollisionBox.segments.get().length > 0; } hasIconCollisionBoxData() { - return this.iconCollisionBox.segments.get().length > 0; + return this.hasDebugData() && this.iconCollisionBox.segments.get().length > 0; } hasTextCollisionCircleData() { - return this.textCollisionCircle.segments.get().length > 0; + return this.hasDebugData() && this.textCollisionCircle.segments.get().length > 0; } hasIconCollisionCircleData() { - return this.iconCollisionCircle.segments.get().length > 0; + return this.hasDebugData() && this.iconCollisionCircle.segments.get().length > 0; } addIndicesForPlacedSymbol(iconOrText: SymbolBuffers, placedSymbolIndex: number) { diff --git a/src/geo/lng_lat.js b/src/geo/lng_lat.js index 0d565158220..971214994a3 100644 --- a/src/geo/lng_lat.js +++ b/src/geo/lng_lat.js @@ -3,6 +3,13 @@ import {wrap} from '../util/util'; import LngLatBounds from './lng_lat_bounds'; +/* +* Approximate radius of the earth in meters. +* Uses the WGS-84 approximation. The radius at the equator is ~6378137 and at the poles is ~6356752. https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84 +* 6371008.8 is one published "average radius" see https://en.wikipedia.org/wiki/Earth_radius#Mean_radius, or ftp://athena.fsv.cvut.cz/ZFG/grs80-Moritz.pdf p.4 +*/ +export const earthRadius = 6371008.8; + /** * A `LngLat` object represents a given longitude and latitude coordinate, measured in degrees. * @@ -73,6 +80,27 @@ class LngLat { return `LngLat(${this.lng}, ${this.lat})`; } + /** + * Returns the approximate distance between a pair of coordinates in meters + * Uses the Haversine Formula (from R.W. Sinnott, "Virtues of the Haversine", Sky and Telescope, vol. 68, no. 2, 1984, p. 159) + * + * @param {LngLat} lngLat coordinates to compute the distance to + * @returns {number} Distance in meters between the two coordinates. + * @example + * var new_york = new mapboxgl.LngLat(-74.0060, 40.7128); + * var los_angeles = new mapboxgl.LngLat(-118.2437, 34.0522); + * new_york.distanceTo(los_angeles); // = 3935751.690893987, "true distance" using a non-spherical approximation is ~3966km + */ + distanceTo(lngLat: LngLat) { + const rad = Math.PI / 180; + const lat1 = this.lat * rad; + const lat2 = lngLat.lat * rad; + const a = Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos((lngLat.lng - this.lng) * rad); + + const maxMeters = earthRadius * Math.acos(Math.min(a, 1)); + return maxMeters; + } + /** * Returns a `LngLatBounds` from the coordinates extended by a given `radius`. The returned `LngLatBounds` completely contains the `radius`. * diff --git a/src/geo/mercator_coordinate.js b/src/geo/mercator_coordinate.js index 02bba7eb8fe..bc593b1ae2a 100644 --- a/src/geo/mercator_coordinate.js +++ b/src/geo/mercator_coordinate.js @@ -1,18 +1,18 @@ // @flow -import LngLat from '../geo/lng_lat'; +import LngLat, {earthRadius} from '../geo/lng_lat'; import type {LngLatLike} from '../geo/lng_lat'; /* - * The circumference of the world in meters at the equator. + * The average circumference of the world in meters. */ -const circumferenceAtEquator = 2 * Math.PI * 6378137; +const earthCircumfrence = 2 * Math.PI * earthRadius; // meters /* - * The circumference of the world in meters at the given latitude. + * The circumference at a line of latitude in meters. */ function circumferenceAtLatitude(latitude: number) { - return circumferenceAtEquator * Math.cos(latitude * Math.PI / 180); + return earthCircumfrence * Math.cos(latitude * Math.PI / 180); } export function mercatorXfromLng(lng: number) { @@ -142,7 +142,7 @@ class MercatorCoordinate { */ meterInMercatorCoordinateUnits() { // 1 meter / circumference at equator in meters * Mercator projection scale factor at this latitude - return 1 / circumferenceAtEquator * mercatorScale(latFromMercatorY(this.y)); + return 1 / earthCircumfrence * mercatorScale(latFromMercatorY(this.y)); } } diff --git a/src/geo/transform.js b/src/geo/transform.js index f80dc279277..af33f5922d9 100644 --- a/src/geo/transform.js +++ b/src/geo/transform.js @@ -6,12 +6,11 @@ import MercatorCoordinate, {mercatorXfromLng, mercatorYfromLat, mercatorZfromAlt import Point from '@mapbox/point-geometry'; import {wrap, clamp} from '../util/util'; import {number as interpolate} from '../style-spec/util/interpolate'; -import tileCover from '../util/tile_cover'; -import {UnwrappedTileID} from '../source/tile_id'; import EXTENT from '../data/extent'; -import {vec4, mat4, mat2} from 'gl-matrix'; +import {vec4, mat4, mat2, vec2} from 'gl-matrix'; +import {Aabb, Frustum} from '../util/primitives.js'; -import type {OverscaledTileID, CanonicalTileID} from '../source/tile_id'; +import {UnwrappedTileID, OverscaledTileID, CanonicalTileID} from '../source/tile_id'; /** * A single transform, generally used for a single tile to be @@ -34,6 +33,7 @@ class Transform { cameraToCenterDistance: number; mercatorMatrix: Array; projMatrix: Float64Array; + invProjMatrix: Float64Array; alignedProjMatrix: Float64Array; pixelMatrix: Float64Array; pixelMatrixInverse: Float64Array; @@ -278,15 +278,90 @@ class Transform { const centerCoord = MercatorCoordinate.fromLngLat(this.center); const numTiles = Math.pow(2, z); - const centerPoint = new Point(numTiles * centerCoord.x - 0.5, numTiles * centerCoord.y - 0.5); - const cornerCoords = [ - this.pointCoordinate(new Point(0, 0)), - this.pointCoordinate(new Point(this.width, 0)), - this.pointCoordinate(new Point(this.width, this.height)), - this.pointCoordinate(new Point(0, this.height)) - ]; - return tileCover(z, cornerCoords, options.reparseOverscaled ? actualZ : z, this._renderWorldCopies) - .sort((a, b) => centerPoint.dist(a.canonical) - centerPoint.dist(b.canonical)); + const centerPoint = [numTiles * centerCoord.x, numTiles * centerCoord.y, 0]; + const cameraFrustum = Frustum.fromInvProjectionMatrix(this.invProjMatrix, this.worldSize, z); + + // No change of LOD behavior for pitch lower than 60: return only tile ids from the requested zoom level + let minZoom = options.minzoom || 0; + if (this.pitch <= 60.0) + minZoom = z; + + // There should always be a certain number of maximum zoom level tiles surrounding the center location + const radiusOfMaxLvlLodInTiles = 3; + + const newRootTile = (wrap: number): any => { + return { + // All tiles are on zero elevation plane => z difference is zero + aabb: new Aabb([wrap * numTiles, 0, 0], [(wrap + 1) * numTiles, numTiles, 0]), + zoom: 0, + x: 0, + y: 0, + wrap, + fullyVisible: false + }; + }; + + // Do a depth-first traversal to find visible tiles and proper levels of detail + const stack = []; + const result = []; + const maxZoom = z; + const overscaledZ = options.reparseOverscaled ? actualZ : z; + + if (this._renderWorldCopies) { + // Render copy of the globe thrice on both sides + for (let i = 1; i <= 3; i++) { + stack.push(newRootTile(-i)); + stack.push(newRootTile(i)); + } + } + + stack.push(newRootTile(0)); + + while (stack.length > 0) { + const it = stack.pop(); + const x = it.x; + const y = it.y; + let fullyVisible = it.fullyVisible; + + // Visibility of a tile is not required if any of its ancestor if fully inside the frustum + if (!fullyVisible) { + const intersectResult = it.aabb.intersects(cameraFrustum); + + if (intersectResult === 0) + continue; + + fullyVisible = intersectResult === 2; + } + + const distanceX = it.aabb.distanceX(centerPoint); + const distanceY = it.aabb.distanceY(centerPoint); + const longestDim = Math.max(Math.abs(distanceX), Math.abs(distanceY)); + + // We're using distance based heuristics to determine if a tile should be split into quadrants or not. + // radiusOfMaxLvlLodInTiles defines that there's always a certain number of maxLevel tiles next to the map center. + // Using the fact that a parent node in quadtree is twice the size of its children (per dimension) + // we can define distance thresholds for each relative level: + // f(k) = offset + 2 + 4 + 8 + 16 + ... + 2^k. This is the same as "offset+2^(k+1)-2" + const distToSplit = radiusOfMaxLvlLodInTiles + (1 << (maxZoom - it.zoom)) - 2; + + // Have we reached the target depth or is the tile too far away to be any split further? + if (it.zoom === maxZoom || (longestDim > distToSplit && it.zoom >= minZoom)) { + result.push({ + tileID: new OverscaledTileID(it.zoom === maxZoom ? overscaledZ : it.zoom, it.wrap, it.zoom, x, y), + distanceSq: vec2.sqrLen([centerPoint[0] - 0.5 - x, centerPoint[1] - 0.5 - y]) + }); + continue; + } + + for (let i = 0; i < 4; i++) { + const childX = (x << 1) + (i % 2); + const childY = (y << 1) + (i >> 1); + + stack.push({aabb: it.aabb.quadrant(i), zoom: it.zoom + 1, x: childX, y: childY, wrap: it.wrap, fullyVisible}); + } + } + + return result.sort((a, b) => a.distanceSq - b.distanceSq).map(a => a.tileID); } resize(width: number, height: number) { @@ -549,7 +624,7 @@ class Transform { // (the distance between[width/2, height/2] and [width/2 + 1, height/2]) const halfFov = this._fov / 2; const groundAngle = Math.PI / 2 + this._pitch; - const topHalfSurfaceDistance = Math.sin(halfFov) * this.cameraToCenterDistance / Math.sin(Math.PI - groundAngle - halfFov); + const topHalfSurfaceDistance = Math.sin(halfFov) * this.cameraToCenterDistance / Math.sin(clamp(Math.PI - groundAngle - halfFov, 0.01, Math.PI - 0.01)); const point = this.point; const x = point.x, y = point.y; @@ -585,6 +660,7 @@ class Transform { mat4.scale(m, m, [1, 1, mercatorZfromAltitude(1, this.center.lat) * this.worldSize, 1]); this.projMatrix = m; + this.invProjMatrix = mat4.invert([], this.projMatrix); // Make a second projection matrix that is aligned to a pixel grid for rendering raster tiles. // We're rounding the (floating point) x/y values to achieve to avoid rendering raster images to fractional diff --git a/src/render/draw_line.js b/src/render/draw_line.js index 9848dd19b19..481dff0b80c 100644 --- a/src/render/draw_line.js +++ b/src/render/draw_line.js @@ -34,8 +34,8 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay const crossfade = layer.getCrossfadeParameters(); const programId = - dasharray ? 'lineSDF' : image ? 'linePattern' : + dasharray ? 'lineSDF' : gradient ? 'lineGradient' : 'line'; const context = painter.context; @@ -73,18 +73,18 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom); } - const uniformValues = dasharray ? lineSDFUniformValues(painter, tile, layer, dasharray, crossfade) : - image ? linePatternUniformValues(painter, tile, layer, crossfade) : + const uniformValues = image ? linePatternUniformValues(painter, tile, layer, crossfade) : + dasharray ? lineSDFUniformValues(painter, tile, layer, dasharray, crossfade) : gradient ? lineGradientUniformValues(painter, tile, layer) : lineUniformValues(painter, tile, layer); - if (dasharray && (programChanged || painter.lineAtlas.dirty)) { - context.activeTexture.set(gl.TEXTURE0); - painter.lineAtlas.bind(context); - } else if (image) { + if (image) { context.activeTexture.set(gl.TEXTURE0); tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); programConfiguration.updatePatternPaintBuffers(crossfade); + } else if (dasharray && (programChanged || painter.lineAtlas.dirty)) { + context.activeTexture.set(gl.TEXTURE0); + painter.lineAtlas.bind(context); } program.draw(context, gl.TRIANGLES, depthMode, diff --git a/src/render/line_atlas.js b/src/render/line_atlas.js index d3208e9bac4..2f5c857f3b2 100644 --- a/src/render/line_atlas.js +++ b/src/render/line_atlas.js @@ -28,8 +28,7 @@ class LineAtlas { this.height = height; this.nextRow = 0; - this.bytes = 4; - this.data = new Uint8Array(this.width * this.height * this.bytes); + this.data = new Uint8Array(this.width * this.height); this.positions = {}; } @@ -114,7 +113,7 @@ class LineAtlas { signedDistance = (inside ? 1 : -1) * dist; } - this.data[3 + (index + x) * 4] = Math.max(0, Math.min(255, signedDistance + offset)); + this.data[index + x] = Math.max(0, Math.min(255, signedDistance + offset)); } } @@ -139,14 +138,14 @@ class LineAtlas { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.data); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this.width, this.height, 0, gl.ALPHA, gl.UNSIGNED_BYTE, this.data); } else { gl.bindTexture(gl.TEXTURE_2D, this.texture); if (this.dirty) { this.dirty = false; - gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, this.width, this.height, gl.RGBA, gl.UNSIGNED_BYTE, this.data); + gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, this.width, this.height, gl.ALPHA, gl.UNSIGNED_BYTE, this.data); } } } diff --git a/src/shaders/symbol_icon.vertex.glsl b/src/shaders/symbol_icon.vertex.glsl index 16ab111429c..dc1a1e53097 100644 --- a/src/shaders/symbol_icon.vertex.glsl +++ b/src/shaders/symbol_icon.vertex.glsl @@ -87,6 +87,10 @@ void main() { vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, 0.0, 1.0); gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * max(a_minFontScale, fontScale) + a_pxoffset / 16.0), 0.0, 1.0); + // Symbols might end up being behind the camera. Modify z-value to be out of visible bounds + // if this is the case, otherwise ignore depth. -1.1 is safely out of the visible depth range [-1, 1] + gl_Position.z = mix(-1.1 * gl_Position.w, gl_Position.z, float(projected_pos.w > 0.0)); + v_tex = a_tex / u_texsize; vec2 fade_opacity = unpack_opacity(a_fade_opacity); float fade_change = fade_opacity[1] > 0.5 ? u_fade_change : -u_fade_change; diff --git a/src/shaders/symbol_sdf.vertex.glsl b/src/shaders/symbol_sdf.vertex.glsl index 71ccf3c81d7..36a46a6bde2 100644 --- a/src/shaders/symbol_sdf.vertex.glsl +++ b/src/shaders/symbol_sdf.vertex.glsl @@ -106,6 +106,10 @@ void main() { gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * fontScale + a_pxoffset), 0.0, 1.0); float gamma_scale = gl_Position.w; + // Symbols might end up being behind the camera. Modify z-value to be out of visible bounds + // if this is the case, otherwise ignore depth. -1.1 is safely out of the visible depth range [-1, 1] + gl_Position.z = mix(-1.1 * gl_Position.w, gl_Position.z, float(projected_pos.w > 0.0)); + vec2 fade_opacity = unpack_opacity(a_fade_opacity); float fade_change = fade_opacity[1] > 0.5 ? u_fade_change : -u_fade_change; float interpolated_fade_opacity = max(0.0, min(1.0, fade_opacity[0] + fade_change)); diff --git a/src/source/load_tilejson.js b/src/source/load_tilejson.js index 725c670eac0..2f96df7f794 100644 --- a/src/source/load_tilejson.js +++ b/src/source/load_tilejson.js @@ -26,10 +26,7 @@ export default function(options: any, requestManager: RequestManager, callback: result.vectorLayerIds = result.vectorLayers.map((layer) => { return layer.id; }); } - // only canonicalize tile tileset if source is declared using a tilejson url - if (options.url) { - result.tiles = requestManager.canonicalizeTileset(result, options.url); - } + result.tiles = requestManager.canonicalizeTileset(result, options.url); callback(null, result); } }; diff --git a/src/source/raster_dem_tile_source.js b/src/source/raster_dem_tile_source.js index b706dba08d4..bd36edfdda7 100644 --- a/src/source/raster_dem_tile_source.js +++ b/src/source/raster_dem_tile_source.js @@ -40,7 +40,7 @@ class RasterDEMTileSource extends RasterTileSource implements Source { } loadTile(tile: Tile, callback: Callback) { - const url = this.map._requestManager.normalizeTileURL(tile.tileID.canonical.url(this.tiles, this.scheme), this.url, this.tileSize); + const url = this.map._requestManager.normalizeTileURL(tile.tileID.canonical.url(this.tiles, this.scheme), this.tileSize); tile.request = getImage(this.map._requestManager.transformRequest(url, ResourceType.Tile), imageLoaded.bind(this)); tile.neighboringTiles = this._getNeighboringTiles(tile.tileID); diff --git a/src/source/raster_tile_source.js b/src/source/raster_tile_source.js index fa55b14722f..4aa5b71045c 100644 --- a/src/source/raster_tile_source.js +++ b/src/source/raster_tile_source.js @@ -110,7 +110,7 @@ class RasterTileSource extends Evented implements Source { } loadTile(tile: Tile, callback: Callback) { - const url = this.map._requestManager.normalizeTileURL(tile.tileID.canonical.url(this.tiles, this.scheme), this.url, this.tileSize); + const url = this.map._requestManager.normalizeTileURL(tile.tileID.canonical.url(this.tiles, this.scheme), this.tileSize); tile.request = getImage(this.map._requestManager.transformRequest(url, ResourceType.Tile), (err, img) => { delete tile.request; diff --git a/src/source/source_cache.js b/src/source/source_cache.js index 4b06ad0ba2d..5283cca6bf2 100644 --- a/src/source/source_cache.js +++ b/src/source/source_cache.js @@ -6,7 +6,7 @@ import Tile from './tile'; import {Event, ErrorEvent, Evented} from '../util/evented'; import TileCache from './tile_cache'; import MercatorCoordinate from '../geo/mercator_coordinate'; -import {keysDifference} from '../util/util'; +import {keysDifference, values} from '../util/util'; import EXTENT from '../data/extent'; import Context from '../gl/context'; import Point from '@mapbox/point-geometry'; @@ -182,7 +182,7 @@ class SourceCache extends Evented { * Return all tile ids ordered with z-order, and cast to numbers */ getIds(): Array { - return (Object.values(this._tiles): any).map((tile: Tile) => tile.tileID).sort(compareTileId).map(id => id.key); + return (values(this._tiles): any).map((tile: Tile) => tile.tileID).sort(compareTileId).map(id => id.key); } getRenderableIds(symbolLayer?: boolean): Array { diff --git a/src/source/vector_tile_source.js b/src/source/vector_tile_source.js index 1554b45bd1d..f6ac6164003 100644 --- a/src/source/vector_tile_source.js +++ b/src/source/vector_tile_source.js @@ -115,7 +115,7 @@ class VectorTileSource extends Evented implements Source { } loadTile(tile: Tile, callback: Callback) { - const url = this.map._requestManager.normalizeTileURL(tile.tileID.canonical.url(this.tiles, this.scheme), this.url, null); + const url = this.map._requestManager.normalizeTileURL(tile.tileID.canonical.url(this.tiles, this.scheme)); const params = { request: this.map._requestManager.transformRequest(url, ResourceType.Tile), uid: tile.uid, diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index c5d8ae0bbba..587838ef31e 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -177,6 +177,10 @@ "type": "string", "doc": "Contains an attribution to be displayed when the map is shown to a user." }, + "promoteId": { + "type": "promoteId", + "doc": "A property to use as a feature id (for feature state). Either a property name, or an object of the form `{: }`. If specified as a string for a vector tile source, the same property is used across all its source layers." + }, "*": { "type": "*", "doc": "Other keys to configure the data source." @@ -317,10 +321,6 @@ "default": "mapbox", "doc": "The encoding used by this source. Mapbox Terrain RGB is used by default" }, - "promoteId": { - "type": "promoteId", - "doc": "A property to use as a feature id (for feature state). Either a property name, or an object of the form `{: }`." - }, "*": { "type": "*", "doc": "Other keys to configure the data source." @@ -2821,7 +2821,9 @@ "group": "Types", "sdk-support": { "basic functionality": { - "js": "1.4.0" + "js": "1.4.0", + "android": "8.6.0", + "ios": "5.6.0" } } }, @@ -5786,5 +5788,11 @@ "type": "property-type", "doc": "Property is constant across all zoom levels and property values." } + }, + "promoteId": { + "*": { + "type": "string", + "doc": "A name of a feature property to use as ID for feature state." + } } } diff --git a/src/style-spec/types.js b/src/style-spec/types.js index bd74a4f62ba..ecfab1078b5 100644 --- a/src/style-spec/types.js +++ b/src/style-spec/types.js @@ -88,7 +88,8 @@ export type VectorSourceSpecification = { "scheme"?: "xyz" | "tms", "minzoom"?: number, "maxzoom"?: number, - "attribution"?: string + "attribution"?: string, + "promoteId"?: PromoteIdSpecification } export type RasterSourceSpecification = { @@ -112,8 +113,7 @@ export type RasterDEMSourceSpecification = { "maxzoom"?: number, "tileSize"?: number, "attribution"?: string, - "encoding"?: "terrarium" | "mapbox", - "promoteId"?: PromoteIdSpecification + "encoding"?: "terrarium" | "mapbox" } export type GeoJSONSourceSpecification = {| diff --git a/src/style/style.js b/src/style/style.js index bcd8d1c1ffb..f06f14981f1 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -250,20 +250,7 @@ class Style extends Evented { } if (json.sprite) { - this._spriteRequest = loadSprite(json.sprite, this.map._requestManager, (err, images) => { - this._spriteRequest = null; - if (err) { - this.fire(new ErrorEvent(err)); - } else if (images) { - for (const id in images) { - this.imageManager.addImage(id, images[id]); - } - } - - this.imageManager.setLoaded(true); - this.dispatcher.broadcast('setImages', this.imageManager.listImages()); - this.fire(new Event('data', {dataType: 'style'})); - }); + this._loadSprite(json.sprite); } else { this.imageManager.setLoaded(true); } @@ -288,6 +275,23 @@ class Style extends Evented { this.fire(new Event('style.load')); } + _loadSprite(url: string) { + this._spriteRequest = loadSprite(url, this.map._requestManager, (err, images) => { + this._spriteRequest = null; + if (err) { + this.fire(new ErrorEvent(err)); + } else if (images) { + for (const id in images) { + this.imageManager.addImage(id, images[id]); + } + } + + this.imageManager.setLoaded(true); + this.dispatcher.broadcast('setImages', this.imageManager.listImages()); + this.fire(new Event('data', {dataType: 'style'})); + }); + } + _validateLayer(layer: StyleLayer) { const sourceCache = this.sourceCaches[layer.source]; if (!sourceCache) { diff --git a/src/symbol/projection.js b/src/symbol/projection.js index 894e7ff345f..92d840f1b1c 100644 --- a/src/symbol/projection.js +++ b/src/symbol/projection.js @@ -185,7 +185,15 @@ function updateLineLabels(bucket: SymbolBucket, fontSize / perspectiveRatio; const tileAnchorPoint = new Point(symbol.anchorX, symbol.anchorY); - const anchorPoint = project(tileAnchorPoint, labelPlaneMatrix).point; + const transformedTileAnchor = project(tileAnchorPoint, labelPlaneMatrix); + + // Skip labels behind the camera + if (transformedTileAnchor.signedDistanceFromCamera <= 0.0) { + hideGlyphs(symbol.numGlyphs, dynamicLayoutVertexArray); + continue; + } + + const anchorPoint = transformedTileAnchor.point; const projectionCache = {}; const placeUnflipped: any = placeGlyphsAlongLine(symbol, pitchScaledFontSize, false /*unflipped*/, keepUpright, posMatrix, labelPlaneMatrix, glCoordMatrix, diff --git a/src/ui/control/geolocate_control.js b/src/ui/control/geolocate_control.js index d9b024753c2..274230a7b7b 100644 --- a/src/ui/control/geolocate_control.js +++ b/src/ui/control/geolocate_control.js @@ -15,6 +15,7 @@ type Options = { positionOptions?: PositionOptions, fitBoundsOptions?: AnimationOptions & CameraOptions, trackUserLocation?: boolean, + showAccuracyCircle?: boolean, showUserLocation?: boolean }; @@ -28,6 +29,7 @@ const defaultOptions: Options = { maxZoom: 15 }, trackUserLocation: false, + showAccuracyCircle: true, showUserLocation: true }; @@ -78,6 +80,7 @@ let noTimeout = false; * @param {Object} [options.positionOptions={enableHighAccuracy: false, timeout: 6000}] A Geolocation API [PositionOptions](https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions) object. * @param {Object} [options.fitBoundsOptions={maxZoom: 15}] A [`fitBounds`](#map#fitbounds) options object to use when the map is panned and zoomed to the user's location. The default is to use a `maxZoom` of 15 to limit how far the map will zoom in for very accurate locations. * @param {Object} [options.trackUserLocation=false] If `true` the Geolocate Control becomes a toggle button and when active the map will receive updates to the user's location as it changes. + * @param {Object} [options.showAccuracyCircle=true] By default a transparent circle will be drawn around the user location indicating the accuracy (95% confidence level) of the user's location. Set to `false` to disable. * @param {Object} [options.showUserLocation=true] By default a dot will be shown on the map at the user's location. Set to `false` to disable. * * @example @@ -94,12 +97,15 @@ class GeolocateControl extends Evented { options: Options; _container: HTMLElement; _dotElement: HTMLElement; + _circleElement: HTMLElement; _geolocateButton: HTMLButtonElement; _geolocationWatchID: number; _timeoutId: ?TimeoutID; _watchState: 'OFF' | 'ACTIVE_LOCK' | 'WAITING_ACTIVE' | 'ACTIVE_ERROR' | 'BACKGROUND' | 'BACKGROUND_ERROR'; _lastKnownPosition: any; _userLocationDotMarker: Marker; + _accuracyCircleMarker: Marker; + _accuracy: number; _setup: boolean; // set to true once the control has been setup constructor(options: Options) { @@ -109,6 +115,7 @@ class GeolocateControl extends Evented { bindAll([ '_onSuccess', '_onError', + '_onZoom', '_finish', '_setupUI', '_updateCamera', @@ -120,6 +127,7 @@ class GeolocateControl extends Evented { this._map = map; this._container = DOM.create('div', `mapboxgl-ctrl mapboxgl-ctrl-group`); checkGeolocationSupport(this._setupUI); + this._map.on('zoom', this._onZoom); return this._container; } @@ -130,12 +138,16 @@ class GeolocateControl extends Evented { this._geolocationWatchID = (undefined: any); } - // clear the marker from the map + // clear the markers from the map if (this.options.showUserLocation && this._userLocationDotMarker) { this._userLocationDotMarker.remove(); } + if (this.options.showAccuracyCircle && this._accuracyCircleMarker) { + this._accuracyCircleMarker.remove(); + } DOM.remove(this._container); + this._map.off('zoom', this._onZoom); this._map = (undefined: any); numberOfWatches = 0; noTimeout = false; @@ -251,9 +263,31 @@ class GeolocateControl extends Evented { _updateMarker(position: ?Position) { if (position) { - this._userLocationDotMarker.setLngLat([position.coords.longitude, position.coords.latitude]).addTo(this._map); + const center = new LngLat(position.coords.longitude, position.coords.latitude); + this._accuracyCircleMarker.setLngLat(center).addTo(this._map); + this._userLocationDotMarker.setLngLat(center).addTo(this._map); + this._accuracy = position.coords.accuracy; + this._onZoom(); } else { this._userLocationDotMarker.remove(); + this._accuracyCircleMarker.remove(); + } + } + + _updateCircleRadius() { + const y = this._map._container.clientHeight / 2; + const a = this._map.unproject([0, y]); + const b = this._map.unproject([1, y]); + const metersPerPixel = a.distanceTo(b); + const circleDiameter = Math.ceil(2.0 * this._accuracy / metersPerPixel); + this._circleElement.style.width = `${circleDiameter}px`; + this._circleElement.style.height = `${circleDiameter}px`; + } + + _onZoom() { + if (this.options.trackUserLocation && this.options.showAccuracyCircle) { + assert(this._circleElement); + this._updateCircleRadius(); } } @@ -326,9 +360,11 @@ class GeolocateControl extends Evented { // when showUserLocation is enabled, keep the Geolocate button disabled until the device location marker is setup on the map if (this.options.showUserLocation) { this._dotElement = DOM.create('div', 'mapboxgl-user-location-dot'); - this._userLocationDotMarker = new Marker(this._dotElement); + this._circleElement = DOM.create('div', 'mapboxgl-user-location-accuracy-circle'); + this._accuracyCircleMarker = new Marker({element: this._circleElement, pitchAlignment: 'map'}); + if (this.options.trackUserLocation) this._watchState = 'OFF'; } diff --git a/src/ui/control/scale_control.js b/src/ui/control/scale_control.js index f57c919ccb9..7caa8ea8dc1 100644 --- a/src/ui/control/scale_control.js +++ b/src/ui/control/scale_control.js @@ -92,7 +92,9 @@ function updateScale(map, container, options) { const maxWidth = options && options.maxWidth || 100; const y = map._container.clientHeight / 2; - const maxMeters = getDistance(map.unproject([0, y]), map.unproject([maxWidth, y])); + const left = map.unproject([0, y]); + const right = map.unproject([maxWidth, y]); + const maxMeters = left.distanceTo(right); // The real distance corresponding to 100px scale length is rounded off to // near pretty number and the scale length for the same is found out. // Default unit of the scale is based on User's locale. @@ -121,21 +123,6 @@ function setScale(container, maxWidth, maxDistance, unit) { container.innerHTML = distance + unit; } -function getDistance(latlng1, latlng2) { - // Uses spherical law of cosines approximation. - const R = 6371000; - - const rad = Math.PI / 180, - lat1 = latlng1.lat * rad, - lat2 = latlng2.lat * rad, - a = Math.sin(lat1) * Math.sin(lat2) + - Math.cos(lat1) * Math.cos(lat2) * Math.cos((latlng2.lng - latlng1.lng) * rad); - - const maxMeters = R * Math.acos(Math.min(a, 1)); - return maxMeters; - -} - function getDecimalRoundNum(d) { const multiplier = Math.pow(10, Math.ceil(-Math.log(d) / Math.LN10)); return Math.round(d * multiplier) / multiplier; diff --git a/src/util/mapbox.js b/src/util/mapbox.js index 59e6c53734b..f4197a5d817 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -101,12 +101,12 @@ export class RequestManager { return this._makeAPIURL(urlObject, this._customAccessToken || accessToken); } - normalizeTileURL(tileURL: string, sourceURL?: ?string, tileSize?: ?number): string { + normalizeTileURL(tileURL: string, tileSize?: ?number): string { if (this._isSkuTokenExpired()) { this._createSkuToken(); } - if (!sourceURL || !isMapboxURL(sourceURL)) return tileURL; + if (tileURL && !isMapboxURL(tileURL) && !isMapboxHTTPURL(tileURL)) return tileURL; const urlObject = parseUrl(tileURL); const imageExtensionRe = /(\.(png|jpg)\d*)(?=$)/; @@ -121,14 +121,15 @@ export class RequestManager { urlObject.path = urlObject.path.replace(tileURLAPIPrefixRe, '/'); urlObject.path = `/v4${urlObject.path}`; - if (config.REQUIRE_ACCESS_TOKEN && (config.ACCESS_TOKEN || this._customAccessToken) && this._skuToken) { + const accessToken = this._customAccessToken || getAccessToken(urlObject.params) || config.ACCESS_TOKEN; + if (config.REQUIRE_ACCESS_TOKEN && accessToken && this._skuToken) { urlObject.params.push(`sku=${this._skuToken}`); } - return this._makeAPIURL(urlObject, this._customAccessToken); + return this._makeAPIURL(urlObject, accessToken); } - canonicalizeTileURL(url: string) { + canonicalizeTileURL(url: string, removeAccessToken: boolean) { const version = "/v4/"; // matches any file extension specified by a dot and one or more alphanumeric characters const extensionRe = /\.[\w]+$/; @@ -145,17 +146,23 @@ export class RequestManager { result += urlObject.path.replace(version, ''); // Append the query string, minus the access token parameter. - const params = urlObject.params.filter(p => !p.match(/^access_token=/)); + let params = urlObject.params; + if (removeAccessToken) { + params = params.filter(p => !p.match(/^access_token=/)); + } if (params.length) result += `?${params.join('&')}`; return result; } - canonicalizeTileset(tileJSON: TileJSON, sourceURL: string) { - if (!isMapboxURL(sourceURL)) return tileJSON.tiles || []; + canonicalizeTileset(tileJSON: TileJSON, sourceURL?: string) { + const removeAccessToken = sourceURL ? isMapboxURL(sourceURL) : false; const canonical = []; - for (const url of tileJSON.tiles) { - const canonicalUrl = this.canonicalizeTileURL(url); - canonical.push(canonicalUrl); + for (const url of tileJSON.tiles || []) { + if (isMapboxHTTPURL(url)) { + canonical.push(this.canonicalizeTileURL(url, removeAccessToken)); + } else { + canonical.push(url); + } } return canonical; } @@ -197,6 +204,16 @@ function hasCacheDefeatingSku(url: string) { return url.indexOf('sku=') > 0 && isMapboxHTTPURL(url); } +function getAccessToken(params: Array): string | null { + for (const param of params) { + const match = param.match(/^access_token=(.*)$/); + if (match) { + return match[1]; + } + } + return null; +} + const urlRe = /^(\w+):\/\/([^/?]*)(\/[^?]+)?\??(.+)?/; function parseUrl(url: string): UrlObject { diff --git a/src/util/primitives.js b/src/util/primitives.js new file mode 100644 index 00000000000..b1cd69e6fe3 --- /dev/null +++ b/src/util/primitives.js @@ -0,0 +1,145 @@ +// @flow + +import {vec3, vec4} from 'gl-matrix'; +import assert from 'assert'; + +class Frustum { + points: Array>; + planes: Array>; + + constructor(points_: Array>, planes_: Array>) { + this.points = points_; + this.planes = planes_; + } + + static fromInvProjectionMatrix(invProj: Float64Array, worldSize: number, zoom: number): Frustum { + const clipSpaceCorners = [ + [-1, 1, -1, 1], + [ 1, 1, -1, 1], + [ 1, -1, -1, 1], + [-1, -1, -1, 1], + [-1, 1, 1, 1], + [ 1, 1, 1, 1], + [ 1, -1, 1, 1], + [-1, -1, 1, 1] + ]; + + const scale = Math.pow(2, zoom); + + // Transform frustum corner points from clip space to tile space + const frustumCoords = clipSpaceCorners + .map(v => vec4.transformMat4([], v, invProj)) + .map(v => vec4.scale([], v, 1.0 / v[3] / worldSize * scale)); + + const frustumPlanePointIndices = [ + [0, 1, 2], // near + [6, 5, 4], // far + [0, 3, 7], // left + [2, 1, 5], // right + [3, 2, 6], // bottom + [0, 4, 5] // top + ]; + + const frustumPlanes = frustumPlanePointIndices.map((p: Array) => { + const a = vec3.sub([], frustumCoords[p[0]], frustumCoords[p[1]]); + const b = vec3.sub([], frustumCoords[p[2]], frustumCoords[p[1]]); + const n = vec3.normalize([], vec3.cross([], a, b)); + const d = -vec3.dot(n, frustumCoords[p[1]]); + return n.concat(d); + }); + + return new Frustum(frustumCoords, frustumPlanes); + } +} + +class Aabb { + min: vec3; + max: vec3; + center: vec3; + + constructor(min_: vec3, max_: vec3) { + this.min = min_; + this.max = max_; + this.center = vec3.scale([], vec3.add([], this.min, this.max), 0.5); + } + + quadrant(index: number): Aabb { + const split = [(index % 2) === 0, index < 2]; + const qMin = vec3.clone(this.min); + const qMax = vec3.clone(this.max); + for (let axis = 0; axis < split.length; axis++) { + qMin[axis] = split[axis] ? this.min[axis] : this.center[axis]; + qMax[axis] = split[axis] ? this.center[axis] : this.max[axis]; + } + // Elevation is always constant, hence quadrant.max.z = this.max.z + qMax[2] = this.max[2]; + return new Aabb(qMin, qMax); + } + + distanceX(point: Array): number { + const pointOnAabb = Math.max(Math.min(this.max[0], point[0]), this.min[0]); + return pointOnAabb - point[0]; + } + + distanceY(point: Array): number { + const pointOnAabb = Math.max(Math.min(this.max[1], point[1]), this.min[1]); + return pointOnAabb - point[1]; + } + + // Performs a frustum-aabb intersection test. Returns 0 if there's no intersection, + // 1 if shapes are intersecting and 2 if the aabb if fully inside the frustum. + intersects(frustum: Frustum): number { + // Execute separating axis test between two convex objects to find intersections + // Each frustum plane together with 3 major axes define the separating axes + // Note: test only 4 points as both min and max points have equal elevation + assert(this.min[2] === 0 && this.max[2] === 0); + + const aabbPoints = [ + [this.min[0], this.min[1], 0.0, 1], + [this.max[0], this.min[1], 0.0, 1], + [this.max[0], this.max[1], 0.0, 1], + [this.min[0], this.max[1], 0.0, 1] + ]; + + let fullyInside = true; + + for (let p = 0; p < frustum.planes.length; p++) { + const plane = frustum.planes[p]; + let pointsInside = 0; + + for (let i = 0; i < aabbPoints.length; i++) { + pointsInside += vec4.dot(plane, aabbPoints[i]) >= 0; + } + + if (pointsInside === 0) + return 0; + + if (pointsInside !== aabbPoints.length) + fullyInside = false; + } + + if (fullyInside) + return 2; + + for (let axis = 0; axis < 3; axis++) { + let projMin = Number.MAX_VALUE; + let projMax = -Number.MAX_VALUE; + + for (let p = 0; p < frustum.points.length; p++) { + const projectedPoint = frustum.points[p][axis] - this.min[axis]; + + projMin = Math.min(projMin, projectedPoint); + projMax = Math.max(projMax, projectedPoint); + } + + if (projMax < 0 || projMin > this.max[axis] - this.min[axis]) + return 0; + } + + return 1; + } +} +export { + Aabb, + Frustum +}; diff --git a/src/util/tile_cover.js b/src/util/tile_cover.js deleted file mode 100644 index 43fe4e03a70..00000000000 --- a/src/util/tile_cover.js +++ /dev/null @@ -1,100 +0,0 @@ -// @flow - -import MercatorCoordinate from '../geo/mercator_coordinate'; -import Point from '@mapbox/point-geometry'; - -import {OverscaledTileID} from '../source/tile_id'; - -export default tileCover; - -function tileCover(z: number, bounds: [MercatorCoordinate, MercatorCoordinate, MercatorCoordinate, MercatorCoordinate], - actualZ: number, renderWorldCopies: boolean | void): Array { - if (renderWorldCopies === undefined) { - renderWorldCopies = true; - } - const tiles = 1 << z; - const t = {}; - - function scanLine(x0, x1, y) { - let x, w, wx, coord; - if (y >= 0 && y <= tiles) { - for (x = x0; x < x1; x++) { - w = Math.floor(x / tiles); - wx = (x % tiles + tiles) % tiles; - if (w === 0 || renderWorldCopies === true) { - coord = new OverscaledTileID(actualZ, w, z, wx, y); - t[coord.key] = coord; - } - } - } - } - - const zoomedBounds = bounds.map((coord) => new Point(coord.x, coord.y)._mult(tiles)); - - // Divide the screen up in two triangles and scan each of them: - // +---/ - // | / | - // /---+ - scanTriangle(zoomedBounds[0], zoomedBounds[1], zoomedBounds[2], 0, tiles, scanLine); - scanTriangle(zoomedBounds[2], zoomedBounds[3], zoomedBounds[0], 0, tiles, scanLine); - - return Object.keys(t).map((id) => { - return t[id]; - }); -} - -// Taken from polymaps src/Layer.js -// https://github.com/simplegeo/polymaps/blob/master/src/Layer.js#L333-L383 - -function edge(a: Point, b: Point) { - if (a.y > b.y) { const t = a; a = b; b = t; } - return { - x0: a.x, - y0: a.y, - x1: b.x, - y1: b.y, - dx: b.x - a.x, - dy: b.y - a.y - }; -} - -function scanSpans(e0, e1, ymin, ymax, scanLine) { - const y0 = Math.max(ymin, Math.floor(e1.y0)); - const y1 = Math.min(ymax, Math.ceil(e1.y1)); - - // sort edges by x-coordinate - if ((e0.x0 === e1.x0 && e0.y0 === e1.y0) ? - (e0.x0 + e1.dy / e0.dy * e0.dx < e1.x1) : - (e0.x1 - e1.dy / e0.dy * e0.dx < e1.x0)) { - const t = e0; e0 = e1; e1 = t; - } - - // scan lines! - const m0 = e0.dx / e0.dy; - const m1 = e1.dx / e1.dy; - const d0 = e0.dx > 0; // use y + 1 to compute x0 - const d1 = e1.dx < 0; // use y + 1 to compute x1 - for (let y = y0; y < y1; y++) { - const x0 = m0 * Math.max(0, Math.min(e0.dy, y + d0 - e0.y0)) + e0.x0; - const x1 = m1 * Math.max(0, Math.min(e1.dy, y + d1 - e1.y0)) + e1.x0; - scanLine(Math.floor(x1), Math.ceil(x0), y); - } -} - -function scanTriangle(a: Point, b: Point, c: Point, ymin, ymax, scanLine) { - let ab = edge(a, b), - bc = edge(b, c), - ca = edge(c, a); - - let t; - - // sort edges by y-length - if (ab.dy > bc.dy) { t = ab; ab = bc; bc = t; } - if (ab.dy > ca.dy) { t = ab; ab = ca; ca = t; } - if (bc.dy > ca.dy) { t = bc; bc = ca; ca = t; } - - // scan span! scan span! - if (ab.dy) scanSpans(ca, ab, ymin, ymax, scanLine); - if (bc.dy) scanSpans(ca, bc, ymin, ymax, scanLine); -} - diff --git a/test/ignores.json b/test/ignores.json index 6846ae63ab1..6358e1aa291 100644 --- a/test/ignores.json +++ b/test/ignores.json @@ -17,5 +17,6 @@ "render-tests/icon-image/icon-sdf-non-sdf-one-layer": "skip - render sdf icon and normal icon in one layer", "render-tests/text-variable-anchor/all-anchors-tile-map-mode": "skip - mapbox-gl-js does not need to render tiles", "render-tests/fill-pattern/update-feature-state": "https://github.com/mapbox/mapbox-gl-js/issues/7207", - "render-tests/text-size/zero": "https://github.com/mapbox/mapbox-gl-js/issues/9161" + "render-tests/text-size/zero": "https://github.com/mapbox/mapbox-gl-js/issues/9161", + "render-tests/text-variable-anchor/left-top-right-buttom-offset-tile-map-mode": "skip - mapbox-gl-js does not need to render tiles" } diff --git a/test/integration/render-tests/extent/1024-symbol/expected.png b/test/integration/render-tests/extent/1024-symbol/expected.png index f8386467f42..851526dda65 100644 Binary files a/test/integration/render-tests/extent/1024-symbol/expected.png and b/test/integration/render-tests/extent/1024-symbol/expected.png differ diff --git a/test/integration/render-tests/feature-state/promote-id-circle/expected.png b/test/integration/render-tests/feature-state/promote-id-circle/expected.png new file mode 100644 index 00000000000..1587818fee0 Binary files /dev/null and b/test/integration/render-tests/feature-state/promote-id-circle/expected.png differ diff --git a/test/integration/render-tests/feature-state/promote-id/style.json b/test/integration/render-tests/feature-state/promote-id-circle/style.json similarity index 100% rename from test/integration/render-tests/feature-state/promote-id/style.json rename to test/integration/render-tests/feature-state/promote-id-circle/style.json diff --git a/test/integration/render-tests/feature-state/promote-id-fill-extrusion/expected.png b/test/integration/render-tests/feature-state/promote-id-fill-extrusion/expected.png new file mode 100644 index 00000000000..e702d4b422f Binary files /dev/null and b/test/integration/render-tests/feature-state/promote-id-fill-extrusion/expected.png differ diff --git a/test/integration/render-tests/feature-state/promote-id-fill-extrusion/style.json b/test/integration/render-tests/feature-state/promote-id-fill-extrusion/style.json new file mode 100644 index 00000000000..a4a31d62dfa --- /dev/null +++ b/test/integration/render-tests/feature-state/promote-id-fill-extrusion/style.json @@ -0,0 +1,161 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 128, + "width": 512, + "operations": [ + [ + "setFeatureState", + { + "source": "geojson", + "id": "9007199254740992" + }, + { + "color": "red" + } + ], + [ + "setFeatureState", + { + "source": "geojson", + "id": "1.2" + }, + { + "color": "blue" + } + ], + [ + "wait" + ] + ] + } + }, + "sources": { + "geojson": { + "type": "geojson", + "promoteId": "foo", + "data": { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0005, + -0.0001 + ], + [ + -0.0005, + 0.0001 + ], + [ + -0.0003, + 0.0001 + ], + [ + -0.0003, + -0.0001 + ], + [ + -0.0005, + -0.0001 + ] + ] + ] + }, + "properties": { + "foo": "9007199254740992" + } + }, { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -0.0001, + -0.0001 + ], + [ + -0.0001, + 0.0001 + ], + [ + 0.0001, + 0.0001 + ], + [ + 0.0001, + -0.0001 + ], + [ + -0.0001, + -0.0001 + ] + ] + ] + }, + "properties": { + "foo": "9007199254740993" + } + }, { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 0.0003, + -0.0001 + ], + [ + 0.0003, + 0.0001 + ], + [ + 0.0005, + 0.0001 + ], + [ + 0.0005, + -0.0001 + ], + [ + 0.0003, + -0.0001 + ] + ] + ] + }, + "properties": { + "foo": "1.2" + } + }] + } + } + }, + "pitch": 60, + "zoom": 18, + "layers": [ + { + "id": "fill-extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 10, + "fill-extrusion-base": 0, + "fill-extrusion-color": [ + "coalesce", + [ + "feature-state", + "color" + ], + "black" + ] + } + } + ] +} diff --git a/test/integration/render-tests/feature-state/promote-id-fill/expected.png b/test/integration/render-tests/feature-state/promote-id-fill/expected.png new file mode 100644 index 00000000000..aa7e7f3f27b Binary files /dev/null and b/test/integration/render-tests/feature-state/promote-id-fill/expected.png differ diff --git a/test/integration/render-tests/feature-state/promote-id-fill/style.json b/test/integration/render-tests/feature-state/promote-id-fill/style.json new file mode 100644 index 00000000000..31514683e4d --- /dev/null +++ b/test/integration/render-tests/feature-state/promote-id-fill/style.json @@ -0,0 +1,114 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 32, + "operations": [ + [ + "setFeatureState", + { + "source": "geojson", + "id": "9007199254740992" + }, + { + "color": "red" + } + ], + [ + "setFeatureState", + { + "source": "geojson", + "id": "1.2" + }, + { + "color": "blue" + } + ], + [ + "wait" + ] + ] + } + }, + "zoom": 2, + "sources": { + "geojson": { + "type": "geojson", + "promoteId": "foo", + "data": { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ -5, -1 ], + [ -5, 1 ], + [ -3, 1 ], + [ -3, -1 ], + [ -5, -1 ] + ] + ] + }, + "properties": { + "foo": "9007199254740992" + } + }, { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ -1, -1 ], + [ -1, 1 ], + [ 1, 1 ], + [ 1, -1 ], + [ -0.5, -1 ] + ] + ] + }, + "properties": { + "foo": "9007199254740993" + } + }, { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ 3, -1 ], + [ 3, 1 ], + [ 5, 1 ], + [ 5, -1 ], + [ 3, -1 ] + ] + ] + }, + "properties": { + "foo": "1.2" + } + }] + } + } + }, + "layers": [ + { + "id": "fill", + "type": "fill", + "source": "geojson", + "paint": { + "fill-antialias": false, + "fill-color": [ + "coalesce", + [ + "feature-state", + "color" + ], + "black" + ] + } + } + ] +} diff --git a/test/integration/render-tests/feature-state/promote-id-line/expected.png b/test/integration/render-tests/feature-state/promote-id-line/expected.png new file mode 100644 index 00000000000..55bfc9fa640 Binary files /dev/null and b/test/integration/render-tests/feature-state/promote-id-line/expected.png differ diff --git a/test/integration/render-tests/feature-state/promote-id-line/style.json b/test/integration/render-tests/feature-state/promote-id-line/style.json new file mode 100644 index 00000000000..b2ec7836cde --- /dev/null +++ b/test/integration/render-tests/feature-state/promote-id-line/style.json @@ -0,0 +1,116 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 32, + "operations": [ + [ + "setFeatureState", + { + "source": "geojson", + "id": "9007199254740992" + }, + { + "color": "red" + } + ], + [ + "setFeatureState", + { + "source": "geojson", + "id": "1.2" + }, + { + "color": "blue" + } + ], + [ + "wait" + ] + ] + } + }, + "sources": { + "geojson": { + "type": "geojson", + "promoteId": "foo", + "data": { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -10, + -10 + ], + [ + -10, + 10 + ] + ] + }, + "properties": { + "foo": "9007199254740992" + } + }, { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 0, + -10 + ], + [ + 0, + 10 + ] + ] + }, + "properties": { + "foo": "9007199254740993" + } + }, { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 10, + -10 + ], + [ + 10, + 10 + ] + ] + }, + "properties": { + "foo": "1.2" + } + }] + } + } + }, + "layers": [ + { + "id": "line", + "type": "line", + "source": "geojson", + "paint": { + "line-width": 5, + "line-color": [ + "coalesce", + [ + "feature-state", + "color" + ], + "black" + ] + } + } + ] +} diff --git a/test/integration/render-tests/feature-state/promote-id-symbol/expected.png b/test/integration/render-tests/feature-state/promote-id-symbol/expected.png new file mode 100644 index 00000000000..3c3d132c91a Binary files /dev/null and b/test/integration/render-tests/feature-state/promote-id-symbol/expected.png differ diff --git a/test/integration/render-tests/feature-state/promote-id-symbol/style.json b/test/integration/render-tests/feature-state/promote-id-symbol/style.json new file mode 100644 index 00000000000..374dc13e84e --- /dev/null +++ b/test/integration/render-tests/feature-state/promote-id-symbol/style.json @@ -0,0 +1,103 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 32, + "operations": [ + [ + "setFeatureState", + { + "source": "geojson", + "id": "9007199254740992" + }, + { + "color": "red" + } + ], + [ + "setFeatureState", + { + "source": "geojson", + "id": "1.2" + }, + { + "color": "blue" + } + ], + [ + "wait" + ] + ] + } + }, + "zoom": 2, + "sprite": "local://sprites/sprite", + "sources": { + "geojson": { + "type": "geojson", + "promoteId": "foo", + "data": { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 0 + ] + }, + "properties": { + "foo": "9007199254740992" + } + }, { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 3, + 0 + ] + }, + "properties": { + "foo": "9007199254740993" + } + }, { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -3, + 0 + ] + }, + "properties": { + "foo": "1.2" + } + }] + } + } + }, + "layers": [ + { + "id": "symbol", + "type": "symbol", + "source": "geojson", + "layout": { + "icon-image": "dot.sdf", + "icon-allow-overlap": true + }, + "paint": { + "icon-color": [ + "coalesce", + [ + "feature-state", + "color" + ], + "black" + ] + } + } + ] +} diff --git a/test/integration/render-tests/feature-state/promote-id/expected.png b/test/integration/render-tests/feature-state/promote-id/expected.png deleted file mode 100644 index 7e58decb017..00000000000 Binary files a/test/integration/render-tests/feature-state/promote-id/expected.png and /dev/null differ diff --git a/test/integration/render-tests/fill-extrusion-base/property-function/expected.png b/test/integration/render-tests/fill-extrusion-base/property-function/expected.png index 65da9bc3c45..8c0a222d01d 100644 Binary files a/test/integration/render-tests/fill-extrusion-base/property-function/expected.png and b/test/integration/render-tests/fill-extrusion-base/property-function/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-base/zoom-and-property-function/expected.png b/test/integration/render-tests/fill-extrusion-base/zoom-and-property-function/expected.png index 441dc0f0173..e215a543852 100644 Binary files a/test/integration/render-tests/fill-extrusion-base/zoom-and-property-function/expected.png and b/test/integration/render-tests/fill-extrusion-base/zoom-and-property-function/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-multiple/multiple/expected.png b/test/integration/render-tests/fill-extrusion-multiple/multiple/expected.png index 42faa2bc3fd..462388936d2 100644 Binary files a/test/integration/render-tests/fill-extrusion-multiple/multiple/expected.png and b/test/integration/render-tests/fill-extrusion-multiple/multiple/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-opacity/default/expected.png b/test/integration/render-tests/fill-extrusion-opacity/default/expected.png index 06fac49b6c4..f3e842403e1 100644 Binary files a/test/integration/render-tests/fill-extrusion-opacity/default/expected.png and b/test/integration/render-tests/fill-extrusion-opacity/default/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-opacity/function/expected.png b/test/integration/render-tests/fill-extrusion-opacity/function/expected.png index 98c4fa5563c..92d92fc0a4f 100644 Binary files a/test/integration/render-tests/fill-extrusion-opacity/function/expected.png and b/test/integration/render-tests/fill-extrusion-opacity/function/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-opacity/literal/expected.png b/test/integration/render-tests/fill-extrusion-opacity/literal/expected.png index 1b853944e23..4f37e45d541 100644 Binary files a/test/integration/render-tests/fill-extrusion-opacity/literal/expected.png and b/test/integration/render-tests/fill-extrusion-opacity/literal/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-pattern/feature-expression/expected.png b/test/integration/render-tests/fill-extrusion-pattern/feature-expression/expected.png index d7573106f76..e8051b28c0e 100644 Binary files a/test/integration/render-tests/fill-extrusion-pattern/feature-expression/expected.png and b/test/integration/render-tests/fill-extrusion-pattern/feature-expression/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-pattern/function/expected.png b/test/integration/render-tests/fill-extrusion-pattern/function/expected.png index e60cd56c3a6..4d75055649a 100644 Binary files a/test/integration/render-tests/fill-extrusion-pattern/function/expected.png and b/test/integration/render-tests/fill-extrusion-pattern/function/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-pattern/literal/expected.png b/test/integration/render-tests/fill-extrusion-pattern/literal/expected.png index 8f4c7eb60d7..4d75055649a 100644 Binary files a/test/integration/render-tests/fill-extrusion-pattern/literal/expected.png and b/test/integration/render-tests/fill-extrusion-pattern/literal/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-pattern/opacity/expected.png b/test/integration/render-tests/fill-extrusion-pattern/opacity/expected.png index 31f8fb76e34..7b38bfdfbb6 100644 Binary files a/test/integration/render-tests/fill-extrusion-pattern/opacity/expected.png and b/test/integration/render-tests/fill-extrusion-pattern/opacity/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-translate/default/expected.png b/test/integration/render-tests/fill-extrusion-translate/default/expected.png index 6b2ddaabc09..32c7fcc0b2a 100644 Binary files a/test/integration/render-tests/fill-extrusion-translate/default/expected.png and b/test/integration/render-tests/fill-extrusion-translate/default/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-vertical-gradient/default/expected.png b/test/integration/render-tests/fill-extrusion-vertical-gradient/default/expected.png index ce1c4f52c99..051c62a6002 100644 Binary files a/test/integration/render-tests/fill-extrusion-vertical-gradient/default/expected.png and b/test/integration/render-tests/fill-extrusion-vertical-gradient/default/expected.png differ diff --git a/test/integration/render-tests/fill-extrusion-vertical-gradient/false/expected.png b/test/integration/render-tests/fill-extrusion-vertical-gradient/false/expected.png index 1ce43e1d89c..48ddf5ab016 100644 Binary files a/test/integration/render-tests/fill-extrusion-vertical-gradient/false/expected.png and b/test/integration/render-tests/fill-extrusion-vertical-gradient/false/expected.png differ diff --git a/test/integration/render-tests/line-gradient/gradient-tile-boundaries/expected.png b/test/integration/render-tests/line-gradient/gradient-tile-boundaries/expected.png new file mode 100644 index 00000000000..e85d8986b31 Binary files /dev/null and b/test/integration/render-tests/line-gradient/gradient-tile-boundaries/expected.png differ diff --git a/test/integration/render-tests/line-gradient/gradient-tile-boundaries/style.json b/test/integration/render-tests/line-gradient/gradient-tile-boundaries/style.json new file mode 100644 index 00000000000..106da0da967 --- /dev/null +++ b/test/integration/render-tests/line-gradient/gradient-tile-boundaries/style.json @@ -0,0 +1,56 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 128 + } + }, + "center": [ + -77.02803308586635, + 38.891047607560125 + ], + "zoom": 18, + "sources": { + "gradient": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-77.028035, 38.890600 ], + [-77.028035, 38.891088 ] + ] + } + }, + "lineMetrics": true + } + }, + "layers": [ + { + "id": "line", + "type": "line", + "source": "gradient", + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": 15, + "line-gradient": [ + "interpolate", + ["linear"], + ["line-progress"], + 0, "rgba(0, 0, 255, 0)", + 0.1, "royalblue", + 0.3, "cyan", + 0.5, "lime", + 0.7, "yellow", + 1, "red" + ] + } + } + ] +} diff --git a/test/integration/render-tests/line-pattern/with-dasharray/expected.png b/test/integration/render-tests/line-pattern/with-dasharray/expected.png new file mode 100644 index 00000000000..c93c29a5cca Binary files /dev/null and b/test/integration/render-tests/line-pattern/with-dasharray/expected.png differ diff --git a/test/integration/render-tests/line-pattern/with-dasharray/style.json b/test/integration/render-tests/line-pattern/with-dasharray/style.json new file mode 100644 index 00000000000..5e7a6cfbcab --- /dev/null +++ b/test/integration/render-tests/line-pattern/with-dasharray/style.json @@ -0,0 +1,92 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "sources": { + "a": { + "type": "geojson", + "data": { + "type": "LineString", + "coordinates": [ + [ + -10, + -10 + ], + [ + 10, + -10 + ] + ] + } + }, + "b": { + "type": "geojson", + "data": { + "type": "LineString", + "coordinates": [ + [ + -10, + 0 + ], + [ + 10, + 0 + ] + ] + } + }, + "c": { + "type": "geojson", + "data": { + "type": "LineString", + "coordinates": [ + [ + -10, + 10 + ], + [ + 10, + 10 + ] + ] + } + } + }, + "sprite": "local://sprites/emerald", + "layers": [ + { + "id": "a", + "type": "line", + "source": "a", + "paint": { + "line-width": 2, + "line-dasharray": [2, 4], + "line-pattern": "generic_icon" + } + }, + { + "id": "b", + "type": "line", + "source": "b", + "paint": { + "line-width": 4, + "line-dasharray": [2, 4], + "line-pattern": "generic_icon" + } + }, + { + "id": "c", + "type": "line", + "source": "c", + "paint": { + "line-width": 8, + "line-dasharray": [2, 4], + "line-pattern": "generic_icon" + } + } + ] +} diff --git a/test/integration/render-tests/projection/perspective/expected.png b/test/integration/render-tests/projection/perspective/expected.png index 0e7750741a4..9f38cb37ba5 100644 Binary files a/test/integration/render-tests/projection/perspective/expected.png and b/test/integration/render-tests/projection/perspective/expected.png differ diff --git a/test/integration/render-tests/regressions/mapbox-gl-js#5171/expected.png b/test/integration/render-tests/regressions/mapbox-gl-js#5171/expected.png new file mode 100644 index 00000000000..8784429cebc Binary files /dev/null and b/test/integration/render-tests/regressions/mapbox-gl-js#5171/expected.png differ diff --git a/test/integration/render-tests/regressions/mapbox-gl-js#5171/style.json b/test/integration/render-tests/regressions/mapbox-gl-js#5171/style.json new file mode 100644 index 00000000000..bbcbe066ed0 --- /dev/null +++ b/test/integration/render-tests/regressions/mapbox-gl-js#5171/style.json @@ -0,0 +1,35 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 32 + } + }, + "center": [-0.63938, 44.77458], + "zoom": 18, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "LineString", + "coordinates": [ + [-0.639208, 44.774651], + [-0.639605, 44.774487], + [-0.639605, 44.774487] + ] + } + } + }, + "layers": [ + { + "id": "line", + "type": "line", + "source": "geojson", + "paint": { + "line-color": "#f44336", + "line-width": 10 + } + } + ] +} diff --git a/test/integration/render-tests/text-variable-anchor/left-top-right-buttom-offset-tile-map-mode/expected.png b/test/integration/render-tests/text-variable-anchor/left-top-right-buttom-offset-tile-map-mode/expected.png new file mode 100644 index 00000000000..2dad19da2ab Binary files /dev/null and b/test/integration/render-tests/text-variable-anchor/left-top-right-buttom-offset-tile-map-mode/expected.png differ diff --git a/test/integration/render-tests/text-variable-anchor/left-top-right-buttom-offset-tile-map-mode/style.json b/test/integration/render-tests/text-variable-anchor/left-top-right-buttom-offset-tile-map-mode/style.json new file mode 100644 index 00000000000..4c262016342 --- /dev/null +++ b/test/integration/render-tests/text-variable-anchor/left-top-right-buttom-offset-tile-map-mode/style.json @@ -0,0 +1,61 @@ +{ + "version": 8, + "metadata": { + "test": { + "debug": true, + "mapMode": "tile", + "height": 256 + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 14, + "sources": { + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "top", + "type": "symbol", + "source": "mapbox", + "source-layer": "poi_label", + "filter": [ + "==", + "maki", + "restaurant" + ], + "layout": { + "text-field": "{name}", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "icon-image": "bank-12", + "text-justify": "auto", + "text-radial-offset": 0.7, + "text-variable-anchor": [ + "left", + "top", + "right", + "bottom" + ] + } + }] +} diff --git a/test/integration/render-tests/zoomed-fill/negative-zoom/expected.png b/test/integration/render-tests/zoomed-fill/negative-zoom/expected.png index ffe61831b03..93fd1c7b186 100644 Binary files a/test/integration/render-tests/zoomed-fill/negative-zoom/expected.png and b/test/integration/render-tests/zoomed-fill/negative-zoom/expected.png differ diff --git a/test/integration/render-tests/zoomed-fill/negative-zoom/style.json b/test/integration/render-tests/zoomed-fill/negative-zoom/style.json index 0cc394a3ef5..e197e225241 100644 --- a/test/integration/render-tests/zoomed-fill/negative-zoom/style.json +++ b/test/integration/render-tests/zoomed-fill/negative-zoom/style.json @@ -2,6 +2,7 @@ "version": 8, "metadata": { "test": { + "allowed": 0.0002, "width": 256, "height": 256 } diff --git a/test/unit/geo/lng_lat.test.js b/test/unit/geo/lng_lat.test.js index 469b4eb12c2..07a11e327e3 100644 --- a/test/unit/geo/lng_lat.test.js +++ b/test/unit/geo/lng_lat.test.js @@ -56,6 +56,33 @@ test('LngLat', (t) => { t.end(); }); + t.test('#distanceTo', (t) => { + const newYork = new LngLat(-74.0060, 40.7128); + const losAngeles = new LngLat(-118.2437, 34.0522); + const d = newYork.distanceTo(losAngeles); // 3935751.690893987, "true distance" is 3966km + t.ok(d > 3935750, "New York should be more than 3935750m from Los Angeles"); + t.ok(d < 3935752, "New York should be less than 3935752m from Los Angeles"); + t.end(); + }); + + t.test('#distanceTo to pole', (t) => { + const newYork = new LngLat(-74.0060, 40.7128); + const northPole = new LngLat(-135, 90); + const d = newYork.distanceTo(northPole); // 5480494.158486183 , "true distance" is 5499km + t.ok(d > 5480493, "New York should be more than 5480493m from the North Pole"); + t.ok(d < 5480495, "New York should be less than 5480495m from the North Pole"); + t.end(); + }); + + t.test('#distanceTo to Null Island', (t) => { + const newYork = new LngLat(-74.0060, 40.7128); + const nullIsland = new LngLat(0, 0); + const d = newYork.distanceTo(nullIsland); // 8667080.125666846 , "true distance" is 8661km + t.ok(d > 8667079, "New York should be more than 8667079m from Null Island"); + t.ok(d < 8667081, "New York should be less than 8667081m from Null Island"); + t.end(); + }); + t.test('#toBounds', (t) => { t.deepEqual(new LngLat(0, 0).toBounds(10).toArray(), [[-0.00008983152770714982, -0.00008983152770714982], [0.00008983152770714982, 0.00008983152770714982]]); t.deepEqual(new LngLat(-73.9749, 40.7736).toBounds(10).toArray(), [[-73.97501862141328, 40.77351016847229], [-73.97478137858673, 40.77368983152771]]); diff --git a/test/unit/geo/mercator_coordinate.test.js b/test/unit/geo/mercator_coordinate.test.js index 097a03be44e..72400950b1a 100644 --- a/test/unit/geo/mercator_coordinate.test.js +++ b/test/unit/geo/mercator_coordinate.test.js @@ -35,7 +35,7 @@ test('LngLat', (t) => { t.test('#meterInMercatorCoordinateUnits', (t) => { const nullIsland = new LngLat(0, 0); - t.equal(MercatorCoordinate.fromLngLat(nullIsland).meterInMercatorCoordinateUnits(), 2.495320233665337e-8, 'length of 1 meter in MercatorCoordinate units at the equator'); + t.equal(MercatorCoordinate.fromLngLat(nullIsland).meterInMercatorCoordinateUnits(), 2.4981121214570498e-8, 'length of 1 meter in MercatorCoordinate units at the equator'); t.end(); }); diff --git a/test/unit/geo/transform.test.js b/test/unit/geo/transform.test.js index bfb7f82e29c..8a9632c516e 100644 --- a/test/unit/geo/transform.test.js +++ b/test/unit/geo/transform.test.js @@ -153,6 +153,103 @@ test('transform', (t) => { new OverscaledTileID(10, 0, 10, 511, 512), new OverscaledTileID(10, 0, 10, 512, 512)]); + transform.zoom = 5.1; + transform.pitch = 60.0; + transform.bearing = 32.0; + transform.center = new LngLat(56.90, 48.20); + transform.resize(1024, 768); + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(5, 0, 5, 21, 11), + new OverscaledTileID(5, 0, 5, 20, 11), + new OverscaledTileID(5, 0, 5, 21, 10), + new OverscaledTileID(5, 0, 5, 20, 10), + new OverscaledTileID(5, 0, 5, 21, 12), + new OverscaledTileID(5, 0, 5, 22, 11), + new OverscaledTileID(5, 0, 5, 20, 12), + new OverscaledTileID(5, 0, 5, 22, 10), + new OverscaledTileID(5, 0, 5, 21, 9), + new OverscaledTileID(5, 0, 5, 20, 9), + new OverscaledTileID(5, 0, 5, 22, 9), + new OverscaledTileID(5, 0, 5, 23, 10), + new OverscaledTileID(5, 0, 5, 21, 8), + new OverscaledTileID(5, 0, 5, 20, 8), + new OverscaledTileID(5, 0, 5, 23, 9), + new OverscaledTileID(5, 0, 5, 22, 8), + new OverscaledTileID(5, 0, 5, 23, 8), + new OverscaledTileID(5, 0, 5, 21, 7), + new OverscaledTileID(5, 0, 5, 20, 7), + new OverscaledTileID(5, 0, 5, 24, 9), + new OverscaledTileID(5, 0, 5, 22, 7) + ]); + + transform.zoom = 8; + transform.pitch = 60; + transform.bearing = 45.0; + transform.center = new LngLat(25.02, 60.15); + transform.resize(300, 50); + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(8, 0, 8, 145, 74), + new OverscaledTileID(8, 0, 8, 145, 73), + new OverscaledTileID(8, 0, 8, 146, 74) + ]); + + transform.resize(50, 300); + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(8, 0, 8, 145, 74), + new OverscaledTileID(8, 0, 8, 145, 73), + new OverscaledTileID(8, 0, 8, 146, 74), + new OverscaledTileID(8, 0, 8, 146, 73) + ]); + + transform.zoom = 2; + transform.pitch = 0; + transform.bearing = 0; + transform.resize(300, 300); + t.test('calculates tile coverage at w > 0', (t) => { + transform.center = {lng: 630.01, lat: 0.01}; + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(2, 2, 2, 1, 1), + new OverscaledTileID(2, 2, 2, 1, 2), + new OverscaledTileID(2, 2, 2, 0, 1), + new OverscaledTileID(2, 2, 2, 0, 2) + ]); + t.end(); + }); + + t.test('calculates tile coverage at w = -1', (t) => { + transform.center = {lng: -360.01, lat: 0.01}; + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(2, -1, 2, 1, 1), + new OverscaledTileID(2, -1, 2, 1, 2), + new OverscaledTileID(2, -1, 2, 2, 1), + new OverscaledTileID(2, -1, 2, 2, 2) + ]); + t.end(); + }); + + t.test('calculates tile coverage across meridian', (t) => { + transform.zoom = 1; + transform.center = {lng: -180.01, lat: 0.01}; + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(1, 0, 1, 0, 0), + new OverscaledTileID(1, 0, 1, 0, 1), + new OverscaledTileID(1, -1, 1, 1, 0), + new OverscaledTileID(1, -1, 1, 1, 1) + ]); + t.end(); + }); + + t.test('only includes tiles for a single world, if renderWorldCopies is set to false', (t) => { + transform.zoom = 1; + transform.center = {lng: -180.01, lat: 0.01}; + transform.renderWorldCopies = false; + t.deepEqual(transform.coveringTiles(options), [ + new OverscaledTileID(1, 0, 1, 0, 0), + new OverscaledTileID(1, 0, 1, 0, 1) + ]); + t.end(); + }); + t.end(); }); diff --git a/test/unit/source/vector_tile_source.test.js b/test/unit/source/vector_tile_source.test.js index b3a00588805..4af69e3c791 100644 --- a/test/unit/source/vector_tile_source.test.js +++ b/test/unit/source/vector_tile_source.test.js @@ -200,6 +200,33 @@ test('VectorTileSource', (t) => { window.server.respond(); }); + t.test('canonicalizes tile URLs in inline TileJSON', (t) => { + const source = createSource({ + minzoom: 1, + maxzoom: 10, + attribution: "Mapbox", + tiles: ["https://api.mapbox.com/v4/user.map/{z}/{x}/{y}.png?access_token=key"] + }); + const transformSpy = t.spy(source.map._requestManager, 'transformRequest'); + source.on('data', (e) => { + if (e.sourceDataType === 'metadata') { + t.deepEqual(source.tiles, ["mapbox://tiles/user.map/{z}/{x}/{y}.png?access_token=key"]); + const tile = { + tileID: new OverscaledTileID(10, 0, 10, 5, 5), + state: 'loading', + loadVectorData () {}, + setExpiryData() {} + }; + source.loadTile(tile, () => {}); + t.ok(transformSpy.calledOnce); + t.equal(transformSpy.getCall(0).args[0], `https://api.mapbox.com/v4/user.map/10/5/5.png?sku=${source.map._requestManager._skuToken}&access_token=key`); + t.equal(transformSpy.getCall(0).args[1], 'Tile'); + t.end(); + } + }); + + }); + t.test('reloads a loading tile properly', (t) => { const source = createSource({ tiles: ["http://example.com/{z}/{x}/{y}.png"] diff --git a/test/unit/ui/control/geolocate.test.js b/test/unit/ui/control/geolocate.test.js index 523e96076e7..aa0828925b1 100644 --- a/test/unit/ui/control/geolocate.test.js +++ b/test/unit/ui/control/geolocate.test.js @@ -422,3 +422,60 @@ test('GeolocateControl switches to BACKGROUND state on map manipulation', (t) => geolocate._geolocateButton.dispatchEvent(click); geolocation.send({latitude: 10, longitude: 20, accuracy: 30, timestamp: 40}); }); + +test('GeolocateControl accuracy circle not shown if showAccuracyCircle = false', (t) => { + const map = createMap(t); + const geolocate = new GeolocateControl({ + trackUserLocation: true, + showUserLocation: true, + showAccuracyCircle: false, + }); + map.addControl(geolocate); + + const click = new window.Event('click'); + + geolocate.once('geolocate', () => { + map.jumpTo({ + center: [10, 20] + }); + map.once('zoomend', () => { + t.ok(!geolocate._circleElement.style.width); + t.end(); + }); + map.zoomTo(10, {duration: 0}); + }); + + geolocate._geolocateButton.dispatchEvent(click); + geolocation.send({latitude: 10, longitude: 20, accuracy: 700}); +}); + +test('GeolocateControl accuracy circle radius matches reported accuracy', (t) => { + const map = createMap(t); + const geolocate = new GeolocateControl({ + trackUserLocation: true, + showUserLocation: true, + }); + map.addControl(geolocate); + + const click = new window.Event('click'); + + geolocate.once('geolocate', () => { + t.ok(geolocate._accuracyCircleMarker._map, 'userLocation accuracy circle marker on map'); + t.equal(geolocate._accuracy, 700); + map.jumpTo({ + center: [10, 20] + }); + map.once('zoomend', () => { + t.equal(geolocate._circleElement.style.width, '20px'); // 700m = 20px at zoom 10 + map.once('zoomend', () => { + t.equal(geolocate._circleElement.style.width, '79px'); // 700m = 79px at zoom 12 + t.end(); + }); + map.zoomTo(12, {duration: 0}); + }); + map.zoomTo(10, {duration: 0}); + }); + + geolocate._geolocateButton.dispatchEvent(click); + geolocation.send({latitude: 10, longitude: 20, accuracy: 700}); +}); diff --git a/test/unit/ui/control/logo.test.js b/test/unit/ui/control/logo.test.js index 0d133794dba..0b271b4a6cf 100644 --- a/test/unit/ui/control/logo.test.js +++ b/test/unit/ui/control/logo.test.js @@ -25,7 +25,10 @@ function createMap(t, logoPosition, logoRequired) { function createSource(options, logoRequired) { const source = new VectorTileSource('id', options, {send () {}}); source.onAdd({ - _requestManager: {_skuToken: '1234567890123'}, + _requestManager: { + _skuToken: '1234567890123', + canonicalizeTileset: tileJSON => tileJSON.tiles + }, transform: {angle: 0, pitch: 0, showCollisionBoxes: false}, _getMapId: () => 1 }); diff --git a/test/unit/util/mapbox.test.js b/test/unit/util/mapbox.test.js index 785d01c886c..161ebc1db9f 100644 --- a/test/unit/util/mapbox.test.js +++ b/test/unit/util/mapbox.test.js @@ -25,9 +25,6 @@ function withFixedDate(t, now, fn) { } test("mapbox", (t) => { - const mapboxSource = 'mapbox://user.map'; - const nonMapboxSource = 'http://www.example.com/tiles.json'; - t.beforeEach((callback) => { config.ACCESS_TOKEN = 'key'; config.REQUIRE_ACCESS_TOKEN = true; @@ -61,7 +58,7 @@ test("mapbox", (t) => { const ms13Hours = (13 * 60 * 60 * 1000); t.notOk(manager._isSkuTokenExpired()); const token = manager._skuToken; - withFixedDate(t, now + ms13Hours, () => manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0.pbf", mapboxSource)); + withFixedDate(t, now + ms13Hours, () => manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0.pbf")); t.notEqual(token, manager._skuToken); t.end(); }); @@ -107,17 +104,17 @@ test("mapbox", (t) => { t.test('.normalizeSourceURL', (t) => { t.test('returns a v4 URL with access_token parameter', (t) => { - t.equal(manager.normalizeSourceURL(mapboxSource), 'https://api.mapbox.com/v4/user.map.json?secure&access_token=key'); + t.equal(manager.normalizeSourceURL('mapbox://user.map'), 'https://api.mapbox.com/v4/user.map.json?secure&access_token=key'); t.end(); }); t.test('uses provided access token', (t) => { - t.equal(manager.normalizeSourceURL(mapboxSource, 'token'), 'https://api.mapbox.com/v4/user.map.json?secure&access_token=token'); + t.equal(manager.normalizeSourceURL('mapbox://user.map', 'token'), 'https://api.mapbox.com/v4/user.map.json?secure&access_token=token'); t.end(); }); t.test('uses provided query parameters', (t) => { - t.equal(manager.normalizeSourceURL(`${mapboxSource}?foo=bar`, 'token'), 'https://api.mapbox.com/v4/user.map.json?foo=bar&secure&access_token=token'); + t.equal(manager.normalizeSourceURL('mapbox://user.map?foo=bar', 'token'), 'https://api.mapbox.com/v4/user.map.json?foo=bar&secure&access_token=token'); t.end(); }); @@ -128,14 +125,14 @@ test("mapbox", (t) => { t.test('throws an error if no access token is provided', (t) => { config.ACCESS_TOKEN = null; - t.throws(() => { manager.normalizeSourceURL(mapboxSource); }, 'An API access token is required to use Mapbox GL.'); + t.throws(() => { manager.normalizeSourceURL('mapbox://user.map'); }, 'An API access token is required to use Mapbox GL.'); config.ACCESS_TOKEN = 'key'; t.end(); }); t.test('throws an error if a secret access token is provided', (t) => { config.ACCESS_TOKEN = 'sk.abc.123'; - t.throws(() => { manager.normalizeSourceURL(mapboxSource); }, 'Use a public access token (pk.*) with Mapbox GL JS.'); + t.throws(() => { manager.normalizeSourceURL('mapbox://user.map'); }, 'Use a public access token (pk.*) with Mapbox GL JS.'); config.ACCESS_TOKEN = 'key'; t.end(); }); @@ -265,39 +262,41 @@ test("mapbox", (t) => { }); t.test('.canonicalizeTileURL', (t) => { - t.equals(manager.canonicalizeTileURL("http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf"), + const tileJSONURL = "mapbox://mapbox.streets"; + + t.equals(manager.canonicalizeTileURL("http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf"); - t.equals(manager.canonicalizeTileURL("http://b.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf"), + t.equals(manager.canonicalizeTileURL("http://b.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf"); - t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf"), + t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf"); - t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf?access_token=key"), + t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf?access_token=key", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf"); - t.equals(manager.canonicalizeTileURL("https://api.mapbox.cn/v4/a.b/{z}/{x}/{y}.vector.pbf?access_token=key"), + t.equals(manager.canonicalizeTileURL("https://api.mapbox.cn/v4/a.b/{z}/{x}/{y}.vector.pbf?access_token=key", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf"); - t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b,c.d/{z}/{x}/{y}.vector.pbf?access_token=key"), + t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b,c.d/{z}/{x}/{y}.vector.pbf?access_token=key", tileJSONURL), "mapbox://tiles/a.b,c.d/{z}/{x}/{y}.vector.pbf"); - t.equals(manager.canonicalizeTileURL("http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf?access_token=key&custom=parameter"), + t.equals(manager.canonicalizeTileURL("http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf?access_token=key&custom=parameter", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf?custom=parameter"); - t.equals(manager.canonicalizeTileURL("http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf?custom=parameter&access_token=key"), + t.equals(manager.canonicalizeTileURL("http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf?custom=parameter&access_token=key", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf?custom=parameter"); - t.equals(manager.canonicalizeTileURL("http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf?custom=parameter&access_token=key&second=param"), + t.equals(manager.canonicalizeTileURL("http://a.tiles.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf?custom=parameter&access_token=key&second=param", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf?custom=parameter&second=param"); - t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg?access_token=key"), + t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg?access_token=key", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.jpg"); - t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg70?access_token=key"), + t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg70?access_token=key", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.jpg70"); - t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg?access_token=key"), + t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg?access_token=key", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.jpg"); - t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg70?access_token=key"), + t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.jpg70?access_token=key", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.jpg70"); - t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png"), + t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.png"); - t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png?access_token=key"), + t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png?access_token=key", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.png"); - t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png"), + t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.png"); - t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png?access_token=key"), + t.equals(manager.canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.png?access_token=key", tileJSONURL), "mapbox://tiles/a.b/{z}/{x}/{y}.png"); // We don't ever expect to see these inputs, but be safe anyway. @@ -312,9 +311,9 @@ test("mapbox", (t) => { t.test('.normalizeTileURL does nothing on 1x devices', (t) => { config.API_URL = 'http://path.png'; config.REQUIRE_ACCESS_TOKEN = false; - t.equal(manager.normalizeTileURL('http://path.png/tile.png', mapboxSource), `http://path.png/v4/tile.png`); - t.equal(manager.normalizeTileURL('http://path.png/tile.png32', mapboxSource), `http://path.png/v4/tile.png32`); - t.equal(manager.normalizeTileURL('http://path.png/tile.jpg70', mapboxSource), `http://path.png/v4/tile.jpg70`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png'), `http://path.png/v4/tile.png`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png32'), `http://path.png/v4/tile.png32`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.jpg70'), `http://path.png/v4/tile.jpg70`); t.end(); }); @@ -322,10 +321,10 @@ test("mapbox", (t) => { window.devicePixelRatio = 2; config.API_URL = 'http://path.png'; config.REQUIRE_ACCESS_TOKEN = false; - t.equal(manager.normalizeTileURL('http://path.png/tile.png', mapboxSource), `http://path.png/v4/tile@2x.png`); - t.equal(manager.normalizeTileURL('http://path.png/tile.png32', mapboxSource), `http://path.png/v4/tile@2x.png32`); - t.equal(manager.normalizeTileURL('http://path.png/tile.jpg70', mapboxSource), `http://path.png/v4/tile@2x.jpg70`); - t.equal(manager.normalizeTileURL('http://path.png/tile.png?access_token=foo', mapboxSource), `http://path.png/v4/tile@2x.png?access_token=foo`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png'), `http://path.png/v4/tile@2x.png`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png32'), `http://path.png/v4/tile@2x.png32`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.jpg70'), `http://path.png/v4/tile@2x.jpg70`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png?access_token=foo'), `http://path.png/v4/tile@2x.png?access_token=foo`); window.devicePixelRatio = 1; t.end(); }); @@ -333,10 +332,10 @@ test("mapbox", (t) => { t.test('.normalizeTileURL inserts @2x when tileSize == 512', (t) => { config.API_URL = 'http://path.png'; config.REQUIRE_ACCESS_TOKEN = false; - t.equal(manager.normalizeTileURL('http://path.png/tile.png', mapboxSource, 512), `http://path.png/v4/tile@2x.png`); - t.equal(manager.normalizeTileURL('http://path.png/tile.png32', mapboxSource, 512), `http://path.png/v4/tile@2x.png32`); - t.equal(manager.normalizeTileURL('http://path.png/tile.jpg70', mapboxSource, 512), `http://path.png/v4/tile@2x.jpg70`); - t.equal(manager.normalizeTileURL('http://path.png/tile.png?access_token=foo', mapboxSource, 512), `http://path.png/v4/tile@2x.png?access_token=foo`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png', 512), `http://path.png/v4/tile@2x.png`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png32', 512), `http://path.png/v4/tile@2x.png32`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.jpg70', 512), `http://path.png/v4/tile@2x.jpg70`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png?access_token=foo', 512), `http://path.png/v4/tile@2x.png?access_token=foo`); t.end(); }); @@ -344,25 +343,25 @@ test("mapbox", (t) => { webpSupported.supported = true; config.API_URL = 'http://path.png'; config.REQUIRE_ACCESS_TOKEN = false; - t.equal(manager.normalizeTileURL('http://path.png/tile.png', mapboxSource), `http://path.png/v4/tile.webp`); - t.equal(manager.normalizeTileURL('http://path.png/tile.png32', mapboxSource), `http://path.png/v4/tile.webp`); - t.equal(manager.normalizeTileURL('http://path.png/tile.jpg70', mapboxSource), `http://path.png/v4/tile.webp`); - t.equal(manager.normalizeTileURL('http://path.png/tile.png?access_token=foo', mapboxSource), `http://path.png/v4/tile.webp?access_token=foo`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png'), `http://path.png/v4/tile.webp`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png32'), `http://path.png/v4/tile.webp`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.jpg70'), `http://path.png/v4/tile.webp`); + t.equal(manager.normalizeTileURL('mapbox://path.png/tile.png?access_token=foo'), `http://path.png/v4/tile.webp?access_token=foo`); webpSupported.supported = false; t.end(); }); t.test('.normalizeTileURL ignores non-mapbox:// sources', (t) => { - t.equal(manager.normalizeTileURL('http://path.png', nonMapboxSource), 'http://path.png'); + t.equal(manager.normalizeTileURL('http://path.png'), 'http://path.png'); t.end(); }); t.test('.normalizeTileURL accounts for tileURLs w/ paths', (t) => { // Add a path to the config: config.API_URL = 'http://localhost:8080/mbx'; - const input = `https://localhost:8080/mbx/v4/mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v7/10/184/401.vector.pbf?access_token=${config.ACCESS_TOKEN}`; - const expected = `http://localhost:8080/mbx/v4/mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v7/10/184/401.vector.pbf?sku=${manager._skuToken}&access_token=${config.ACCESS_TOKEN}`; - t.equal(manager.normalizeTileURL(input, 'mapbox://mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v7'), expected); + const input = `mapbox://tiles/mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v7/10/184/401.vector.pbf?access_token=${config.ACCESS_TOKEN}`; + const expected = `http://localhost:8080/mbx/v4/mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v7/10/184/401.vector.pbf?sku=${manager._skuToken}&access_token=${config.ACCESS_TOKEN}`; + t.equal(manager.normalizeTileURL(input), expected); t.end(); }); @@ -373,13 +372,13 @@ test("mapbox", (t) => { t.test('.normalizeTileURL does not modify the access token for non-mapbox sources', (t) => { config.API_URL = 'http://example.com'; - t.equal(manager.normalizeTileURL('http://example.com/tile.png?access_token=tk.abc.123', nonMapboxSource), 'http://example.com/tile.png?access_token=tk.abc.123'); + t.equal(manager.normalizeTileURL('http://example.com/tile.png?access_token=tk.abc.123'), 'http://example.com/tile.png?access_token=tk.abc.123'); t.end(); }); t.test('.normalizeTileURL throw error on falsy url input', (t) => { t.throws(() => { - manager.normalizeTileURL('', mapboxSource); + manager.normalizeTileURL(''); }, new Error('Unable to parse URL object')); t.end(); }); @@ -388,16 +387,16 @@ test("mapbox", (t) => { config.API_URL = 'https://api.mapbox.com/'; // ensure the token exists t.ok(manager._skuToken); - t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0.pbf", mapboxSource), `https://api.mapbox.com/v4/a.b/0/0/0.pbf?sku=${manager._skuToken}&access_token=key`); - t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0.pbf?style=mapbox://styles/mapbox/streets-v9@0", mapboxSource), `https://api.mapbox.com/v4/a.b/0/0/0.pbf?style=mapbox://styles/mapbox/streets-v9@0&sku=${manager._skuToken}&access_token=key`); - t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0.pbf?", mapboxSource), `https://api.mapbox.com/v4/a.b/0/0/0.pbf?sku=${manager._skuToken}&access_token=key`); - t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0.png", mapboxSource), `https://api.mapbox.com/v4/a.b/0/0/0.png?sku=${manager._skuToken}&access_token=key`); - t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0@2x.png", mapboxSource), `https://api.mapbox.com/v4/a.b/0/0/0@2x.png?sku=${manager._skuToken}&access_token=key`); - t.equal(manager.normalizeTileURL("mapbox://tiles/a.b,c.d/0/0/0.pbf", mapboxSource), `https://api.mapbox.com/v4/a.b,c.d/0/0/0.pbf?sku=${manager._skuToken}&access_token=key`); + t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0.pbf"), `https://api.mapbox.com/v4/a.b/0/0/0.pbf?sku=${manager._skuToken}&access_token=key`); + t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0.pbf?style=mapbox://styles/mapbox/streets-v9@0"), `https://api.mapbox.com/v4/a.b/0/0/0.pbf?style=mapbox://styles/mapbox/streets-v9@0&sku=${manager._skuToken}&access_token=key`); + t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0.pbf?"), `https://api.mapbox.com/v4/a.b/0/0/0.pbf?sku=${manager._skuToken}&access_token=key`); + t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0.png"), `https://api.mapbox.com/v4/a.b/0/0/0.png?sku=${manager._skuToken}&access_token=key`); + t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0@2x.png"), `https://api.mapbox.com/v4/a.b/0/0/0@2x.png?sku=${manager._skuToken}&access_token=key`); + t.equal(manager.normalizeTileURL("mapbox://tiles/a.b,c.d/0/0/0.pbf"), `https://api.mapbox.com/v4/a.b,c.d/0/0/0.pbf?sku=${manager._skuToken}&access_token=key`); config.API_URL = 'https://api.example.com/'; - t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0.png", mapboxSource), `https://api.example.com/v4/a.b/0/0/0.png?sku=${manager._skuToken}&access_token=key`); - t.equal(manager.normalizeTileURL("http://path", nonMapboxSource), "http://path"); + t.equal(manager.normalizeTileURL("mapbox://tiles/a.b/0/0/0.png"), `https://api.example.com/v4/a.b/0/0/0.png?sku=${manager._skuToken}&access_token=key`); + t.equal(manager.normalizeTileURL("http://path"), "http://path"); t.end(); }); diff --git a/test/unit/util/primitives.test.js b/test/unit/util/primitives.test.js new file mode 100644 index 00000000000..35b56d50cba --- /dev/null +++ b/test/unit/util/primitives.test.js @@ -0,0 +1,154 @@ +import {test} from '../../util/test'; +import {Aabb, Frustum} from '../../../src/util/primitives'; +import {mat4, vec3} from 'gl-matrix'; + +test('primitives', (t) => { + t.test('aabb', (t) => { + t.test('Create an aabb', (t) => { + const min = vec3.fromValues(0, 0, 0); + const max = vec3.fromValues(2, 4, 6); + const aabb = new Aabb(min, max); + + t.equal(aabb.min, min); + t.equal(aabb.max, max); + t.deepEqual(aabb.center, vec3.fromValues(1, 2, 3)); + t.end(); + }); + + t.test('Create 4 quadrants', (t) => { + const min = vec3.fromValues(0, 0, 0); + const max = vec3.fromValues(2, 4, 1); + const aabb = new Aabb(min, max); + + t.deepEqual(aabb.quadrant(0), new Aabb(vec3.fromValues(0, 0, 0), vec3.fromValues(1, 2, 1))); + t.deepEqual(aabb.quadrant(1), new Aabb(vec3.fromValues(1, 0, 0), vec3.fromValues(2, 2, 1))); + t.deepEqual(aabb.quadrant(2), new Aabb(vec3.fromValues(0, 2, 0), vec3.fromValues(1, 4, 1))); + t.deepEqual(aabb.quadrant(3), new Aabb(vec3.fromValues(1, 2, 0), vec3.fromValues(2, 4, 1))); + + t.end(); + }); + + t.test('Distance to a point', (t) => { + const min = vec3.fromValues(-1, -1, -1); + const max = vec3.fromValues(1, 1, 1); + const aabb = new Aabb(min, max); + + t.equal(aabb.distanceX([0.5, -0.5]), 0); + t.equal(aabb.distanceY([0.5, -0.5]), 0); + + t.equal(aabb.distanceX([1, 1]), 0); + t.equal(aabb.distanceY([1, 1]), 0); + + t.equal(aabb.distanceX([0, 10]), 0); + t.equal(aabb.distanceY([0, 10]), -9); + + t.equal(aabb.distanceX([-2, -2]), 1); + t.equal(aabb.distanceY([-2, -2]), 1); + t.end(); + }); + + const createTestCameraFrustum = (fovy, aspectRatio, zNear, zFar, elevation, rotation) => { + const proj = new Float64Array(16); + const invProj = new Float64Array(16); + // Note that left handed coordinate space is used where z goes towards the sky. + // Y has to be flipped as well because it's part of the projection/camera matrix used in transform.js + mat4.perspective(proj, fovy, aspectRatio, zNear, zFar); + mat4.scale(proj, proj, [1, -1, 1]); + mat4.translate(proj, proj, [0, 0, elevation]); + mat4.rotateZ(proj, proj, rotation); + mat4.invert(invProj, proj); + + return Frustum.fromInvProjectionMatrix(invProj, 1.0, 0.0); + }; + + t.test('Aabb fully inside a frustum', (t) => { + const frustum = createTestCameraFrustum(Math.PI / 2, 1.0, 0.1, 100.0, -5, 0); + + // Intersection test is done in xy-plane + const aabbList = [ + new Aabb(vec3.fromValues(-1, -1, 0), vec3.fromValues(1, 1, 0)), + new Aabb(vec3.fromValues(-5, -5, 0), vec3.fromValues(5, 5, 0)), + new Aabb(vec3.fromValues(-5, -5, 0), vec3.fromValues(-4, -2, 0)) + ]; + + for (const aabb of aabbList) + t.equal(aabb.intersects(frustum), 2); + + t.end(); + }); + + t.test('Aabb intersecting with a frustum', (t) => { + const frustum = createTestCameraFrustum(Math.PI / 2, 1.0, 0.1, 100.0, -5, 0); + + const aabbList = [ + new Aabb(vec3.fromValues(-6, -6, 0), vec3.fromValues(6, 6, 0)), + new Aabb(vec3.fromValues(-6, -6, 0), vec3.fromValues(-5, -5, 0)) + ]; + + for (const aabb of aabbList) + t.equal(aabb.intersects(frustum), 1); + + t.end(); + }); + + t.test('No intersection between aabb and frustum', (t) => { + const frustum = createTestCameraFrustum(Math.PI / 2, 1.0, 0.1, 100.0, -5); + + const aabbList = [ + new Aabb(vec3.fromValues(-6, 0, 0), vec3.fromValues(-5.5, 0, 0)), + new Aabb(vec3.fromValues(-6, -6, 0), vec3.fromValues(-5.5, -5.5, 0)), + new Aabb(vec3.fromValues(7, -10, 0), vec3.fromValues(7.1, 20, 0)) + ]; + + for (const aabb of aabbList) + t.equal(aabb.intersects(frustum), 0); + + t.end(); + }); + + t.end(); + }); + + t.test('frustum', (t) => { + t.test('Create a frustum from inverse projection matrix', (t) => { + const proj = new Float64Array(16); + const invProj = new Float64Array(16); + mat4.perspective(proj, Math.PI / 2, 1.0, 0.1, 100.0); + mat4.invert(invProj, proj); + + const frustum = Frustum.fromInvProjectionMatrix(invProj, 1.0, 0.0); + + // mat4.perspective generates a projection matrix for right handed coordinate space. + // This means that forward direction will be -z + const expectedFrustumPoints = [ + [-0.1, 0.1, -0.1, 1.0], + [0.1, 0.1, -0.1, 1.0], + [0.1, -0.1, -0.1, 1.0], + [-0.1, -0.1, -0.1, 1.0], + [-100.0, 100.0, -100.0, 1.0], + [100.0, 100.0, -100.0, 1.0], + [100.0, -100.0, -100.0, 1.0], + [-100.0, -100.0, -100.0, 1.0], + ]; + + // Round numbers to mitigate the precision loss + frustum.points = frustum.points.map(array => array.map(n => Math.round(n * 10) / 10)); + frustum.planes = frustum.planes.map(array => array.map(n => Math.round(n * 1000) / 1000)); + + const expectedFrustumPlanes = [ + [0, 0, 1.0, 0.1], + [0, 0, -1.0, -100.0], + [-0.707, 0, 0.707, 0], + [0.707, 0, 0.707, 0], + [0, -0.707, 0.707, 0], + [0, 0.707, 0.707, 0] + ]; + + t.deepEqual(frustum.points, expectedFrustumPoints); + t.deepEqual(frustum.planes, expectedFrustumPlanes); + t.end(); + }); + t.end(); + }); + t.end(); +}); diff --git a/test/unit/util/tile_cover.test.js b/test/unit/util/tile_cover.test.js deleted file mode 100644 index 27cf8370f47..00000000000 --- a/test/unit/util/tile_cover.test.js +++ /dev/null @@ -1,95 +0,0 @@ -import {test} from '../../util/test'; -import tileCover from '../../../src/util/tile_cover'; -import MercatorCoordinate from '../../../src/geo/mercator_coordinate'; -import {OverscaledTileID} from '../../../src/source/tile_id'; - -test('tileCover', (t) => { - - t.test('.cover', (t) => { - t.test('calculates tile coverage at w = 0', (t) => { - const z = 2, - coords = [ - new MercatorCoordinate(0, 0.25), - new MercatorCoordinate(0.25, 0.25), - new MercatorCoordinate(0.25, 0.5), - new MercatorCoordinate(0, 0.5) - ], - res = tileCover(z, coords, z); - t.deepEqual(res, [new OverscaledTileID(2, 0, 2, 0, 1)]); - t.end(); - }); - - t.test('calculates tile coverage at w > 0', (t) => { - const z = 2, - coords = [ - new MercatorCoordinate(3, 0.25), - new MercatorCoordinate(3.25, 0.25), - new MercatorCoordinate(3.25, 0.5), - new MercatorCoordinate(3, 0.5) - ], - res = tileCover(z, coords, z); - t.deepEqual(res, [new OverscaledTileID(2, 3, 2, 0, 1)]); - t.end(); - }); - - t.test('calculates tile coverage at w = -1', (t) => { - const z = 2, - coords = [ - new MercatorCoordinate(-0.25, 0.25), - new MercatorCoordinate(0, 0.25), - new MercatorCoordinate(0, 0.5), - new MercatorCoordinate(-0.25, 0.5) - ], - res = tileCover(z, coords, z); - t.deepEqual(res, [new OverscaledTileID(2, -1, 2, 3, 1)]); - t.end(); - }); - - t.test('calculates tile coverage at w < -1', (t) => { - const z = 2, - coords = [ - new MercatorCoordinate(-3.25, 0.25), - new MercatorCoordinate(-3, 0.25), - new MercatorCoordinate(-3, 0.5), - new MercatorCoordinate(-3.25, 0.5) - ], - res = tileCover(z, coords, z); - t.deepEqual(res, [new OverscaledTileID(2, -4, 2, 3, 1)]); - t.end(); - }); - - t.test('calculates tile coverage across meridian', (t) => { - const z = 2, - coords = [ - new MercatorCoordinate(-0.125, 0.25), - new MercatorCoordinate(0.125, 0.25), - new MercatorCoordinate(0.125, 0.5), - new MercatorCoordinate(-0.125, 0.5) - ], - res = tileCover(z, coords, z); - t.deepEqual(res, [ - new OverscaledTileID(2, 0, 2, 0, 1), - new OverscaledTileID(2, -1, 2, 3, 1)]); - t.end(); - }); - - t.test('only includes tiles for a single world, if renderWorldCopies is set to false', (t) => { - const z = 2, - coords = [ - new MercatorCoordinate(-0.125, 0.25), - new MercatorCoordinate(0.125, 0.25), - new MercatorCoordinate(0.125, 0.5), - new MercatorCoordinate(-0.125, 0.5) - ], - renderWorldCopies = false, - res = tileCover(z, coords, z, renderWorldCopies); - t.deepEqual(res, [ - new OverscaledTileID(2, 0, 2, 0, 1)]); - t.end(); - }); - - t.end(); - }); - - t.end(); -}); diff --git a/yarn.lock b/yarn.lock index 3f1d32ba56c..525db365395 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3677,10 +3677,10 @@ duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" -earcut@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.1.tgz#3bae0b1b6fec41853b56b126f03a42a34b28f1d5" - integrity sha512-5jIMi2RB3HtGPHcYd9Yyl0cczo84y+48lgKPxMijliNQaKAHEZJbdzLmKmdxG/mCdS/YD9DQ1gihL8mxzR0F9w== +earcut@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.2.tgz#41b0bc35f63e0fe80da7cddff28511e7e2e80d11" + integrity sha512-eZoZPPJcUHnfRZ0PjLvx2qBordSiO8ofC3vt+qACLM95u+4DovnbYNpQtJh0DNsWj8RnxrQytD4WA8gj5cRIaQ== ecc-jsbn@~0.1.1: version "0.1.2"