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;