diff --git a/debug/markers-altitude.html b/debug/markers-altitude.html
new file mode 100644
index 00000000000..c530fb14b03
--- /dev/null
+++ b/debug/markers-altitude.html
@@ -0,0 +1,216 @@
+
+
+
+ Mapbox GL JS debug page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/geo/projection/globe.ts b/src/geo/projection/globe.ts
index e4b916b1e90..b94680ede40 100644
--- a/src/geo/projection/globe.ts
+++ b/src/geo/projection/globe.ts
@@ -52,13 +52,18 @@ export default class Globe extends Mercator {
return {x: pos[0], y: pos[1], z: pos[2]};
}
- override locationPoint(tr: Transform, lngLat: LngLat): Point {
+ override locationPoint(tr: Transform, lngLat: LngLat, terrain: boolean = true, altitude?: number): Point {
const pos = latLngToECEF(lngLat.lat, lngLat.lng);
const up = vec3.normalize([] as any, pos);
- const elevation = tr.elevation ?
- tr.elevation.getAtPointOrZero(tr.locationCoordinate(lngLat), tr._centerAltitude) :
- tr._centerAltitude;
+ let elevation = 0;
+ if (altitude) {
+ elevation = tr._centerAltitude + altitude;
+ } else {
+ elevation = tr.elevation ?
+ tr.elevation.getAtPointOrZero(tr.locationCoordinate(lngLat), tr._centerAltitude) :
+ tr._centerAltitude;
+ }
const upScale = mercatorZfromAltitude(1, 0) * EXTENT * elevation;
vec3.scaleAndAdd(pos, pos, up, upScale);
diff --git a/src/geo/projection/projection.ts b/src/geo/projection/projection.ts
index c9d2ea2efc9..12c320949ac 100644
--- a/src/geo/projection/projection.ts
+++ b/src/geo/projection/projection.ts
@@ -71,8 +71,8 @@ export default class Projection {
return {x, y, z: 0};
}
- locationPoint(tr: Transform, lngLat: LngLat, terrain: boolean = true): Point {
- return tr._coordinatePoint(tr.locationCoordinate(lngLat), terrain);
+ locationPoint(tr: Transform, lngLat: LngLat, terrain: boolean = true, altitude?: number): Point {
+ return tr._coordinatePoint(tr.locationCoordinate(lngLat, altitude), terrain);
}
pixelsPerMeter(lat: number, worldSize: number): number {
diff --git a/src/geo/transform.ts b/src/geo/transform.ts
index 4c990a769ed..6b3b19dd5c4 100644
--- a/src/geo/transform.ts
+++ b/src/geo/transform.ts
@@ -1529,8 +1529,8 @@ class Transform {
* @returns {Point} screen point
* @private
*/
- locationPoint3D(lnglat: LngLat): Point {
- return this.projection.locationPoint(this, lnglat, true);
+ locationPoint3D(lnglat: LngLat, altitude?: number): Point {
+ return this.projection.locationPoint(this, lnglat, true, (altitude || 0));
}
/**
@@ -1551,8 +1551,8 @@ class Transform {
* @returns {LngLat} lnglat location
* @private
*/
- pointLocation3D(p: Point): LngLat {
- return this.coordinateLocation(this.pointCoordinate3D(p));
+ pointLocation3D(p: Point, altitude?: number): LngLat {
+ return this.coordinateLocation(this.pointCoordinate3D(p, (altitude || 0)));
}
/**
@@ -1674,12 +1674,12 @@ class Transform {
* @param {Point} p top left origin screen point, in pixels.
* @private
*/
- pointCoordinate3D(p: Point): MercatorCoordinate {
- if (!this.elevation) return this.pointCoordinate(p);
+ pointCoordinate3D(p: Point, altitude?: number): MercatorCoordinate {
+ if (!this.elevation) return this.pointCoordinate(p, (altitude || 0));
let raycast: vec3 | null | undefined = this.projection.pointCoordinate3D(this, p.x, p.y);
if (raycast) return new MercatorCoordinate(raycast[0], raycast[1], raycast[2]);
let start = 0, end = this.horizonLineFromTop();
- if (p.y > end) return this.pointCoordinate(p); // holes between tiles below horizon line or below bottom.
+ if (p.y > end) return this.pointCoordinate(p, (altitude || 0)); // holes between tiles below horizon line or below bottom.
const samples = 10;
const threshold = 0.02 * end;
const r = p.clone();
diff --git a/src/ui/map.ts b/src/ui/map.ts
index fb26db4b46d..a997c3c021d 100644
--- a/src/ui/map.ts
+++ b/src/ui/map.ts
@@ -1516,13 +1516,14 @@ export class Map extends Camera {
* the `x` and `y` components of the returned {@link Point} are set to Number.MAX_VALUE.
*
* @param {LngLatLike} lnglat The geographical location to project.
+ * @param {number} altitude The height above sea level.
* @returns {Point} The {@link Point} corresponding to `lnglat`, relative to the map's `container`.
* @example
* const coordinate = [-122.420679, 37.772537];
* const point = map.project(coordinate);
*/
- project(lnglat: LngLatLike): Point {
- return this.transform.locationPoint3D(LngLat.convert(lnglat));
+ project(lnglat: LngLatLike, altitude?: number): Point {
+ return this.transform.locationPoint3D(LngLat.convert(lnglat), (altitude || 0));
}
/**
@@ -1532,6 +1533,7 @@ export class Map extends Camera {
* to the point.
*
* @param {PointLike} point The pixel coordinates to unproject.
+ *@param {number} altitude The height above sea level.
* @returns {LngLat} The {@link LngLat} corresponding to `point`.
* @example
* map.on('click', (e) => {
@@ -1539,8 +1541,8 @@ export class Map extends Camera {
* const coordinate = map.unproject(e.point);
* });
*/
- unproject(point: PointLike): LngLat {
- return this.transform.pointLocation3D(Point.convert(point));
+ unproject(point: PointLike, altitude?: number): LngLat {
+ return this.transform.pointLocation3D(Point.convert(point), (altitude || 0));
}
/** @section {Movement state} */
diff --git a/src/ui/marker.ts b/src/ui/marker.ts
index 9610f80449c..4b7a357c8bf 100644
--- a/src/ui/marker.ts
+++ b/src/ui/marker.ts
@@ -29,6 +29,7 @@ export type MarkerOptions = {
pitchAlignment?: string;
occludedOpacity?: number;
className?: string;
+ altitude?: number;
};
type MarkerEvents = {
@@ -54,6 +55,7 @@ type MarkerEvents = {
* @param {string} [options.rotationAlignment='auto'] The alignment of the marker's rotation.`'map'` is aligned with the map plane, consistent with the cardinal directions as the map rotates. `'viewport'` is screenspace-aligned. `'horizon'` is aligned according to the nearest horizon, on non-globe projections it is equivalent to `'viewport'`. `'auto'` is equivalent to `'viewport'`.
* @param {number} [options.occludedOpacity=0.2] The opacity of a marker that's occluded by 3D terrain.
* @param {string} [options.className] Space-separated CSS class names to add to marker element.
+ * @param {number} [options.altitude=0] The altitude above ground level,how many meters.
* @example
* // Create a new marker.
* const marker = new mapboxgl.Marker()
@@ -94,6 +96,7 @@ export default class Marker extends Evented {
_updateFrameId: number;
_updateMoving: () => void;
_occludedOpacity: number;
+ _altitude: number;
constructor(options?: MarkerOptions, legacyOptions?: MarkerOptions) {
super();
@@ -125,6 +128,7 @@ export default class Marker extends Evented {
this._pitchAlignment = (options && options.pitchAlignment && options.pitchAlignment) || 'auto';
this._updateMoving = () => this._update(true);
this._occludedOpacity = (options && options.occludedOpacity) || 0.2;
+ this._altitude = (options && options.altitude) || 0;
if (!options || !options.element) {
this._defaultMarker = true;
@@ -355,6 +359,7 @@ export default class Marker extends Evented {
}
this._popup = popup;
popup._marker = this;
+ popup._altitude = this._altitude;
if (this._lngLat) this._popup.setLngLat(this._lngLat);
this._element.setAttribute('role', 'button');
@@ -436,7 +441,7 @@ export default class Marker extends Evented {
const map = this._map;
const pos = this._pos;
if (!map || !pos) return false;
- const unprojected = map.unproject(pos);
+ const unprojected = map.unproject(pos, (this._altitude || 0));
const camera = map.getFreeCameraOptions();
if (!camera.position) return false;
const cameraLngLat = camera.position.toLngLat();
@@ -456,7 +461,7 @@ export default class Marker extends Evented {
this._clearFadeTimer();
return;
}
- const mapLocation = map.unproject(pos);
+ const mapLocation = map.unproject(pos, (this._altitude || 0));
let opacity;
if (map._showingGlobe() && isLngLatBehindGlobe(map.transform, this._lngLat)) {
opacity = 0;
@@ -535,8 +540,8 @@ export default class Marker extends Evented {
const alignment = this.getRotationAlignment();
if (alignment === 'map') {
if (map._showingGlobe()) {
- const north = map.project(new LngLat(this._lngLat.lng, this._lngLat.lat + .001));
- const south = map.project(new LngLat(this._lngLat.lng, this._lngLat.lat - .001));
+ const north = map.project(new LngLat(this._lngLat.lng, this._lngLat.lat + .001), (this._altitude || 0));
+ const south = map.project(new LngLat(this._lngLat.lng, this._lngLat.lat - .001), (this._altitude || 0));
const diff = south.sub(north);
rotation = radToDeg(Math.atan2(diff.y, diff.x)) - 90;
} else {
@@ -568,10 +573,10 @@ export default class Marker extends Evented {
if (!map) return;
if (map.transform.renderWorldCopies) {
- this._lngLat = smartWrap(this._lngLat, this._pos, map.transform);
+ this._lngLat = smartWrap(this._lngLat, this._pos, map.transform, (this._altitude || 0));
}
- this._pos = map.project(this._lngLat);
+ this._pos = map.project(this._lngLat, (this._altitude || 0));
// because rounding the coordinates at every `move` event causes stuttered zooming
// we only round them when _update is called with `moveend` or when its called with
@@ -686,7 +691,7 @@ export default class Marker extends Evented {
}
this._pos = e.point.sub(posDelta);
- this._lngLat = map.unproject(this._pos);
+ this._lngLat = map.unproject(this._pos, (this._altitude || 0));
this.setLngLat(this._lngLat);
// suppress click event so that popups don't toggle on drag
this._element.style.pointerEvents = 'none';
diff --git a/src/ui/popup.ts b/src/ui/popup.ts
index 87b13692ea4..3e3da91587e 100644
--- a/src/ui/popup.ts
+++ b/src/ui/popup.ts
@@ -33,6 +33,7 @@ export type PopupOptions = {
offset?: Offset;
className?: string;
maxWidth?: string;
+ altitude?: number;
};
type PopupEvents = {
@@ -76,6 +77,7 @@ const focusQuerySelector = [
*
* Negative offsets indicate left and up.
* @param {string} [options.className] Space-separated CSS class names to add to popup container.
+ * @param {number} [options.altitude=0] The altitude above ground level,how many meters.
* @param {string} [options.maxWidth='240px'] -
* A string that sets the CSS property of the popup's maximum width (for example, `'300px'`).
* To ensure the popup resizes to fit its content, set this property to `'none'`.
@@ -117,10 +119,12 @@ export default class Popup extends Evented {
_anchor: Anchor;
_classList: Set;
_marker: Marker | null | undefined;
+ _altitude: number;
constructor(options?: PopupOptions) {
super();
this.options = extend(Object.create(defaultOptions), options);
+ this._altitude = (options && options.altitude) || 0;
bindAll(['_update', '_onClose', 'remove', '_onMouseEvent'], this);
this._classList = new Set(options && options.className ?
options.className.trim().split(/\s+/) : []);
@@ -617,11 +621,11 @@ export default class Popup extends Evented {
}
if (map.transform.renderWorldCopies && !this._trackPointer) {
- this._lngLat = smartWrap(this._lngLat, this._pos, map.transform);
+ this._lngLat = smartWrap(this._lngLat, this._pos, map.transform, (this._altitude || 0));
}
if (!this._trackPointer || cursor) {
- const pos = this._pos = this._trackPointer && cursor instanceof Point ? cursor : map.project(this._lngLat);
+ const pos = this._pos = this._trackPointer && cursor instanceof Point ? cursor : map.project(this._lngLat, (this._altitude || 0));
const offsetBottom = normalizeOffset(this.options.offset);
const anchor = this._anchor = this._getAnchor(offsetBottom.y);
diff --git a/src/util/smart_wrap.ts b/src/util/smart_wrap.ts
index 0312932a034..a1b06dd3019 100644
--- a/src/util/smart_wrap.ts
+++ b/src/util/smart_wrap.ts
@@ -18,7 +18,7 @@ import type Transform from '../geo/transform';
*
* @private
*/
-export default function(lngLat: LngLat, priorPos: Point | null | undefined, transform: Transform): LngLat {
+export default function(lngLat: LngLat, priorPos: Point | null | undefined, transform: Transform, altitude?: number): LngLat {
lngLat = new LngLat(lngLat.lng, lngLat.lat);
// First, try shifting one world in either direction, and see if either is closer to the
@@ -31,11 +31,11 @@ export default function(lngLat: LngLat, priorPos: Point | null | undefined, tran
// Unless offscreen, keep the marker within same wrap distance to center. This is to prevent
// running it to infinity `lng` near horizon when bearing is ~90°.
const withinWrap = Math.ceil(Math.abs(lngLat.lng - transform.center.lng) / 360) * 360;
- const delta = transform.locationPoint(lngLat).distSqr(priorPos);
+ const delta = transform.locationPoint3D(lngLat, altitude).distSqr(priorPos);
const offscreen = priorPos.x < 0 || priorPos.y < 0 || priorPos.x > transform.width || priorPos.y > transform.height;
- if (transform.locationPoint(left).distSqr(priorPos) < delta && (offscreen || Math.abs(left.lng - transform.center.lng) < withinWrap)) {
+ if (transform.locationPoint3D(left, altitude).distSqr(priorPos) < delta && (offscreen || Math.abs(left.lng - transform.center.lng) < withinWrap)) {
lngLat = left;
- } else if (transform.locationPoint(right).distSqr(priorPos) < delta && (offscreen || Math.abs(right.lng - transform.center.lng) < withinWrap)) {
+ } else if (transform.locationPoint3D(right, altitude).distSqr(priorPos) < delta && (offscreen || Math.abs(right.lng - transform.center.lng) < withinWrap)) {
lngLat = right;
}
}
@@ -43,7 +43,7 @@ export default function(lngLat: LngLat, priorPos: Point | null | undefined, tran
// Second, wrap toward the center until the new position is on screen, or we can't get
// any closer.
while (Math.abs(lngLat.lng - transform.center.lng) > 180) {
- const pos = transform.locationPoint(lngLat);
+ const pos = transform.locationPoint3D(lngLat, altitude);
if (pos.x >= 0 && pos.y >= 0 && pos.x <= transform.width && pos.y <= transform.height) {
break;
}