|
1 | 1 | import Point from '@mapbox/point-geometry';
|
2 | 2 | import {IReadonlyTransform, ITransform} from '../transform_interface';
|
3 |
| -import {ICameraHelper, MapControlsDeltas} from './camera_helper'; |
| 3 | +import {cameraBoundsWarning, ICameraHelper, MapControlsDeltas} from './camera_helper'; |
4 | 4 | import {GlobeProjection} from './globe';
|
5 | 5 | import {LngLat} from '../lng_lat';
|
6 | 6 | import {MercatorCameraHelper} from './mercator_camera_helper';
|
7 |
| -import {computeGlobePanCenter, getGlobeRadiusPixels, getZoomAdjustment} from './globe_utils'; |
| 7 | +import {angularCoordinatesToSurfaceVector, computeGlobePanCenter, getGlobeRadiusPixels, getZoomAdjustment} from './globe_utils'; |
8 | 8 | import {clamp, createVec3f64, differenceOfAnglesDegrees, remapSaturate} from '../../util/util';
|
9 |
| -import {vec3} from 'gl-matrix'; |
10 |
| -import {MAX_VALID_LATITUDE, zoomScale} from '../transform_helper'; |
| 9 | +import {mat4, vec3} from 'gl-matrix'; |
| 10 | +import {MAX_VALID_LATITUDE, scaleZoom, zoomScale} from '../transform_helper'; |
| 11 | +import {CameraForBoundsOptions} from '../../ui/camera'; |
| 12 | +import {LngLatBounds} from '../lng_lat_bounds'; |
| 13 | +import {PaddingOptions} from '../edge_insets'; |
11 | 14 |
|
12 | 15 | /**
|
13 | 16 | * @internal
|
@@ -154,4 +157,131 @@ export class GlobeCameraHelper implements ICameraHelper {
|
154 | 157 | // Setting the center might adjust zoom to keep globe size constant, we need to avoid adding this adjustment a second time
|
155 | 158 | tr.setZoom(oldZoom + getZoomAdjustment(oldLat, tr.center.lat));
|
156 | 159 | }
|
| 160 | + |
| 161 | + cameraForBoxAndBearing(options: CameraForBoundsOptions, padding: PaddingOptions, bounds: LngLatBounds, bearing: number, tr: ITransform) { |
| 162 | + const result = this._mercatorCameraHelper.cameraForBoxAndBearing(options, padding, bounds, bearing, tr); |
| 163 | + |
| 164 | + if (!this.useGlobeControls) { |
| 165 | + return result; |
| 166 | + } |
| 167 | + |
| 168 | + // If globe is enabled, we use the parameters computed for mercator, and just update the zoom to fit the bounds. |
| 169 | + |
| 170 | + // Get clip space bounds including padding |
| 171 | + const xLeft = (padding.left) / tr.width * 2.0 - 1.0; |
| 172 | + const xRight = (tr.width - padding.right) / tr.width * 2.0 - 1.0; |
| 173 | + const yTop = (padding.top) / tr.height * -2.0 + 1.0; |
| 174 | + const yBottom = (tr.height - padding.bottom) / tr.height * -2.0 + 1.0; |
| 175 | + |
| 176 | + // Get camera bounds |
| 177 | + const flipEastWest = differenceOfAnglesDegrees(bounds.getWest(), bounds.getEast()) < 0; |
| 178 | + const lngWest = flipEastWest ? bounds.getEast() : bounds.getWest(); |
| 179 | + const lngEast = flipEastWest ? bounds.getWest() : bounds.getEast(); |
| 180 | + |
| 181 | + const latNorth = Math.max(bounds.getNorth(), bounds.getSouth()); // "getNorth" doesn't always return north... |
| 182 | + const latSouth = Math.min(bounds.getNorth(), bounds.getSouth()); |
| 183 | + |
| 184 | + // Additional vectors will be tested for the rectangle midpoints |
| 185 | + const lngMid = lngWest + differenceOfAnglesDegrees(lngWest, lngEast) * 0.5; |
| 186 | + const latMid = latNorth + differenceOfAnglesDegrees(latNorth, latSouth) * 0.5; |
| 187 | + |
| 188 | + // Obtain a globe projection matrix that does not include pitch (unsupported) |
| 189 | + const clonedTr = tr.clone(); |
| 190 | + clonedTr.setCenter(result.center); |
| 191 | + clonedTr.setBearing(result.bearing); |
| 192 | + clonedTr.setPitch(0); |
| 193 | + clonedTr.setZoom(result.zoom); |
| 194 | + const matrix = clonedTr.modelViewProjectionMatrix; |
| 195 | + |
| 196 | + // Vectors to test - the bounds' corners and edge midpoints |
| 197 | + const testVectors = [ |
| 198 | + angularCoordinatesToSurfaceVector(bounds.getNorthWest()), |
| 199 | + angularCoordinatesToSurfaceVector(bounds.getNorthEast()), |
| 200 | + angularCoordinatesToSurfaceVector(bounds.getSouthWest()), |
| 201 | + angularCoordinatesToSurfaceVector(bounds.getSouthEast()), |
| 202 | + // Also test edge midpoints |
| 203 | + angularCoordinatesToSurfaceVector(new LngLat(lngEast, latMid)), |
| 204 | + angularCoordinatesToSurfaceVector(new LngLat(lngWest, latMid)), |
| 205 | + angularCoordinatesToSurfaceVector(new LngLat(lngMid, latNorth)), |
| 206 | + angularCoordinatesToSurfaceVector(new LngLat(lngMid, latSouth)) |
| 207 | + ]; |
| 208 | + const vecToCenter = angularCoordinatesToSurfaceVector(result.center); |
| 209 | + |
| 210 | + // Test each vector, measure how much to scale down the globe to satisfy all tested points that they are inside clip space. |
| 211 | + let smallestNeededScale = Number.POSITIVE_INFINITY; |
| 212 | + for (const vec of testVectors) { |
| 213 | + if (xLeft < 0) |
| 214 | + smallestNeededScale = GlobeCameraHelper.getLesserNonNegativeNonNull(smallestNeededScale, GlobeCameraHelper.solveVectorScale(vec, vecToCenter, matrix, 'x', xLeft)); |
| 215 | + if (xRight > 0) |
| 216 | + smallestNeededScale = GlobeCameraHelper.getLesserNonNegativeNonNull(smallestNeededScale, GlobeCameraHelper.solveVectorScale(vec, vecToCenter, matrix, 'x', xRight)); |
| 217 | + if (yTop > 0) |
| 218 | + smallestNeededScale = GlobeCameraHelper.getLesserNonNegativeNonNull(smallestNeededScale, GlobeCameraHelper.solveVectorScale(vec, vecToCenter, matrix, 'y', yTop)); |
| 219 | + if (yBottom < 0) |
| 220 | + smallestNeededScale = GlobeCameraHelper.getLesserNonNegativeNonNull(smallestNeededScale, GlobeCameraHelper.solveVectorScale(vec, vecToCenter, matrix, 'y', yBottom)); |
| 221 | + } |
| 222 | + |
| 223 | + if (!Number.isFinite(smallestNeededScale) || smallestNeededScale === 0) { |
| 224 | + cameraBoundsWarning(); |
| 225 | + return undefined; |
| 226 | + } |
| 227 | + |
| 228 | + // Compute target zoom from the obtained scale. |
| 229 | + result.zoom = clonedTr.zoom + scaleZoom(smallestNeededScale); |
| 230 | + } |
| 231 | + |
| 232 | + /** |
| 233 | + * Computes how much to scale the globe in order for a given point on its surface (a location) to project to a given clip space coordinate in either the X or the Y axis. |
| 234 | + * @param vector - Position of the queried location on the surface of the unit sphere globe. |
| 235 | + * @param toCenter - Position of current transform center on the surface of the unit sphere globe. |
| 236 | + * This is needed because zooming the globe not only changes its scale, |
| 237 | + * but also moves the camera closer or further away along this vector (pitch is disregarded). |
| 238 | + * @param projection - The globe projection matrix. |
| 239 | + * @param targetDimension - The dimension in which the scaled vector must match the target value in clip space. |
| 240 | + * @param targetValue - The target clip space value in the specified dimension to which the queried vector must project. |
| 241 | + * @returns How much to scale the globe. |
| 242 | + */ |
| 243 | + private static solveVectorScale(vector: vec3, toCenter: vec3, projection: mat4, targetDimension: 'x' | 'y', targetValue: number): number | null { |
| 244 | + // We want to compute how much to scale the sphere in order for the input `vector` to project to `targetValue` in the given `targetDimension` (X or Y). |
| 245 | + const k = targetValue; |
| 246 | + const columnXorY = targetDimension === 'x' ? |
| 247 | + [projection[0], projection[4], projection[8], projection[12]] : // X |
| 248 | + [projection[1], projection[5], projection[9], projection[13]]; // Y |
| 249 | + const columnZ = [projection[3], projection[7], projection[11], projection[15]]; |
| 250 | + |
| 251 | + const vecDotXY = vector[0] * columnXorY[0] + vector[1] * columnXorY[1] + vector[2] * columnXorY[2]; |
| 252 | + const vecDotZ = vector[0] * columnZ[0] + vector[1] * columnZ[1] + vector[2] * columnZ[2]; |
| 253 | + const toCenterDotXY = toCenter[0] * columnXorY[0] + toCenter[1] * columnXorY[1] + toCenter[2] * columnXorY[2]; |
| 254 | + const toCenterDotZ = toCenter[0] * columnZ[0] + toCenter[1] * columnZ[1] + toCenter[2] * columnZ[2]; |
| 255 | + |
| 256 | + // The following can be derived from writing down what happens to a vector scaled by a parameter ("V * t") when it is multiplied by a projection matrix, then solving for "t". |
| 257 | + // Or rather, we derive it for a vector "V * t + (1-t) * C". Where V is `vector` and C is `toCenter`. The extra addition is needed because zooming out also moves the camera along "C". |
| 258 | + |
| 259 | + const t = (toCenterDotXY + columnXorY[3] - k * toCenterDotZ - k * columnZ[3]) / (toCenterDotXY - vecDotXY - k * toCenterDotZ + k * vecDotZ); |
| 260 | + |
| 261 | + if ( |
| 262 | + toCenterDotXY + k * vecDotZ === vecDotXY + k * toCenterDotZ || |
| 263 | + columnZ[3] * (vecDotXY - toCenterDotXY) + columnXorY[3] * (toCenterDotZ - vecDotZ) + vecDotXY * toCenterDotZ === toCenterDotXY * vecDotZ |
| 264 | + ) { |
| 265 | + // The computed result is invalid. |
| 266 | + return null; |
| 267 | + } |
| 268 | + return t; |
| 269 | + } |
| 270 | + |
| 271 | + /** |
| 272 | + * Returns `newValue` if it is: |
| 273 | + * |
| 274 | + * - not null AND |
| 275 | + * - not negative AND |
| 276 | + * - smaller than `newValue`, |
| 277 | + * |
| 278 | + * ...otherwise returns `oldValue`. |
| 279 | + */ |
| 280 | + private static getLesserNonNegativeNonNull(oldValue: number, newValue: number): number { |
| 281 | + if (newValue !== null && newValue >= 0 && newValue < oldValue) { |
| 282 | + return newValue; |
| 283 | + } else { |
| 284 | + return oldValue; |
| 285 | + } |
| 286 | + } |
157 | 287 | }
|
0 commit comments