diff --git a/examples/get-started/pure-js/maplibre-globe/README.md b/examples/get-started/pure-js/maplibre-globe/README.md
new file mode 100644
index 00000000000..6b0922daf8f
--- /dev/null
+++ b/examples/get-started/pure-js/maplibre-globe/README.md
@@ -0,0 +1,21 @@
+## Example: Use deck.gl with Maplibre globe projection
+
+Uses [Vite](https://vitejs.dev/) to bundle and serve files.
+
+## Usage
+
+To install dependencies:
+
+```bash
+npm install
+# or
+yarn
+```
+
+Commands:
+* `npm start` is the development target, to serve the app and hot reload.
+* `npm run build` is the production target, to create the final bundle and write to disk.
+
+### Basemap
+
+The basemap in this example is provided by [CARTO free basemap service](https://carto.com/basemaps). To use an alternative base map solution, visit [this guide](https://deck.gl/docs/get-started/using-with-map#using-other-basemap-services)
diff --git a/examples/get-started/pure-js/maplibre-globe/app.js b/examples/get-started/pure-js/maplibre-globe/app.js
new file mode 100644
index 00000000000..d6cc3aff225
--- /dev/null
+++ b/examples/get-started/pure-js/maplibre-globe/app.js
@@ -0,0 +1,62 @@
+// deck.gl
+// SPDX-License-Identifier: MIT
+// Copyright (c) vis.gl contributors
+
+import {MapboxOverlay as DeckOverlay} from '@deck.gl/mapbox';
+import {GeoJsonLayer, ArcLayer} from '@deck.gl/layers';
+import maplibregl from 'maplibre-gl';
+import 'maplibre-gl/dist/maplibre-gl.css';
+
+// source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz
+const AIR_PORTS =
+ 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson';
+
+const map = new maplibregl.Map({
+ container: 'map',
+ style: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
+ center: [0.45, 51.47],
+ zoom: 0
+});
+
+const deckOverlay = new DeckOverlay({
+ interleaved: true,
+ layers: [
+ new GeoJsonLayer({
+ id: 'airports',
+ data: AIR_PORTS,
+ // Styles
+ filled: true,
+ pointRadiusMinPixels: 2,
+ pointRadiusScale: 2000,
+ getPointRadius: f => 11 - f.properties.scalerank,
+ getFillColor: [200, 0, 80, 180],
+ // Interactive props
+ pickable: true,
+ autoHighlight: true,
+ onClick: info =>
+ // eslint-disable-next-line
+ info.object && alert(`${info.object.properties.name} (${info.object.properties.abbrev})`)
+ // beforeId: 'watername_ocean' // In interleaved mode, render the layer under map labels
+ }),
+ new ArcLayer({
+ id: 'arcs',
+ data: AIR_PORTS,
+ parameters: {
+ cullMode: 'none'
+ },
+ dataTransform: d => d.features.filter(f => f.properties.scalerank < 4),
+ // Styles
+ getSourcePosition: f => [-0.4531566, 51.4709959], // London
+ getTargetPosition: f => f.geometry.coordinates,
+ getSourceColor: [0, 128, 200],
+ getTargetColor: [200, 0, 80],
+ getWidth: 1
+ })
+ ]
+});
+
+map.on('load', () => {
+ map.setProjection({type: 'globe'});
+ map.addControl(deckOverlay);
+ map.addControl(new maplibregl.NavigationControl());
+});
diff --git a/examples/get-started/pure-js/maplibre-globe/index.html b/examples/get-started/pure-js/maplibre-globe/index.html
new file mode 100644
index 00000000000..abd74622796
--- /dev/null
+++ b/examples/get-started/pure-js/maplibre-globe/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+ deck.gl example
+
+
+
+
+
+
+
diff --git a/examples/get-started/pure-js/maplibre-globe/package.json b/examples/get-started/pure-js/maplibre-globe/package.json
new file mode 100644
index 00000000000..a8be9f3cc69
--- /dev/null
+++ b/examples/get-started/pure-js/maplibre-globe/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "deckgl-example-pure-js-maplibre",
+ "version": "0.0.0",
+ "private": true,
+ "license": "MIT",
+ "scripts": {
+ "start": "vite --open",
+ "start-local": "vite --config ../../../vite.config.local.mjs",
+ "build": "vite build"
+ },
+ "dependencies": {
+ "@deck.gl/core": "^9.0.0",
+ "@deck.gl/layers": "^9.0.0",
+ "@deck.gl/mapbox": "^9.0.0",
+ "maplibre-gl": "5.0.0-pre.9"
+ },
+ "devDependencies": {
+ "vite": "^4.0.0"
+ }
+}
diff --git a/modules/core/src/viewports/globe-viewport.ts b/modules/core/src/viewports/globe-viewport.ts
index 1d7657dd7e5..3c25d5fb2ab 100644
--- a/modules/core/src/viewports/globe-viewport.ts
+++ b/modules/core/src/viewports/globe-viewport.ts
@@ -58,6 +58,10 @@ export type GlobeViewportOptions = {
nearZMultiplier?: number;
/** Scaler for the far plane, 1 unit equals to the distance from the camera to the edge of the screen. Default `1` */
farZMultiplier?: number;
+ /** Optionally override the near plane position. `nearZMultiplier` is ignored if `nearZ` is supplied. */
+ nearZ?: number;
+ /** Optionally override the far plane position. `farZMultiplier` is ignored if `farZ` is supplied. */
+ farZ?: 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;
};
@@ -94,7 +98,8 @@ export default class GlobeViewport extends Viewport {
// 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;
+ const nearZ = opts.nearZ ?? nearZMultiplier;
+ const farZ = opts.farZ ?? (altitude + (GLOBE_RADIUS * 2 * scale) / height) * farZMultiplier;
// Calculate view matrix
const viewMatrix = new Matrix4().lookAt({eye: [0, -altitude, 0], up: [0, 0, 1]});
@@ -117,8 +122,8 @@ export default class GlobeViewport extends Viewport {
distanceScales: getDistanceScales(),
fovy,
focalDistance: altitude,
- near: nearZMultiplier,
- far: farZ * farZMultiplier
+ near: nearZ,
+ far: farZ
});
this.scale = scale;
diff --git a/modules/core/src/views/globe-view.ts b/modules/core/src/views/globe-view.ts
index a457f1623c3..148d1eb2f1c 100644
--- a/modules/core/src/views/globe-view.ts
+++ b/modules/core/src/views/globe-view.ts
@@ -18,6 +18,10 @@ export type GlobeViewState = {
minZoom?: number;
/** Max zoom, default `20` */
maxZoom?: number;
+ /** The near plane position */
+ nearZ?: number;
+ /** The far plane position */
+ farZ?: number;
} & CommonViewState;
export type GlobeViewProps = {
diff --git a/modules/core/src/views/map-view.ts b/modules/core/src/views/map-view.ts
index 4d80ef73cc3..22e2dd79164 100644
--- a/modules/core/src/views/map-view.ts
+++ b/modules/core/src/views/map-view.ts
@@ -29,6 +29,10 @@ export type MapViewState = {
maxPitch?: number;
/** Viewport center offsets from lng, lat in meters */
position?: number[];
+ /** The near plane position */
+ nearZ?: number;
+ /** The far plane position */
+ farZ?: number;
} & CommonViewState;
export type MapViewProps = {
diff --git a/modules/mapbox/src/deck-utils.ts b/modules/mapbox/src/deck-utils.ts
index b416e339bef..69e18a1998d 100644
--- a/modules/mapbox/src/deck-utils.ts
+++ b/modules/mapbox/src/deck-utils.ts
@@ -2,17 +2,17 @@
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
-import {Deck, WebMercatorViewport, MapView, _flatten as flatten} from '@deck.gl/core';
-import type {DeckProps, MapViewState, Layer} from '@deck.gl/core';
+import {Deck, MapView, _GlobeView as GlobeView, _flatten as flatten} from '@deck.gl/core';
+import type {Viewport, MapViewState, Layer} from '@deck.gl/core';
+import type {Parameters} from '@luma.gl/core';
import type MapboxLayer from './mapbox-layer';
import type {Map} from './types';
import {lngLatToWorld, unitsPerMeter} from '@math.gl/web-mercator';
-import {GL} from '@luma.gl/constants';
type UserData = {
isExternal: boolean;
- currentViewport?: WebMercatorViewport | null;
+ currentViewport?: Viewport | null;
mapboxLayers: Set>;
// mapboxVersion: {minor: number; major: number};
};
@@ -27,10 +27,10 @@ export function getDeckInstance({
gl,
deck
}: {
- map: Map & {__deck?: Deck | null};
+ map: Map & {__deck?: Deck | null};
gl: WebGL2RenderingContext;
- deck?: Deck;
-}): Deck {
+ deck?: Deck;
+}): Deck {
// Only create one deck instance per context
if (map.__deck) {
return map.__deck;
@@ -40,7 +40,7 @@ export function getDeckInstance({
const customRender = deck?.props._customRender;
const onLoad = deck?.props.onLoad;
- const deckProps = getInterleavedProps({
+ const deckProps = {
...deck?.props,
_customRender: () => {
map.triggerRepaint();
@@ -50,7 +50,9 @@ export function getDeckInstance({
// Rerender will be triggered by MapboxLayer's render()
customRender?.('');
}
- });
+ };
+ deckProps.parameters = {...getDefaultParameters(map, true), ...deckProps.parameters};
+ deckProps.views ||= getDefaultView(map);
let deckInstance: Deck;
@@ -115,26 +117,25 @@ export function removeDeckInstance(map: Map & {__deck?: Deck | null}) {
map.__deck = null;
}
-export function getInterleavedProps(currProps: DeckProps) {
- const nextProps: DeckProps = {
- ...currProps,
- // TODO: remove 'any' cast
- parameters: {
- depthMask: true,
- depthWriteEnabled: true,
- depthCompare: 'less-equal',
- blend: true,
- blendFunc: [GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA],
- polygonOffsetFill: true,
- depthFunc: GL.LEQUAL,
- blendEquation: GL.FUNC_ADD,
- ...currProps.parameters
- } as any,
- // @ts-ignore views prop is hidden by the types because it is not expected to work the same way as in standalone Deck, see documentation
- views: currProps.views || [new MapView({id: 'mapbox'})]
- };
-
- return nextProps;
+export function getDefaultParameters(map: Map, interleaved: boolean): Parameters {
+ const result: Parameters = interleaved
+ ? {
+ depthWriteEnabled: true,
+ depthCompare: 'less-equal',
+ blend: true,
+ blendColorSrcFactor: 'src-alpha',
+ blendColorDstFactor: 'one-minus-src-alpha',
+ blendAlphaSrcFactor: 'one',
+ blendAlphaDstFactor: 'one-minus-src-alpha',
+ blendColorOperation: 'add',
+ blendAlphaOperation: 'add',
+ polygonOffsetLine: true
+ }
+ : {};
+ if (getProjection(map) === 'globe') {
+ result.cullMode = 'back';
+ }
+ return result;
}
export function addLayer(deck: Deck, layer: MapboxLayer): void {
@@ -151,13 +152,18 @@ export function updateLayer(deck: Deck, layer: MapboxLayer): void {
updateLayers(deck);
}
-export function drawLayer(deck: Deck, map: Map, layer: MapboxLayer): void {
+export function drawLayer(
+ deck: Deck,
+ map: Map,
+ layer: MapboxLayer,
+ renderParameters: any
+): void {
let {currentViewport} = deck.userData as UserData;
let clearStack: boolean = false;
if (!currentViewport) {
// This is the first layer drawn in this render cycle.
// Generate viewport from the current map state.
- currentViewport = getViewport(deck, map, true);
+ currentViewport = getViewport(deck, map, renderParameters);
(deck.userData as UserData).currentViewport = currentViewport;
clearStack = true;
}
@@ -175,6 +181,26 @@ export function drawLayer(deck: Deck, map: Map, layer: MapboxLayer): void {
});
}
+function getProjection(map: Map): 'mercator' | 'globe' {
+ const projection = map.getProjection?.();
+ const type =
+ // maplibre projection spec
+ projection?.type ||
+ // mapbox projection spec
+ projection?.name;
+ if (type === 'globe') {
+ return 'globe';
+ }
+ return 'mercator';
+}
+
+export function getDefaultView(map: Map): GlobeView | MapView {
+ if (getProjection(map) === 'globe') {
+ return new GlobeView({id: 'mapbox'});
+ }
+ return new MapView({id: 'mapbox'});
+}
+
export function getViewState(map: Map): MapViewState & {
repeat: boolean;
padding: {
@@ -258,34 +284,42 @@ function centerCameraOnTerrain(map: Map, viewState: MapViewState) {
}
}
-// function getMapboxVersion(map: Map): {minor: number; major: number} {
-// // parse mapbox version string
-// let major = 0;
-// let minor = 0;
-// // @ts-ignore (2339) undefined property
-// const version: string = map.version;
-// if (version) {
-// [major, minor] = version.split('.').slice(0, 2).map(Number);
-// }
-// return {major, minor};
-// }
-
-function getViewport(deck: Deck, map: Map, useMapboxProjection = true): WebMercatorViewport {
- return new WebMercatorViewport({
- id: 'mapbox',
- x: 0,
- y: 0,
+// Since maplibre-gl@5
+// https://github.com/maplibre/maplibre-gl-js/blob/main/src/style/style_layer/custom_style_layer.ts
+type MaplibreRenderParameters = {
+ farZ: number;
+ nearZ: number;
+ fov: number;
+ modelViewProjectionMatrix: number[];
+ projectionMatrix: number[];
+};
+
+function getViewport(deck: Deck, map: Map, renderParameters?: unknown): Viewport {
+ const viewState = getViewState(map);
+ const view = getDefaultView(map);
+
+ if (renderParameters) {
+ // Called from MapboxLayer.render
+ // Magic number, matches mapbox-gl@>=1.3.0's projection matrix
+ view.props.nearZMultiplier = 0.2;
+ }
+
+ // Get the base map near/far plane
+ // renderParameters is maplibre API but not mapbox
+ // Transform is not an official API, properties could be undefined for older versions
+ const nearZ = (renderParameters as MaplibreRenderParameters)?.nearZ ?? map.transform._nearZ;
+ const farZ = (renderParameters as MaplibreRenderParameters)?.farZ ?? map.transform._farZ;
+ if (Number.isFinite(nearZ)) {
+ viewState.nearZ = nearZ / map.transform.height;
+ viewState.farZ = farZ / map.transform.height;
+ }
+ // Otherwise fallback to default calculation using nearZMultiplier/farZMultiplier
+
+ return view.makeViewport({
width: deck.width,
height: deck.height,
- ...getViewState(map),
- nearZMultiplier: useMapboxProjection
- ? // match mapbox-gl@>=1.3.0's projection matrix
- 0.02
- : // use deck.gl's own default
- 0.1,
- nearZ: map.transform._nearZ / map.transform.height,
- farZ: map.transform._farZ / map.transform.height
- });
+ viewState
+ }) as Viewport;
}
function afterRender(deck: Deck, map: Map): void {
@@ -305,7 +339,7 @@ function afterRender(deck: Deck, map: Map): void {
if (hasNonMapboxLayers || hasNonMapboxViews) {
if (mapboxViewportIdx >= 0) {
viewports = viewports.slice();
- viewports[mapboxViewportIdx] = getViewport(deck, map, false);
+ viewports[mapboxViewportIdx] = getViewport(deck, map);
}
deck._drawLayers('mapbox-repaint', {
diff --git a/modules/mapbox/src/mapbox-layer.ts b/modules/mapbox/src/mapbox-layer.ts
index dd12489e1a0..ab6853e55ff 100644
--- a/modules/mapbox/src/mapbox-layer.ts
+++ b/modules/mapbox/src/mapbox-layer.ts
@@ -57,7 +57,7 @@ export default class MapboxLayer implements CustomLayerInt
}
}
- render() {
- drawLayer(this.deck!, this.map!, this);
+ render(gl, renderParameters) {
+ drawLayer(this.deck!, this.map!, this, renderParameters);
}
}
diff --git a/modules/mapbox/src/mapbox-overlay.ts b/modules/mapbox/src/mapbox-overlay.ts
index 015c8d1978b..f7318642bcb 100644
--- a/modules/mapbox/src/mapbox-overlay.ts
+++ b/modules/mapbox/src/mapbox-overlay.ts
@@ -3,7 +3,13 @@
// Copyright (c) vis.gl contributors
import {Deck, assert} from '@deck.gl/core';
-import {getViewState, getDeckInstance, removeDeckInstance, getInterleavedProps} from './deck-utils';
+import {
+ getViewState,
+ getDefaultView,
+ getDeckInstance,
+ removeDeckInstance,
+ getDefaultParameters
+} from './deck-utils';
import type {Map, IControl, MapMouseEvent, ControlPosition} from './types';
import type {MjolnirGestureEvent, MjolnirPointerEvent} from 'mjolnir.js';
@@ -33,7 +39,7 @@ export type MapboxOverlayProps = Omit<
*/
export default class MapboxOverlay implements IControl {
private _props: MapboxOverlayProps;
- private _deck?: Deck;
+ private _deck?: Deck;
private _map?: Map;
private _container?: HTMLDivElement;
private _interleaved: boolean;
@@ -53,8 +59,14 @@ export default class MapboxOverlay implements IControl {
Object.assign(this._props, props);
- if (this._deck) {
- this._deck.setProps(this._interleaved ? getInterleavedProps(this._props) : this._props);
+ if (this._deck && this._map) {
+ this._deck.setProps({
+ ...this._props,
+ parameters: {
+ ...getDefaultParameters(this._map, this._interleaved),
+ ...this._props.parameters
+ }
+ });
}
}
@@ -76,9 +88,11 @@ export default class MapboxOverlay implements IControl {
});
this._container = container;
- this._deck = new Deck({
+ this._deck = new Deck({
...this._props,
parent: container,
+ parameters: {...getDefaultParameters(map, false), ...this._props.parameters},
+ views: this._props.views || getDefaultView(map),
viewState: getViewState(map)
});
@@ -212,9 +226,12 @@ export default class MapboxOverlay implements IControl {
private _updateViewState = () => {
const deck = this._deck;
- if (deck) {
- // @ts-ignore (2345) map is always defined if deck is
- deck.setProps({viewState: getViewState(this._map)});
+ const map = this._map;
+ if (deck && map) {
+ deck.setProps({
+ views: this._props.views || getDefaultView(map),
+ viewState: getViewState(map)
+ });
// Redraw immediately if view state has changed
if (deck.isInitialized) {
deck.redraw();
diff --git a/modules/mapbox/src/types.ts b/modules/mapbox/src/types.ts
index eeab2e6b72a..4e1d1bddf10 100644
--- a/modules/mapbox/src/types.ts
+++ b/modules/mapbox/src/types.ts
@@ -111,6 +111,8 @@ export interface Map extends Evented {
// mapbox v2+, maplibre v3+
getTerrain?(): any;
+ // mapbox v2+, maplibre v5+
+ getProjection?(): any;
// mapbox v2+
getFreeCameraOptions?(): FreeCameraOptions;