Skip to content

Commit

Permalink
Globe view marker support (#11556)
Browse files Browse the repository at this point in the history
  • Loading branch information
SnailBones authored Mar 29, 2022
1 parent 0781eed commit 45dd9cb
Show file tree
Hide file tree
Showing 10 changed files with 403 additions and 55 deletions.
5 changes: 3 additions & 2 deletions debug/markers.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<label>Projection:</label>
<select id="projName">
<option value="mercator" selected>Mercator</option>
<option value="globe">Globe</option>
<option value="albers">Albers USA</option>
<option value="equalEarth">Equal Earth</option>
<option value="equirectangular">Equirectangular</option>
Expand Down Expand Up @@ -51,8 +52,8 @@
var w = bounds.getWest();
var e = bounds.getEast();

var lng = w + (e - w) * (i / 3);
var lat = s + (n - s) * (j / 3);
var lng = w + (e - w) * ((i + .5) / 3);
var lat = s + (n - s) * ((j + .5) / 3);

var popup = new mapboxgl.Popup().setHTML(`Pitch Alignment: ${pitchAlignment}<br>Rotation Alignment: ${rotationAlignment}`);

Expand Down
8 changes: 7 additions & 1 deletion flow-typed/gl-matrix.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ declare module "gl-matrix" {
lerp<T: Vec3>(T, Vec3, Vec3, number): T,
transformQuat<T: Vec3>(T, Vec3, Quat): T,
transformMat3<T: Vec3>(T, Vec3, Mat3): T,
transformMat4<T: Vec3>(T, Vec3, Mat4): T
transformMat4<T: Vec3>(T, Vec3, Mat4): T,
angle(Vec3, Vec3): number;
rotateX<T: Vec3>(T, Vec3, Vec3, number): T,
rotateY<T: Vec3>(T, Vec3, Vec3, number): T,
rotateZ<T: Vec3>(T, Vec3, Vec3, number): T,
};

declare var vec4: {
Expand Down Expand Up @@ -82,6 +86,8 @@ declare module "gl-matrix" {
rotateX<T: Mat4>(T, Mat4, number): T,
rotateY<T: Mat4>(T, Mat4, number): T,
rotateZ<T: Mat4>(T, Mat4, number): T,
rotate<T: Mat4>(T, Mat4, number, Vec3): T,
fromRotation<T: Mat4>(T, number, Vec3): T,
translate<T: Mat4>(T, Mat4, Vec3): T,
invert<T: Mat4>(T, Mat4): T,
copy<T: Mat4>(T, Mat4): T,
Expand Down
12 changes: 6 additions & 6 deletions src/geo/projection/far_z.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ export function farthestPixelDistanceOnSphere(tr: Transform, pixelsPerMeter: num
} else {
// Background space is visible. Find distance to the point of the
// globe where surface normal is parallel to the view vector
const p0 = vec3.sub([], cameraPosition, globeCenter);
const p1 = vec3.sub([], globeCenter, cameraPosition);
vec3.normalize(p1, p1);
const globeCenterToCamera = vec3.sub([], cameraPosition, globeCenter);
const cameraToGlobe = vec3.sub([], globeCenter, cameraPosition);
vec3.normalize(cameraToGlobe, cameraToGlobe);

const cameraHeight = vec3.length(p0) - globeRadius;
pixelDistance = Math.sqrt(cameraHeight * cameraHeight + 2 * globeRadius * cameraHeight);
const angle = Math.acos(pixelDistance / (globeRadius + cameraHeight)) - Math.acos(vec3.dot(forward, p1));
const cameraHeight = vec3.length(globeCenterToCamera) - globeRadius;
pixelDistance = Math.sqrt(cameraHeight * (cameraHeight + 2 * globeRadius));
const angle = Math.acos(pixelDistance / (globeRadius + cameraHeight)) - Math.acos(vec3.dot(forward, cameraToGlobe));
pixelDistance *= Math.cos(angle);
}

Expand Down
47 changes: 45 additions & 2 deletions src/geo/projection/globe_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import EXTENT from '../../data/extent.js';
import {number as interpolate} from '../../style-spec/util/interpolate.js';
import {degToRad, smoothstep, clamp} from '../../util/util.js';
import {mat4, vec3} from 'gl-matrix';
import {vec3, mat4} from 'gl-matrix';
import SegmentVector from '../../data/segment.js';
import {members as globeLayoutAttributes, atmosphereLayout} from '../../terrain/globe_attributes.js';
import posAttributes from '../../data/pos_attributes.js';
Expand All @@ -20,12 +20,14 @@ import LngLatBounds from '../lng_lat_bounds.js';

import type {CanonicalTileID, UnwrappedTileID} from '../../source/tile_id.js';
import type Context from '../../gl/context.js';
import type {Mat4, Vec3} from 'gl-matrix';
import type {Vec3, Mat4} from 'gl-matrix';
import type IndexBuffer from '../../gl/index_buffer.js';
import type VertexBuffer from '../../gl/vertex_buffer.js';
import type Transform from '../transform.js';
import Point from '@mapbox/point-geometry';

export const GLOBE_RADIUS = EXTENT / Math.PI / 2.0;
const GLOBE_METERS_TO_ECEF = mercatorZfromAltitude(1, 0.0) * 2.0 * GLOBE_RADIUS * Math.PI;
const GLOBE_NORMALIZATION_BIT_RANGE = 15;
const GLOBE_NORMALIZATION_MASK = (1 << (GLOBE_NORMALIZATION_BIT_RANGE - 1)) - 1;
const GLOBE_VERTEX_GRID_SIZE = 64;
Expand Down Expand Up @@ -419,6 +421,47 @@ const POLE_RAD = degToRad(85.0);
const POLE_COS = Math.cos(POLE_RAD);
const POLE_SIN = Math.sin(POLE_RAD);

function cameraPositionInECEF(tr: Transform): Array<number> {
// Here "center" is the center of the globe. We refer to transform._center
// (the surface of the map on the center of the screen) as "pivot" to avoid confusion.
const centerToPivot = latLngToECEF(tr._center.lat, tr._center.lng);

// Set axis to East-West line tangent to sphere at pivot
const south = vec3.fromValues(0, 1, 0);
let axis = vec3.cross([], south, centerToPivot);

// Rotate axis around pivot by bearing
const rotation = mat4.fromRotation([], -tr.angle, centerToPivot);
axis = vec3.transformMat4(axis, axis, rotation);

// Rotate camera around axis by pitch
mat4.fromRotation(rotation, -tr._pitch, axis);

const pivotToCamera = vec3.normalize([], centerToPivot);
vec3.scale(pivotToCamera, pivotToCamera, tr.cameraToCenterDistance / tr.pixelsPerMeter * GLOBE_METERS_TO_ECEF);
vec3.transformMat4(pivotToCamera, pivotToCamera, rotation);

return vec3.add([], centerToPivot, pivotToCamera);
}

// Return the angle of the normal vector of the sphere relative to the camera at a screen point.
// i.e. how much to tilt map-aligned markers.
export function globeTiltAtScreenPoint(tr: Transform, point: Point): number {
const lngLat = tr.pointLocation(point);
const centerToPoint = latLngToECEF(lngLat.lat, lngLat.lng);
const centerToCamera = cameraPositionInECEF(tr);
const pointToCamera = vec3.subtract([], centerToCamera, centerToPoint);
return vec3.angle(pointToCamera, centerToPoint);
}

export function globeCenterToScreenPoint(tr: Transform): Point {
const pos = [0, 0, 0];
const matrix = mat4.identity(new Float64Array(16));
mat4.multiply(matrix, tr.pixelMatrix, tr.globeMatrix);
vec3.transformMat4(pos, pos, matrix);
return new Point(pos[0], pos[1]);
}

export class GlobeSharedBuffers {
_poleNorthVertexBuffer: VertexBuffer;
_poleSouthVertexBuffer: VertexBuffer;
Expand Down
4 changes: 2 additions & 2 deletions src/geo/projection/projection.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ export default class Projection {
return {x, y, z: 0};
}

locationPoint(tr: Transform, lngLat: LngLat): Point {
return tr._coordinatePoint(tr.locationCoordinate(lngLat), false);
locationPoint(tr: Transform, lngLat: LngLat, terrain: boolean = true): Point {
return tr._coordinatePoint(tr.locationCoordinate(lngLat), terrain);
}

pixelsPerMeter(lat: number, worldSize: number): number {
Expand Down
2 changes: 1 addition & 1 deletion src/geo/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -1081,7 +1081,7 @@ class Transform {
* @private
*/
locationPoint3D(lnglat: LngLat): Point {
return this._coordinatePoint(this.locationCoordinate(lnglat), true);
return this.projection.locationPoint(this, lnglat, true);
}

/**
Expand Down
11 changes: 11 additions & 0 deletions src/ui/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,17 @@ class Map extends Camera {
return {name: "mercator", center:[0, 0]};
}

/**
* Returns true if map [projection](https://docs.mapbox.com/mapbox-gl-js/style-spec/projection/) has been set to globe AND the map is at a low enough zoom level that globe view is enabled.
* @private
* @returns {boolean} Returns `globe-is-active` boolean.
* @example
* if (map._usingGlobe()) {
* // do globe things here
* }
*/
_usingGlobe(): boolean { return this.transform.projection.name === 'globe'; }

/**
* Sets the map's projection. If called with `null` or `undefined`, the map will reset to Mercator.
*
Expand Down
109 changes: 75 additions & 34 deletions src/ui/marker.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import window from '../util/window.js';
import LngLat from '../geo/lng_lat.js';
import Point from '@mapbox/point-geometry';
import smartWrap from '../util/smart_wrap.js';
import {bindAll, extend} from '../util/util.js';
import {bindAll, extend, radToDeg} from '../util/util.js';
import {type Anchor, anchorTranslate} from './anchor.js';
import {Event, Evented} from '../util/evented.js';
import type Map from './map.js';
import type Popup from './popup.js';
import type {LngLatLike} from "../geo/lng_lat.js";
import type {MapMouseEvent, MapTouchEvent} from './events.js';
import type {PointLike} from '@mapbox/point-geometry';
import {globeTiltAtScreenPoint, globeCenterToScreenPoint} from '../geo/projection/globe_util.js';

type Options = {
element?: HTMLElement,
Expand Down Expand Up @@ -111,7 +112,7 @@ export default class Marker extends Evented {
this._state = 'inactive';
this._rotation = (options && options.rotation) || 0;
this._rotationAlignment = (options && options.rotationAlignment) || 'auto';
this._pitchAlignment = options && options.pitchAlignment && options.pitchAlignment !== 'auto' ? options.pitchAlignment : this._rotationAlignment;
this._pitchAlignment = (options && options.pitchAlignment && options.pitchAlignment) || 'auto';
this._updateMoving = () => this._update(true);

if (!options || !options.element) {
Expand Down Expand Up @@ -409,6 +410,22 @@ export default class Marker extends Evented {
return this;
}

_occluded(unprojected: LngLat): boolean {
const map = this._map;
if (!map) return false;
const camera = map.getFreeCameraOptions();
if (camera.position) {
const cameraLngLat = camera.position.toLngLat();
const shortestDistance = cameraLngLat.distanceTo(unprojected);
const distanceToMarker = cameraLngLat.distanceTo(this._lngLat);
// In globe view, we only occlude if past ~100 km from cameraLngLat (i.e. screen center).
// This fixes an issue where a marker at screen center results in very small distances,
// with the error introduced from `map.unproject(pos)` occasionally causing the marker to be incorrectly occluded.
return shortestDistance < distanceToMarker * 0.9 && (!map._usingGlobe() || distanceToMarker > 100000);
}
return false;
}

_evaluateOpacity() {
const map = this._map;
if (!map) return;
Expand All @@ -419,25 +436,25 @@ export default class Marker extends Evented {
this._clearFadeTimer();
return;
}

const mapLocation = map.unproject(pos);

let terrainOccluded = false;
if (map.transform._terrainEnabled() && map.getTerrain()) {
const camera = map.getFreeCameraOptions();
if (camera.position) {
const cameraPos = camera.position.toLngLat();
// the distance to the marker lat/lng + marker offset location
const offsetDistance = cameraPos.distanceTo(mapLocation);
const distance = cameraPos.distanceTo(this._lngLat);
terrainOccluded = offsetDistance < distance * 0.9;
}
let opacity = 1;
if (map._usingGlobe()) {
opacity = this._occluded(mapLocation) ? 0 : 1;
} else if (map.transform._terrainEnabled() && map.getTerrain()) {
opacity = this._occluded(mapLocation) ? TERRAIN_OCCLUDED_OPACITY : 1;
}

const fogOpacity = map._queryFogOpacity(mapLocation);
const opacity = (1.0 - fogOpacity) * (terrainOccluded ? TERRAIN_OCCLUDED_OPACITY : 1.0);
opacity *= (1.0 - fogOpacity);
const pointerEvents = opacity ? 'auto' : 'none';

this._element.style.opacity = `${opacity}`;
if (this._popup) this._popup._setOpacity(`${opacity}`);
this._element.style.pointerEvents = pointerEvents;
if (this._popup) {
const container = this._popup._container;
if (container) { container.style.pointerEvents = pointerEvents; }
this._popup._setOpacity(opacity);
}

this._fadeTimer = null;
}
Expand All @@ -451,30 +468,54 @@ export default class Marker extends Evented {

_updateDOM() {
const pos = this._pos;
if (!pos) { return; }
const map = this._map;
if (!pos || !map) { return; }

const rotation = this._calculateXYTransform() + this._calculateZTransform();
const offset = this._offset.mult(this._scale);
const pitch = this._calculatePitch();
const rotation = this._calculateRotation();

this._element.style.transform = `
translate(${pos.x}px, ${pos.y}px) ${anchorTranslate[this._anchor]}
rotateX(${pitch}deg) rotateZ(${rotation}deg)
translate(${offset.x}px, ${offset.y}px)
translate(${pos.x}px,${pos.y}px) ${anchorTranslate[this._anchor]}
${rotation}
translate(${offset.x}px,${offset.y}px)
`;
}

_calculatePitch(): number {
if (this._pitchAlignment === "viewport" || this._pitchAlignment === "auto") {
return 0;
} if (this._map && this._pitchAlignment === "map") {
return this._map.getPitch();
_calculateXYTransform(): string {
const pos = this._pos;
const map = this._map;

if (this.getPitchAlignment() !== 'map' || !map || !pos) { return ''; }
if (!map._usingGlobe()) {
const pitch = map.getPitch();
return pitch ? `rotateX(${pitch}deg)` : '';
}
return 0;
const tilt = radToDeg(globeTiltAtScreenPoint(map.transform, pos));
const posFromCenter = pos.sub(globeCenterToScreenPoint(map.transform));
const tiltOverDist = tilt / (Math.abs(posFromCenter.x) + Math.abs(posFromCenter.y));
const yTilt = posFromCenter.x * tiltOverDist;
const xTilt = -posFromCenter.y * tiltOverDist;
if (!xTilt && !yTilt) { return ''; }
return `rotateX(${xTilt}deg) rotateY(${yTilt}deg)`;
}
_calculateZTransform(): string {
const spin = this._calculateRotation();
return spin ? `rotateZ(${spin}deg)` : ``;
}
_calculateRotation(): number {
if (this._rotationAlignment === "viewport" || this._rotationAlignment === "auto") {
return this._rotation;
} if (this._map && this._rotationAlignment === "map") {
const pos = this._pos;
const map = this._map;
if (pos && map && map._usingGlobe()) {
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 diff = south.sub(north);
return radToDeg(Math.atan2(diff.y, diff.x)) - 90;
}
return this._rotation - this._map.getBearing();
}
return 0;
Expand Down Expand Up @@ -512,7 +553,7 @@ export default class Marker extends Evented {
this._updateDOM();
}
if ((map.getTerrain() || map.getFog()) && !this._fadeTimer) {
if ((map._usingGlobe() || map.getTerrain() || map.getFog()) && !this._fadeTimer) {
this._fadeTimer = setTimeout(this._evaluateOpacity.bind(this), 60);
}
});
Expand Down Expand Up @@ -729,7 +770,7 @@ export default class Marker extends Evented {
* const alignment = marker.getRotationAlignment();
*/
getRotationAlignment(): string {
return this._rotationAlignment;
return this._rotationAlignment === `auto` ? 'viewport' : this._rotationAlignment;
}
/**
Expand All @@ -741,19 +782,19 @@ export default class Marker extends Evented {
* marker.setPitchAlignment('map');
*/
setPitchAlignment(alignment: string): this {
this._pitchAlignment = alignment && alignment !== 'auto' ? alignment : this._rotationAlignment;
this._pitchAlignment = alignment || 'auto';
this._update();
return this;
}
/**
* Returns the current `pitchAlignment` property of the marker.
* Returns the current `pitchAlignment` behavior of the marker.
*
* @returns {string} The current pitch alignment of the marker in degrees.
* @returns {string} The current pitch alignment of the marker.
* @example
* const alignment = marker.getPitchAlignment();
*/
getPitchAlignment(): string {
return this._pitchAlignment;
return this._pitchAlignment === `auto` ? this.getRotationAlignment() : this._pitchAlignment;
}
}
9 changes: 6 additions & 3 deletions src/ui/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -640,9 +640,12 @@ export default class Popup extends Evented {
this.remove();
}

_setOpacity(opacity: string) {
if (this._content) this._content.style.opacity = opacity;
if (this._tip) this._tip.style.opacity = opacity;
_setOpacity(opacity: number) {
if (this._content) {
this._content.style.opacity = `${opacity}`;
this._content.style.pointerEvents = opacity ? 'auto' : 'none';
}
if (this._tip) this._tip.style.opacity = `${opacity}`;
}
}

Expand Down
Loading

0 comments on commit 45dd9cb

Please sign in to comment.