Skip to content

Commit

Permalink
Match GlobeView projection parameters with Maplibre v5 (#9201)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress authored Dec 11, 2024
1 parent 0a4a25a commit e9eada6
Show file tree
Hide file tree
Showing 22 changed files with 407 additions and 36 deletions.
4 changes: 3 additions & 1 deletion modules/core/src/controllers/globe-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {MapState, MapStateProps} from './map-controller';
import {mod} from '../utils/math-utils';
import LinearInterpolator from '../transitions/linear-interpolator';

import {MAX_LATITUDE} from '@math.gl/web-mercator';

class GlobeState extends MapState {
// Apply any constraints (mathematical or defined by _viewportProps) to map state
applyConstraints(props: Required<MapStateProps>): Required<MapStateProps> {
Expand All @@ -20,7 +22,7 @@ class GlobeState extends MapState {
if (longitude < -180 || longitude > 180) {
props.longitude = mod(longitude + 180, 360) - 180;
}
props.latitude = clamp(latitude, -89, 89);
props.latitude = clamp(latitude, -MAX_LATITUDE, MAX_LATITUDE);

return props;
}
Expand Down
49 changes: 31 additions & 18 deletions modules/core/src/viewports/globe-viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import {Matrix4} from '@math.gl/core';
import Viewport from './viewport';
import {PROJECTION_MODE} from '../lib/constants';
import {altitudeToFovy, fovyToAltitude} from '@math.gl/web-mercator';
import {MAX_LATITUDE} from '@math.gl/web-mercator';

import {vec3, vec4} from '@math.gl/core';

Expand Down Expand Up @@ -50,46 +52,56 @@ export type GlobeViewportOptions = {
zoom?: number;
/** Use orthographic projection */
orthographic?: boolean;
/** Scaler for the near plane, 1 unit equals to the height of the viewport. Default `0.1` */
/** Camera fovy in degrees. If provided, overrides `altitude` */
fovy?: number;
/** Scaler for the near plane, 1 unit equals to the height of the viewport. Default `0.5` */
nearZMultiplier?: number;
/** Scaler for the far plane, 1 unit equals to the distance from the camera to the edge of the screen. Default `2` */
/** Scaler for the far plane, 1 unit equals to the distance from the camera to the edge of the screen. Default `1` */
farZMultiplier?: number;
/** The resolution at which to turn flat features into 3D meshes, in degrees. Smaller numbers will generate more detailed mesh. Default `10` */
resolution?: number;
};

export default class GlobeViewport extends Viewport {
// @ts-ignore
longitude: number;
// @ts-ignore
latitude: number;
resolution: number;
longitude!: number;
latitude!: number;
resolution!: number;

constructor(opts: GlobeViewportOptions = {}) {
const {
latitude = 0,
longitude = 0,
zoom = 0,
nearZMultiplier = 0.1,
farZMultiplier = 2,
// Matches Maplibre defaults
// https://github.com/maplibre/maplibre-gl-js/blob/f8ab4b48d59ab8fe7b068b102538793bbdd4c848/src/geo/projection/globe_transform.ts#L632-L633
nearZMultiplier = 0.5,
farZMultiplier = 1,
resolution = 10
} = opts;

let {height, altitude = 1.5} = opts;
let {latitude = 0, height, altitude = 1.5, fovy} = opts;

// Clamp to web mercator limit to prevent bad inputs
latitude = Math.max(Math.min(latitude, MAX_LATITUDE), -MAX_LATITUDE);

height = height || 1;
altitude = Math.max(0.75, altitude);
if (fovy) {
altitude = fovyToAltitude(fovy);
} else {
fovy = altitudeToFovy(altitude);
}
// Exagerate distance by latitude to match the Web Mercator distortion
// The goal is that globe and web mercator projection results converge at high zoom
// https://github.com/maplibre/maplibre-gl-js/blob/f8ab4b48d59ab8fe7b068b102538793bbdd4c848/src/geo/projection/globe_transform.ts#L575-L577
const scaleAdjust = 1 / Math.PI / Math.cos((latitude * Math.PI) / 180);
const scale = Math.pow(2, zoom) * scaleAdjust;
const farZ = altitude + (GLOBE_RADIUS * 2 * scale) / height;

// Calculate view matrix
const viewMatrix = new Matrix4().lookAt({eye: [0, -altitude, 0], up: [0, 0, 1]});
const scale = Math.pow(2, zoom);
viewMatrix.rotateX(latitude * DEGREES_TO_RADIANS);
viewMatrix.rotateZ(-longitude * DEGREES_TO_RADIANS);
viewMatrix.scale(scale / height);

const halfFov = Math.atan(0.5 / altitude);
const relativeScale = (GLOBE_RADIUS * 2 * scale) / height;

super({
...opts,
// x, y, width,
Expand All @@ -103,12 +115,13 @@ export default class GlobeViewport extends Viewport {

// projection matrix parameters
distanceScales: getDistanceScales(),
fovyRadians: halfFov * 2,
fovy,
focalDistance: altitude,
near: nearZMultiplier,
far: Math.min(2, 1 / relativeScale + 1) * altitude * farZMultiplier
far: farZ * farZMultiplier
});

this.scale = scale;
this.latitude = latitude;
this.longitude = longitude;
this.resolution = resolution;
Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/views/first-person-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default class FirstPersonView extends View<FirstPersonViewState, FirstPer
super(props);
}

get ViewportType() {
getViewportType() {
return FirstPersonViewport;
}

Expand Down
5 changes: 3 additions & 2 deletions modules/core/src/views/globe-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import View, {CommonViewState, CommonViewProps} from './view';
import GlobeViewport from '../viewports/globe-viewport';
import WebMercatorViewport from '../viewports/web-mercator-viewport';
import GlobeController from '../controllers/globe-controller';

export type GlobeViewState = {
Expand Down Expand Up @@ -37,8 +38,8 @@ export default class GlobeView extends View<GlobeViewState, GlobeViewProps> {
super(props);
}

get ViewportType() {
return GlobeViewport;
getViewportType(viewState: GlobeViewState) {
return viewState.zoom > 12 ? WebMercatorViewport : GlobeViewport;
}

get ControllerType() {
Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/views/map-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default class MapView extends View<MapViewState, MapViewProps> {
super(props);
}

get ViewportType() {
getViewportType() {
return WebMercatorViewport;
}

Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/views/orbit-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default class OrbitView extends View<OrbitViewState, OrbitViewProps> {
this.props.orbitAxis = props.orbitAxis || 'Z';
}

get ViewportType() {
getViewportType() {
return OrbitViewport;
}

Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/views/orthographic-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default class OrthographicView extends View<OrthographicViewState, Orthog
super(props);
}

get ViewportType() {
getViewportType() {
return OrthographicViewport;
}

Expand Down
7 changes: 4 additions & 3 deletions modules/core/src/views/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default abstract class View<
ViewProps extends CommonViewProps<ViewState> = CommonViewProps<ViewState>
> {
id: string;
abstract get ViewportType(): ConstructorOf<Viewport>;
abstract getViewportType(viewState: ViewState): ConstructorOf<Viewport>;
protected abstract get ControllerType(): ConstructorOf<Controller<any>>;

private _x: Position;
Expand Down Expand Up @@ -102,7 +102,7 @@ export default abstract class View<
}

// To correctly compare padding use depth=2
return this.ViewportType === view.ViewportType && deepEqual(this.props, view.props, 2);
return this.constructor === view.constructor && deepEqual(this.props, view.props, 2);
}

/** Make viewport from canvas dimensions and view state */
Expand All @@ -114,7 +114,8 @@ export default abstract class View<
if (!viewportDimensions.height || !viewportDimensions.width) {
return null;
}
return new this.ViewportType({...viewState, ...this.props, ...viewportDimensions});
const ViewportType = this.getViewportType(viewState);
return new ViewportType({...viewState, ...this.props, ...viewportDimensions});
}

getViewStateId(): string {
Expand Down
186 changes: 186 additions & 0 deletions test/apps/projection/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import * as React from 'react';
import {createRoot} from 'react-dom/client';

import {DeckGL} from '@deck.gl/react';
import {
View,
PickingInfo,
MapView,
MapViewState,
_GlobeView as GlobeView,
GlobeViewState,
OrthographicView,
OrthographicViewState
} from '@deck.gl/core';
import {ScatterplotLayer} from '@deck.gl/layers';
import {makePointGrid, makeLineGrid, Point} from './data';
import {Map, Source, Layer} from '@vis.gl/react-maplibre';
// Use dev build for source map and debugging
import maplibregl from 'maplibre-gl/dist/maplibre-gl-dev';
import {CPULineLayer} from './cpu-line-layer';

const VIEWS: Record<
string,
{
view: View;
viewState: any;
xRange: [number, number];
yRange: [number, number];
step: number;
baseMap: 'mercator' | 'globe' | false;
}
> = {
map: {
view: new MapView(),
viewState: {
longitude: 0,
latitude: 0,
zoom: 1
} satisfies MapViewState,
baseMap: 'mercator',
xRange: [-180, 180],
yRange: [-85, 85],
step: 5
},
'map-high-zoom': {
view: new MapView(),
viewState: {
longitude: 24.87,
latitude: 60.175,
zoom: 16
} satisfies MapViewState,
baseMap: 'mercator',
xRange: [24.86, 24.88],
yRange: [60.17, 60.18],
step: 1 / 3000
},
globe: {
view: new GlobeView(),
viewState: {
longitude: 0,
latitude: 0,
zoom: 2
} satisfies GlobeViewState,
baseMap: 'globe',
xRange: [-180, 180],
yRange: [-85, 85],
step: 5
},
orthographic: {
view: new OrthographicView({flipY: false}),
viewState: {
target: [0, 0, 0],
zoom: 0
} satisfies OrthographicViewState,
baseMap: false,
xRange: [-500, 500],
yRange: [-400, 400],
step: 40
},
'orthographic-high-zoom': {
view: new OrthographicView({flipY: false}),
viewState: {
target: [20001, 10001, 0],
zoom: 16
} satisfies OrthographicViewState,
baseMap: false,
xRange: [20000.99, 20001.01],
yRange: [10000.99, 10001.01],
step: 1 / 3000
}
} as const;

function getTooltip({object}: PickingInfo): string | null {
return object ? JSON.stringify(object) : null;
}

function App() {
const [viewMode, setViewMode] = React.useState<keyof typeof VIEWS>('map');

const opts = VIEWS[viewMode];

const pointData = React.useMemo(() => makePointGrid(opts), [viewMode]);
const lineData = React.useMemo(() => makeLineGrid(opts), [viewMode]);
const lineDataGeoJson = React.useMemo(() => {
return {
type: 'FeatureCollection',
features: lineData.map(line => ({
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: line
}
}))
};
}, [lineData]);

const layers = [
// Reference grid when base map is not available (non-geo)
!opts.baseMap &&
new CPULineLayer<Point[]>({
id: 'lines',
data: lineData,
getStartPosition: d => d[0],
getEndPosition: d => d[1]
}),
new ScatterplotLayer<Point>({
id: 'points',
data: pointData,
getPosition: d => d,
getRadius: 5,
getFillColor: d => [
((d[0] - opts.xRange[0]) / (opts.xRange[1] - opts.xRange[0])) * 255,
((d[1] - opts.yRange[0]) / (opts.yRange[1] - opts.yRange[0])) * 255,
0
],
opacity: 0.8,
radiusUnits: 'pixels',
radiusMaxPixels: 5,
pickable: true
})
];

return (
<>
<DeckGL
controller
parameters={{cullMode: 'back'}}
views={opts.view}
initialViewState={opts.viewState}
layers={layers}
getTooltip={getTooltip}
>
{opts.baseMap && (
<Map
reuseMaps
mapLib={maplibregl}
projection={opts.baseMap}
mapStyle="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
>
<Source type="geojson" data={lineDataGeoJson}>
<Layer
type="line"
paint={{
'line-color': 'black',
'line-width': 1
}}
/>
</Source>
</Map>
)}
</DeckGL>
<select
value={viewMode}
onChange={evt => setViewMode(evt.target.value as keyof typeof VIEWS)}
>
{Object.keys(VIEWS).map(mode => (
<option key={mode} value={mode}>
{mode}
</option>
))}
</select>
</>
);
}

createRoot(document.getElementById('app')!).render(<App />);
Loading

0 comments on commit e9eada6

Please sign in to comment.