Skip to content

Commit c139ad9

Browse files
committed
Drag globe near poles with versor (maplibre#5296)
1 parent 73ee272 commit c139ad9

File tree

4 files changed

+36
-106
lines changed

4 files changed

+36
-106
lines changed

package-lock.json

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"quickselect": "^3.0.0",
4343
"supercluster": "^8.0.1",
4444
"tinyqueue": "^3.0.0",
45+
"versor": "^0.2.0",
4546
"vt-pbf": "^3.1.3"
4647
},
4748
"devDependencies": {

src/geo/projection/vertical_perspective_camera_helper.ts

+2-9
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,7 @@ export class VerticalPerspectiveCameraHelper implements ICameraHelper {
128128
return;
129129
}
130130

131-
// These are actually very similar to mercator controls, and should converge to them at high zooms.
132-
// We avoid using the "grab a place and move it around" approach from mercator here,
133-
// since it is not a very pleasant way to pan a globe.
134-
const oldLat = tr.center.lat;
135-
const oldZoom = tr.zoom;
136-
tr.setCenter(computeGlobePanCenter(deltas.panDelta, tr).wrap());
137-
// Setting the center might adjust zoom to keep globe size constant, we need to avoid adding this adjustment a second time
138-
tr.setZoom(oldZoom + getZoomAdjustment(oldLat, tr.center.lat));
131+
tr.setLocationAtPoint(_preZoomAroundLoc, deltas.around)
139132
}
140133

141134
cameraForBoxAndBearing(options: CameraForBoundsOptions, padding: PaddingOptions, bounds: LngLatBounds, bearing: number, tr: ITransform): CameraForBoxAndBearingHandlerResult {
@@ -451,4 +444,4 @@ export class VerticalPerspectiveCameraHelper implements ICameraHelper {
451444
return oldValue;
452445
}
453446
}
454-
}
447+
}

src/geo/projection/vertical_perspective_transform.ts

+23-97
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {IReadonlyTransform, ITransform} from '../transform_interface';
1717
import type {PaddingOptions} from '../edge_insets';
1818
import type {ProjectionData, ProjectionDataParams} from './projection_data';
1919
import type {CoveringTilesDetailsProvider} from './covering_tiles_details_provider';
20+
import versor from 'versor';
2021

2122
/**
2223
* Describes the intersection of ray and sphere.
@@ -218,14 +219,14 @@ export class VerticalPerspectiveTransform implements ITransform {
218219
get renderWorldCopies(): boolean {
219220
return this._helper.renderWorldCopies;
220221
}
221-
public get nearZ(): number {
222-
return this._helper.nearZ;
222+
public get nearZ(): number {
223+
return this._helper.nearZ;
223224
}
224-
public get farZ(): number {
225-
return this._helper.farZ;
225+
public get farZ(): number {
226+
return this._helper.farZ;
226227
}
227-
public get autoCalculateNearFarZ(): boolean {
228-
return this._helper.autoCalculateNearFarZ;
228+
public get autoCalculateNearFarZ(): boolean {
229+
return this._helper.autoCalculateNearFarZ;
229230
}
230231
setTransitionState(_value: number): void {
231232
// Do nothing
@@ -659,100 +660,25 @@ export class VerticalPerspectiveTransform implements ITransform {
659660
* Note: automatically adjusts zoom to keep planet size consistent
660661
* (same size before and after a {@link setLocationAtPoint} call).
661662
*/
662-
setLocationAtPoint(lnglat: LngLat, point: Point): void {
663-
// This returns some fake coordinates for pixels that do not lie on the planet.
664-
// Whatever uses this `setLocationAtPoint` function will need to account for that.
665-
const pointLngLat = this.unprojectScreenPoint(point);
666-
const vecToPixelCurrent = angularCoordinatesToSurfaceVector(pointLngLat);
667-
const vecToTarget = angularCoordinatesToSurfaceVector(lnglat);
668-
669-
const zero = createVec3f64();
670-
vec3.zero(zero);
671-
672-
const rotatedPixelVector = createVec3f64();
673-
vec3.rotateY(rotatedPixelVector, vecToPixelCurrent, zero, -this.center.lng * Math.PI / 180.0);
674-
vec3.rotateX(rotatedPixelVector, rotatedPixelVector, zero, this.center.lat * Math.PI / 180.0);
675-
676-
// We are looking for the lng,lat that will rotate `vecToTarget`
677-
// so that it is equal to `rotatedPixelVector`.
678-
679-
// The second rotation around X axis cannot change the X component,
680-
// so we first must find the longitude such that rotating `vecToTarget` with it
681-
// will place it so its X component is equal to X component of `rotatedPixelVector`.
682-
// There will exist zero, one or two longitudes that satisfy this.
683-
684-
// x |
685-
// / |
686-
// / | the line is the target X - rotatedPixelVector.x
687-
// / | the x is vecToTarget projected to x,z plane
688-
// . | the dot is origin
689-
//
690-
// We need to rotate vecToTarget so that it intersects the line.
691-
// If vecToTarget is shorter than the distance to the line from origin, it is impossible.
692-
693-
// Otherwise, we compute the intersection of the line with a ring with radius equal to
694-
// length of vecToTarget projected to XZ plane.
695-
696-
const vecToTargetXZLengthSquared = vecToTarget[0] * vecToTarget[0] + vecToTarget[2] * vecToTarget[2];
697-
const targetXSquared = rotatedPixelVector[0] * rotatedPixelVector[0];
698-
if (vecToTargetXZLengthSquared < targetXSquared) {
699-
// Zero solutions - setLocationAtPoint is impossible.
700-
return;
701-
}
702-
703-
// The intersection's Z coordinates
704-
const intersectionA = Math.sqrt(vecToTargetXZLengthSquared - targetXSquared);
705-
const intersectionB = -intersectionA; // the second solution
706-
707-
const lngA = angleToRotateBetweenVectors2D(vecToTarget[0], vecToTarget[2], rotatedPixelVector[0], intersectionA);
708-
const lngB = angleToRotateBetweenVectors2D(vecToTarget[0], vecToTarget[2], rotatedPixelVector[0], intersectionB);
709-
710-
const vecToTargetLngA = createVec3f64();
711-
vec3.rotateY(vecToTargetLngA, vecToTarget, zero, -lngA);
712-
const latA = angleToRotateBetweenVectors2D(vecToTargetLngA[1], vecToTargetLngA[2], rotatedPixelVector[1], rotatedPixelVector[2]);
713-
const vecToTargetLngB = createVec3f64();
714-
vec3.rotateY(vecToTargetLngB, vecToTarget, zero, -lngB);
715-
const latB = angleToRotateBetweenVectors2D(vecToTargetLngB[1], vecToTargetLngB[2], rotatedPixelVector[1], rotatedPixelVector[2]);
716-
// Is at least one of the needed latitudes valid?
717-
718-
const limit = Math.PI * 0.5;
719-
720-
const isValidA = latA >= -limit && latA <= limit;
721-
const isValidB = latB >= -limit && latB <= limit;
722-
723-
let validLng: number;
724-
let validLat: number;
725-
if (isValidA && isValidB) {
726-
// Pick the solution that is closer to current map center.
727-
const centerLngRadians = this.center.lng * Math.PI / 180.0;
728-
const centerLatRadians = this.center.lat * Math.PI / 180.0;
729-
const lngDistA = distanceOfAnglesRadians(lngA, centerLngRadians);
730-
const latDistA = distanceOfAnglesRadians(latA, centerLatRadians);
731-
const lngDistB = distanceOfAnglesRadians(lngB, centerLngRadians);
732-
const latDistB = distanceOfAnglesRadians(latB, centerLatRadians);
733-
734-
if ((lngDistA + latDistA) < (lngDistB + latDistB)) {
735-
validLng = lngA;
736-
validLat = latA;
737-
} else {
738-
validLng = lngB;
739-
validLat = latB;
740-
}
741-
} else if (isValidA) {
742-
validLng = lngA;
743-
validLat = latA;
744-
} else if (isValidB) {
745-
validLng = lngB;
746-
validLat = latB;
747-
} else {
748-
// No solution.
749-
return;
663+
setLocationAtPoint(sourceLnglat: LngLat, targetPoint: Point): void {
664+
const targetLngLat = this.unprojectScreenPoint(targetPoint);
665+
const reprojTargetPoint = this.locationToScreenPoint(targetLngLat);
666+
if (Math.abs(targetPoint.x - reprojTargetPoint.x) > 0.001 || Math.abs(targetPoint.y - reprojTargetPoint.y) > 0.001) {
667+
return; // The point is not on the planet
750668
}
751669

752-
const newLng = validLng / Math.PI * 180;
753-
const newLat = validLat / Math.PI * 180;
670+
const sourceCartesian = versor.cartesian([sourceLnglat.lng, sourceLnglat.lat]);
671+
const targetCartesian = versor.cartesian([targetLngLat.lng, targetLngLat.lat]);
672+
const delta = versor.delta(sourceCartesian, targetCartesian);
673+
const centerQuaternion = versor([-this.center.lng, -this.center.lat, this.bearing]);
674+
const newCenterQuaternion = versor.multiply(centerQuaternion, delta);
675+
const [lambda, phi, gamma] = versor.rotation(newCenterQuaternion);
676+
const newLng = - lambda;
677+
const newLat = - phi;
678+
const newBearing = gamma;
754679
const oldLat = this.center.lat;
755-
this.setCenter(new LngLat(newLng, clamp(newLat, -90, 90)));
680+
this.setCenter(new LngLat(newLng, newLat));
681+
this.setBearing(newBearing);
756682
this.setZoom(this.zoom + getZoomAdjustment(oldLat, this.center.lat));
757683
}
758684

0 commit comments

Comments
 (0)