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; }