Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Globe view marker support #11556

Merged
merged 30 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
245d19d
transform.locationPoint3D to return 2d variant with globe
SnailBones Feb 25, 2022
ff8b63e
Added map.usingGlobe() and improving Flow typing
SnailBones Feb 26, 2022
a2d3d02
Hiding markers and popups and disabling click interactions when behi…
SnailBones Feb 26, 2022
fd8ce5a
Clarifying variable names
SnailBones Mar 2, 2022
ec0b559
Marker rotation responds to globe, first attempt at pitch
SnailBones Mar 2, 2022
6e5a202
Fix to marker pitch 'auto' inconsistency
SnailBones Mar 3, 2022
43280d9
Default to no pitch on globe
SnailBones Mar 3, 2022
64a0d49
Fix type error
SnailBones Mar 3, 2022
22c00a3
Changed map.usingGlobe to private _usingGlobe()
SnailBones Mar 3, 2022
a4a9eb8
Merge branch 'main' into aidan/globe-markers
SnailBones Mar 4, 2022
0208496
Add conditional transform ordering logic so markers lean forward with…
SnailBones Mar 4, 2022
727043d
Limit max marker pitch and fix false occlusion at center
SnailBones Mar 4, 2022
b95d0d1
Always return fallback pitch/rotation alignment instead of 'auto'
SnailBones Mar 4, 2022
13f0c1a
Map pitch effects marker pitch
SnailBones Mar 5, 2022
02d934f
Merge branch 'main' into aidan/globe-markers
SnailBones Mar 21, 2022
41b7d0a
Adding Flow types for fl-matrix
SnailBones Mar 22, 2022
f265dcd
Adding marker draping. Removed new marker layout options
SnailBones Mar 22, 2022
99ff18d
Removing unused variables
SnailBones Mar 22, 2022
5802b24
Don't add rotations of 0 to CSS and update tests to pass
SnailBones Mar 22, 2022
a1208dd
Adding test for marker offset
SnailBones Mar 22, 2022
805bcc7
Adding test for default marker position
SnailBones Mar 22, 2022
0c7b515
Adding unit tests covering marker position and transform on globe
SnailBones Mar 22, 2022
4e1004b
Cleaning up marker transform functions
SnailBones Mar 22, 2022
15f6e28
Expanding test for pitch
SnailBones Mar 23, 2022
a57f8e4
Apply suggestions from code review
SnailBones Mar 25, 2022
d43c5c3
Code cleanup and switch to atan2
SnailBones Mar 25, 2022
7da2d4c
Clarifying comment about occlusion edge case
SnailBones Mar 25, 2022
615388d
Using helper function
SnailBones Mar 25, 2022
1a2ab66
Update example so markers are centered
SnailBones Mar 29, 2022
1e25213
Simplifying check for terrain by moving logic to projection.js
SnailBones Mar 29, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 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
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 @@ -408,6 +410,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
3 changes: 3 additions & 0 deletions src/geo/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,9 @@ class Transform {
* @private
*/
locationPoint3D(lnglat: LngLat): Point {
if (this.projection.name === "globe") {
return this.locationPoint(lnglat);
}
return this._coordinatePoint(this.locationCoordinate(lnglat), true);
karimnaaji marked this conversation as resolved.
Show resolved Hide resolved
}

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