diff --git a/docs/documentation.yml b/docs/documentation.yml
index 51038ea2ea8..2f69418b898 100644
--- a/docs/documentation.yml
+++ b/docs/documentation.yml
@@ -19,6 +19,7 @@ toc:
- LngLatBoundsLike
- Point
- PointLike
+ - MercatorCoordinate
- name: User Interface
description: |
Controls, markers, and popups add new user interface elements to the map.
diff --git a/docs/pages/example/custom-style-layer.html b/docs/pages/example/custom-style-layer.html
index 3cbc1ddc796..8f2172ebca6 100644
--- a/docs/pages/example/custom-style-layer.html
+++ b/docs/pages/example/custom-style-layer.html
@@ -2,8 +2,8 @@
diff --git a/src/geo/coordinate.js b/src/geo/coordinate.js
deleted file mode 100644
index 52f981d77ab..00000000000
--- a/src/geo/coordinate.js
+++ /dev/null
@@ -1,79 +0,0 @@
-// @flow
-
-/**
- * A coordinate is a column, row, zoom combination, often used
- * as the data component of a tile.
- *
- * @param {number} column
- * @param {number} row
- * @param {number} zoom
- * @private
- */
-class Coordinate {
- column: number;
- row: number;
- zoom: number;
- constructor(column: number, row: number, zoom: number) {
- this.column = column;
- this.row = row;
- this.zoom = zoom;
- }
-
- /**
- * Create a clone of this coordinate that can be mutated without
- * changing the original coordinate
- *
- * @returns {Coordinate} clone
- * @private
- * var coord = new Coordinate(0, 0, 0);
- * var c2 = coord.clone();
- * // since coord is cloned, modifying a property of c2 does
- * // not modify it.
- * c2.zoom = 2;
- */
- clone() {
- return new Coordinate(this.column, this.row, this.zoom);
- }
-
- /**
- * Zoom this coordinate to a given zoom level. This returns a new
- * coordinate object, not mutating the old one.
- *
- * @param {number} zoom
- * @returns {Coordinate} zoomed coordinate
- * @private
- * @example
- * var coord = new Coordinate(0, 0, 0);
- * var c2 = coord.zoomTo(1);
- * c2 // equals new Coordinate(0, 0, 1);
- */
- zoomTo(zoom: number) { return this.clone()._zoomTo(zoom); }
-
- /**
- * Subtract the column and row values of this coordinate from those
- * of another coordinate. The other coordinat will be zoomed to the
- * same level as `this` before the subtraction occurs
- *
- * @param {Coordinate} c other coordinate
- * @returns {Coordinate} result
- * @private
- */
- sub(c: Coordinate) { return this.clone()._sub(c); }
-
- _zoomTo(zoom: number) {
- const scale = Math.pow(2, zoom - this.zoom);
- this.column *= scale;
- this.row *= scale;
- this.zoom = zoom;
- return this;
- }
-
- _sub(c: Coordinate) {
- c = c.zoomTo(this.zoom);
- this.column -= c.column;
- this.row -= c.row;
- return this;
- }
-}
-
-export default Coordinate;
diff --git a/src/geo/mercator_coordinate.js b/src/geo/mercator_coordinate.js
new file mode 100644
index 00000000000..30d18bd7820
--- /dev/null
+++ b/src/geo/mercator_coordinate.js
@@ -0,0 +1,118 @@
+// @flow
+
+import LngLat from '../geo/lng_lat';
+import type {LngLatLike} from '../geo/lng_lat';
+
+/*
+ * The circumference of the world in meters at the given latitude.
+ */
+function circumferenceAtLatitude(latitude: number) {
+ const circumference = 2 * Math.PI * 6378137;
+ return circumference * Math.cos(latitude * Math.PI / 180);
+}
+
+export function mercatorXfromLng(lng: number) {
+ return (180 + lng) / 360;
+}
+
+export function mercatorYfromLat(lat: number) {
+ return (180 - (180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)))) / 360;
+}
+
+export function mercatorZfromAltitude(altitude: number, lat: number) {
+ return altitude / circumferenceAtLatitude(lat);
+}
+
+export function lngFromMercatorX(x: number) {
+ return x * 360 - 180;
+}
+
+export function latFromMercatorY(y: number) {
+ const y2 = 180 - y * 360;
+ return 360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90;
+}
+
+export function altitudeFromMercatorZ(z: number, y: number) {
+ return z * circumferenceAtLatitude(latFromMercatorY(y));
+}
+
+/**
+ * A `MercatorCoordinate` object represents a projected three dimensional position.
+ *
+ * `MercatorCoordinate` uses the web mercator projection ([EPSG:3857](https://epsg.io/3857)) with slightly different units:
+ * - the size of 1 unit is the width of the projected world instead of the "mercator meter"
+ * - the origin of the coordinate space is at the north-west corner instead of the middle
+ *
+ * For example, `MercatorCoordinate(0, 0, 0)` is the north-west corner of the mercator world and
+ * `MercatorCoordinate(1, 1, 0)` is the south-east corner. If you are familiar with
+ * [vector tiles](https://github.com/mapbox/vector-tile-spec) it may be helpful to think
+ * of the coordinate space as the `0/0/0` tile with an extent of `1`.
+ *
+ * The `z` dimension of `MercatorCoordinate` is conformal. A cube in the mercator coordinate space would be rendered as a cube.
+ *
+ * @param {number} x The x component of the position.
+ * @param {number} y The y component of the position.
+ * @param {number} z The z component of the position.
+ * @example
+ * var nullIsland = new mapboxgl.MercatorCoordinate(0.5, 0.5, 0);
+ *
+ * @see [Add a custom style layer](https://www.mapbox.com/mapbox-gl-js/example/custom-style-layer/)
+ */
+class MercatorCoordinate {
+ x: number;
+ y: number;
+ z: number;
+
+ constructor(x: number, y: number, z: number = 0) {
+ this.x = +x;
+ this.y = +y;
+ this.z = +z;
+ }
+
+ /**
+ * Project a `LngLat` to a `MercatorCoordinate`.
+ *
+ * @param {LngLatLike} lngLatLike The location to project.
+ * @param {number} altitude The altitude in meters of the position.
+ * @returns {MercatorCoordinate} The projected mercator coordinate.
+ * @example
+ * var coord = mapboxgl.MercatorCoordinate.fromLngLat({ lng: 0, lat: 0}, 0);
+ * coord; // MercatorCoordinate(0.5, 0.5, 0)
+ */
+ static fromLngLat(lngLatLike: LngLatLike, altitude: number = 0) {
+ const lngLat = LngLat.convert(lngLatLike);
+
+ return new MercatorCoordinate(
+ mercatorXfromLng(lngLat.lng),
+ mercatorYfromLat(lngLat.lat),
+ mercatorZfromAltitude(altitude, lngLat.lat));
+ }
+
+ /**
+ * Returns the `LatLng` for the coordinate.
+ *
+ * @returns {LngLat} The `LngLat` object.
+ * @example
+ * var coord = new mapboxgl.MercatorCoordinate(0.5, 0.5, 0);
+ * var latLng = coord.toLatLng(); // LngLat(0, 0)
+ */
+ toLngLat() {
+ return new LngLat(
+ lngFromMercatorX(this.x),
+ latFromMercatorY(this.y));
+ }
+
+ /**
+ * Returns the altitude in meters of the coordinate.
+ *
+ * @returns {number} The altitude in meters.
+ * @example
+ * var coord = new mapboxgl.MercatorCoordinate(0, 0, 0.02);
+ * coord.toAltitude(); // 6914.281956295339
+ */
+ toAltitude() {
+ return altitudeFromMercatorZ(this.z, this.y);
+ }
+}
+
+export default MercatorCoordinate;
diff --git a/src/geo/transform.js b/src/geo/transform.js
index acda6fc631b..3445e4e93c6 100644
--- a/src/geo/transform.js
+++ b/src/geo/transform.js
@@ -2,8 +2,8 @@
import LngLat from './lng_lat';
import LngLatBounds from './lng_lat_bounds';
+import MercatorCoordinate, {mercatorXfromLng, mercatorYfromLat, mercatorZfromAltitude} from './mercator_coordinate';
import Point from '@mapbox/point-geometry';
-import Coordinate from './coordinate';
import { wrap, clamp } from '../util/util';
import {number as interpolate} from '../style-spec/util/interpolate';
import tileCover from '../util/tile_cover';
@@ -203,12 +203,12 @@ class Transform {
getVisibleUnwrappedCoordinates(tileID: CanonicalTileID) {
const result = [new UnwrappedTileID(0, tileID)];
if (this._renderWorldCopies) {
- const utl = this.pointCoordinate(new Point(0, 0), 0);
- const utr = this.pointCoordinate(new Point(this.width, 0), 0);
- const ubl = this.pointCoordinate(new Point(this.width, this.height), 0);
- const ubr = this.pointCoordinate(new Point(0, this.height), 0);
- const w0 = Math.floor(Math.min(utl.column, utr.column, ubl.column, ubr.column));
- const w1 = Math.floor(Math.max(utl.column, utr.column, ubl.column, ubr.column));
+ const utl = this.pointCoordinate(new Point(0, 0));
+ const utr = this.pointCoordinate(new Point(this.width, 0));
+ const ubl = this.pointCoordinate(new Point(this.width, this.height));
+ const ubr = this.pointCoordinate(new Point(0, this.height));
+ const w0 = Math.floor(Math.min(utl.x, utr.x, ubl.x, ubr.x));
+ const w1 = Math.floor(Math.max(utl.x, utr.x, ubl.x, ubr.x));
// Add an extra copy of the world on each side to properly render ImageSources and CanvasSources.
// Both sources draw outside the tile boundaries of the tile that "contains them" so we need
@@ -251,13 +251,14 @@ class Transform {
if (options.minzoom !== undefined && z < options.minzoom) return [];
if (options.maxzoom !== undefined && z > options.maxzoom) z = options.maxzoom;
- const centerCoord = this.pointCoordinate(this.centerPoint, z);
- const centerPoint = new Point(centerCoord.column - 0.5, centerCoord.row - 0.5);
+ 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), z),
- this.pointCoordinate(new Point(this.width, 0), z),
- this.pointCoordinate(new Point(this.width, this.height), z),
- this.pointCoordinate(new Point(0, this.height), z)
+ 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));
@@ -278,50 +279,26 @@ class Transform {
scaleZoom(scale: number) { return Math.log(scale) / Math.LN2; }
project(lnglat: LngLat) {
+ const lat = clamp(lnglat.lat, -this.maxValidLatitude, this.maxValidLatitude);
return new Point(
- this.lngX(lnglat.lng),
- this.latY(lnglat.lat));
+ mercatorXfromLng(lnglat.lng) * this.worldSize,
+ mercatorYfromLat(lat) * this.worldSize);
}
unproject(point: Point): LngLat {
- return new LngLat(
- this.xLng(point.x),
- this.yLat(point.y));
+ return new MercatorCoordinate(point.x / this.worldSize, point.y / this.worldSize).toLngLat();
}
- get x(): number { return this.lngX(this.center.lng); }
- get y(): number { return this.latY(this.center.lat); }
-
- get point(): Point { return new Point(this.x, this.y); }
-
- /**
- * longitude to absolute x coord
- * @returns {number} pixel coordinate
- */
- lngX(lng: number) {
- return (180 + lng) * this.worldSize / 360;
- }
- /**
- * latitude to absolute y coord
- * @returns {number} pixel coordinate
- */
- latY(lat: number) {
- lat = clamp(lat, -this.maxValidLatitude, this.maxValidLatitude);
- const y = 180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360));
- return (180 - y) * this.worldSize / 360;
- }
-
- xLng(x: number) {
- return x * 360 / this.worldSize - 180;
- }
- yLat(y: number) {
- const y2 = 180 - y * 360 / this.worldSize;
- return 360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90;
- }
+ get point(): Point { return this.project(this.center); }
setLocationAtPoint(lnglat: LngLat, point: Point) {
- const translate = this.pointCoordinate(point)._sub(this.pointCoordinate(this.centerPoint));
- this.center = this.coordinateLocation(this.locationCoordinate(lnglat)._sub(translate));
+ const a = this.pointCoordinate(point);
+ const b = this.pointCoordinate(this.centerPoint);
+ const loc = this.locationCoordinate(lnglat);
+ const newCenter = new MercatorCoordinate(
+ loc.x - (a.x - b.x),
+ loc.y - (a.y - b.y));
+ this.center = this.coordinateLocation(newCenter);
if (this._renderWorldCopies) {
this.center = this.center.wrap();
}
@@ -352,10 +329,7 @@ class Transform {
* @returns {Coordinate}
*/
locationCoordinate(lnglat: LngLat) {
- return new Coordinate(
- this.lngX(lnglat.lng) / this.tileSize,
- this.latY(lnglat.lat) / this.tileSize,
- this.zoom).zoomTo(this.tileZoom);
+ return MercatorCoordinate.fromLngLat(lnglat);
}
/**
@@ -363,16 +337,11 @@ class Transform {
* @param {Coordinate} coord
* @returns {LngLat} lnglat
*/
- coordinateLocation(coord: Coordinate) {
- const zoomedCoord = coord.zoomTo(this.zoom);
- return new LngLat(
- this.xLng(zoomedCoord.column * this.tileSize),
- this.yLat(zoomedCoord.row * this.tileSize));
+ coordinateLocation(coord: MercatorCoordinate) {
+ return coord.toLngLat();
}
- pointCoordinate(p: Point, zoom?: number) {
- if (zoom === undefined) zoom = this.tileZoom;
-
+ pointCoordinate(p: Point) {
const targetZ = 0;
// since we don't know the correct projected z value for the point,
// unproject two points to get a line and then find the point on that
@@ -395,10 +364,9 @@ class Transform {
const t = z0 === z1 ? 0 : (targetZ - z0) / (z1 - z0);
- return new Coordinate(
- interpolate(x0, x1, t) / this.tileSize,
- interpolate(y0, y1, t) / this.tileSize,
- this.zoom)._zoomTo(zoom);
+ return new MercatorCoordinate(
+ interpolate(x0, x1, t) / this.worldSize,
+ interpolate(y0, y1, t) / this.worldSize);
}
/**
@@ -406,9 +374,8 @@ class Transform {
* @param {Coordinate} coord
* @returns {Point} screen point
*/
- coordinatePoint(coord: Coordinate) {
- const zoomedCoord = coord.zoomTo(this.zoom);
- const p = [zoomedCoord.column * this.tileSize, zoomedCoord.row * this.tileSize, 0, 1];
+ coordinatePoint(coord: MercatorCoordinate) {
+ const p = [coord.x * this.worldSize, coord.y * this.worldSize, 0, 1];
vec4.transformMat4(p, p, this.pixelMatrix);
return new Point(p[0] / p[3], p[1] / p[3]);
}
@@ -492,25 +459,27 @@ class Transform {
if (this.latRange) {
const latRange = this.latRange;
- minY = this.latY(latRange[1]);
- maxY = this.latY(latRange[0]);
+ minY = mercatorYfromLat(latRange[1]) * this.worldSize;
+ maxY = mercatorYfromLat(latRange[0]) * this.worldSize;
sy = maxY - minY < size.y ? size.y / (maxY - minY) : 0;
}
if (this.lngRange) {
const lngRange = this.lngRange;
- minX = this.lngX(lngRange[0]);
- maxX = this.lngX(lngRange[1]);
+ minX = mercatorXfromLng(lngRange[0]) * this.worldSize;
+ maxX = mercatorXfromLng(lngRange[1]) * this.worldSize;
sx = maxX - minX < size.x ? size.x / (maxX - minX) : 0;
}
+ const point = this.point;
+
// how much the map should scale to fit the screen into given latitude/longitude ranges
const s = Math.max(sx || 0, sy || 0);
if (s) {
this.center = this.unproject(new Point(
- sx ? (maxX + minX) / 2 : this.x,
- sy ? (maxY + minY) / 2 : this.y));
+ sx ? (maxX + minX) / 2 : point.x,
+ sy ? (maxY + minY) / 2 : point.y));
this.zoom += this.scaleZoom(s);
this._unmodified = unmodified;
this._constraining = false;
@@ -518,7 +487,7 @@ class Transform {
}
if (this.latRange) {
- const y = this.y,
+ const y = point.y,
h2 = size.y / 2;
if (y - h2 < minY) y2 = minY + h2;
@@ -526,7 +495,7 @@ class Transform {
}
if (this.lngRange) {
- const x = this.x,
+ const x = point.x,
w2 = size.x / 2;
if (x - w2 < minX) x2 = minX + w2;
@@ -536,8 +505,8 @@ class Transform {
// pan the map if the screen goes off the range
if (x2 !== undefined || y2 !== undefined) {
this.center = this.unproject(new Point(
- x2 !== undefined ? x2 : this.x,
- y2 !== undefined ? y2 : this.y));
+ x2 !== undefined ? x2 : point.x,
+ y2 !== undefined ? y2 : point.y));
}
this._unmodified = unmodified;
@@ -556,7 +525,8 @@ class Transform {
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 x = this.x, y = this.y;
+ const point = this.point;
+ const x = point.x, y = point.y;
// Calculate z distance of the farthest fragment that should be rendered.
const furthestDistance = Math.cos(Math.PI / 2 - this._pitch) * topHalfSurfaceDistance + this.cameraToCenterDistance;
@@ -578,9 +548,7 @@ class Transform {
this.mercatorMatrix = mat4.scale([], m, [this.worldSize, this.worldSize, this.worldSize]);
// scale vertically to meters per pixel (inverse of ground resolution):
- // worldSize / (circumferenceOfEarth * cos(lat * π / 180))
- const verticalScale = this.worldSize / (2 * Math.PI * 6378137 * Math.abs(Math.cos(this.center.lat * (Math.PI / 180))));
- mat4.scale(m, m, [1, 1, verticalScale, 1]);
+ mat4.scale(m, m, [1, 1, mercatorZfromAltitude(1, this.center.lat) * this.worldSize, 1]);
this.projMatrix = m;
@@ -617,8 +585,8 @@ class Transform {
// calcMatrices hasn't run yet
if (!this.pixelMatrixInverse) return 1;
- const coord = this.pointCoordinate(new Point(0, 0)).zoomTo(this.zoom);
- const p = [coord.column * this.tileSize, coord.row * this.tileSize, 0, 1];
+ const coord = this.pointCoordinate(new Point(0, 0));
+ const p = [coord.x * this.worldSize, coord.y * this.worldSize, 0, 1];
const topPoint = vec4.transformMat4(p, p, this.pixelMatrix);
return topPoint[3] / this.cameraToCenterDistance;
}
diff --git a/src/index.js b/src/index.js
index ca505cd65f6..d04047eb8e0 100644
--- a/src/index.js
+++ b/src/index.js
@@ -16,6 +16,7 @@ import Style from './style/style';
import LngLat from './geo/lng_lat';
import LngLatBounds from './geo/lng_lat_bounds';
import Point from '@mapbox/point-geometry';
+import MercatorCoordinate from './geo/mercator_coordinate';
import {Evented} from './util/evented';
import config from './util/config';
import {setRTLTextPlugin} from './source/rtl_text_plugin';
@@ -37,6 +38,7 @@ const exported = {
LngLat,
LngLatBounds,
Point,
+ MercatorCoordinate,
Evented,
config,
diff --git a/src/render/program/hillshade_program.js b/src/render/program/hillshade_program.js
index e84c9626427..ddfa8948da1 100644
--- a/src/render/program/hillshade_program.js
+++ b/src/render/program/hillshade_program.js
@@ -11,7 +11,7 @@ import {
UniformMatrix4f
} from '../uniform_binding';
import EXTENT from '../../data/extent';
-import Coordinate from '../../geo/coordinate';
+import MercatorCoordinate from '../../geo/mercator_coordinate';
import type Context from '../../gl/context';
import type {UniformValues, UniformLocations} from '../uniform_binding';
@@ -104,13 +104,11 @@ const hillshadeUniformPrepareValues = (
function getTileLatRange(painter: Painter, tileID: OverscaledTileID) {
// for scaling the magnitude of a points slope by its latitude
- const coordinate0 = tileID.toCoordinate();
- const coordinate1 = new Coordinate(
- coordinate0.column, coordinate0.row + 1, coordinate0.zoom);
+ const tilesAtZoom = Math.pow(2, tileID.canonical.z);
+ const y = tileID.canonical.y;
return [
- painter.transform.coordinateLocation(coordinate0).lat,
- painter.transform.coordinateLocation(coordinate1).lat
- ];
+ new MercatorCoordinate(0, y / tilesAtZoom).toLngLat().lat,
+ new MercatorCoordinate(0, (y + 1) / tilesAtZoom).toLngLat().lat];
}
export {
diff --git a/src/source/image_source.js b/src/source/image_source.js
index 8b7d7ddc323..ce3013f454f 100644
--- a/src/source/image_source.js
+++ b/src/source/image_source.js
@@ -1,10 +1,6 @@
// @flow
-import { getCoordinatesCenter } from '../util/util';
-
import { CanonicalTileID } from './tile_id';
-import LngLat from '../geo/lng_lat';
-import Point from '@mapbox/point-geometry';
import { Event, ErrorEvent, Evented } from '../util/evented';
import { getImage, ResourceType } from '../util/ajax';
import browser from '../util/browser';
@@ -13,13 +9,13 @@ import { RasterBoundsArray } from '../data/array_types';
import rasterBoundsAttributes from '../data/raster_bounds_attributes';
import SegmentVector from '../data/segment';
import Texture from '../render/texture';
+import MercatorCoordinate from '../geo/mercator_coordinate';
import type {Source} from './source';
import type {CanvasSourceSpecification} from './canvas_source';
import type Map from '../ui/map';
import type Dispatcher from '../util/dispatcher';
import type Tile from './tile';
-import type Coordinate from '../geo/coordinate';
import type {Callback} from '../types/callback';
import type VertexBuffer from '../gl/vertex_buffer';
import type {
@@ -84,7 +80,6 @@ class ImageSource extends Evented implements Source {
map: Map;
texture: Texture | null;
image: ImageData;
- centerCoord: Coordinate;
tileID: CanonicalTileID;
_boundsArray: RasterBoundsArray;
boundsBuffer: VertexBuffer;
@@ -180,35 +175,21 @@ class ImageSource extends Evented implements Source {
// and create a buffer with the corner coordinates. These coordinates
// may be outside the tile, because raster tiles aren't clipped when rendering.
- const map = this.map;
-
// transform the geo coordinates into (zoom 0) tile space coordinates
- const cornerZ0Coords = coordinates.map((coord) => {
- return map.transform.locationCoordinate(LngLat.convert(coord)).zoomTo(0);
- });
+ const cornerCoords = coordinates.map(MercatorCoordinate.fromLngLat);
// Compute the coordinates of the tile we'll use to hold this image's
// render data
- const centerCoord = this.centerCoord = getCoordinatesCenter(cornerZ0Coords);
- // `column` and `row` may be fractional; round them down so that they
- // represent integer tile coordinates
- centerCoord.column = Math.floor(centerCoord.column);
- centerCoord.row = Math.floor(centerCoord.row);
- this.tileID = new CanonicalTileID(centerCoord.zoom, centerCoord.column, centerCoord.row);
+ this.tileID = getCoordinatesCenterTileID(cornerCoords);
// Constrain min/max zoom to our tile's zoom level in order to force
// SourceCache to request this tile (no matter what the map's zoom
// level)
- this.minzoom = this.maxzoom = centerCoord.zoom;
+ this.minzoom = this.maxzoom = this.tileID.z;
// Transform the corner coordinates into the coordinate space of our
// tile.
- const tileCoords = cornerZ0Coords.map((coord) => {
- const zoomedCoord = coord.zoomTo(centerCoord.zoom);
- return new Point(
- Math.round((zoomedCoord.column - centerCoord.column) * EXTENT),
- Math.round((zoomedCoord.row - centerCoord.row) * EXTENT));
- });
+ const tileCoords = cornerCoords.map((coord) => this.tileID.getTilePoint(coord)._round());
this._boundsArray = new RasterBoundsArray();
this._boundsArray.emplaceBack(tileCoords[0].x, tileCoords[0].y, 0, 0);
@@ -285,4 +266,35 @@ class ImageSource extends Evented implements Source {
}
}
+/**
+ * Given a list of coordinates, get their center as a coordinate.
+ *
+ * @returns centerpoint
+ * @private
+ */
+export function getCoordinatesCenterTileID(coords: Array) {
+ let minX = Infinity;
+ let minY = Infinity;
+ let maxX = -Infinity;
+ let maxY = -Infinity;
+
+ for (const coord of coords) {
+ minX = Math.min(minX, coord.x);
+ minY = Math.min(minY, coord.y);
+ maxX = Math.max(maxX, coord.x);
+ maxY = Math.max(maxY, coord.y);
+ }
+
+ const dx = maxX - minX;
+ const dy = maxY - minY;
+ const dMax = Math.max(dx, dy);
+ const zoom = Math.max(0, Math.floor(-Math.log(dMax) / Math.LN2));
+ const tilesAtZoom = Math.pow(2, zoom);
+
+ return new CanonicalTileID(
+ zoom,
+ Math.floor((minX + maxX) / 2 * tilesAtZoom),
+ Math.floor((minY + maxY) / 2 * tilesAtZoom));
+}
+
export default ImageSource;
diff --git a/src/source/query_features.js b/src/source/query_features.js
index 078c5c65a91..ffa9eb18475 100644
--- a/src/source/query_features.js
+++ b/src/source/query_features.js
@@ -2,7 +2,7 @@
import type SourceCache from './source_cache';
import type StyleLayer from '../style/style_layer';
-import type Coordinate from '../geo/coordinate';
+import type MercatorCoordinate from '../geo/mercator_coordinate';
import type CollisionIndex from '../symbol/collision_index';
import type Transform from '../geo/transform';
import type { RetainedQueryData } from '../symbol/placement';
@@ -11,7 +11,7 @@ import assert from 'assert';
export function queryRenderedFeatures(sourceCache: SourceCache,
styleLayers: {[string]: StyleLayer},
- queryGeometry: Array,
+ queryGeometry: Array,
params: { filter: FilterSpecification, layers: Array },
transform: Transform) {
const maxPitchScaleFactor = transform.maxPitchScaleFactor();
diff --git a/src/source/source_cache.js b/src/source/source_cache.js
index 0976507b204..f80a059d8b0 100644
--- a/src/source/source_cache.js
+++ b/src/source/source_cache.js
@@ -5,7 +5,7 @@ import { create as createSource } from './source';
import Tile from './tile';
import { Event, ErrorEvent, Evented } from '../util/evented';
import TileCache from './tile_cache';
-import Coordinate from '../geo/coordinate';
+import MercatorCoordinate from '../geo/mercator_coordinate';
import { keysDifference } from '../util/util';
import EXTENT from '../data/extent';
import Context from '../gl/context';
@@ -734,7 +734,7 @@ class SourceCache extends Evented {
* @param queryGeometry coordinates of the corners of bounding rectangle
* @returns {Array