diff --git a/examples/basemap-browser/README.md b/examples/basemap-browser/README.md new file mode 100644 index 00000000000..d98dea30ed3 --- /dev/null +++ b/examples/basemap-browser/README.md @@ -0,0 +1,142 @@ +# Basemap Browser + +A TypeScript/React test application for quickly testing deck.gl with different basemap providers and configurations. Uses React function components throughout. + +## Features + +- **Multiple Basemap Providers**: Google Maps, Mapbox, MapLibre, and MapLibre Globe +- **Framework Variants**: Pure JS and React implementations (configurations from get-started examples) +- **Interleaved Mode Toggle**: Test both interleaved (shared GL context) and overlaid modes +- **Live Metrics**: Monitor Device Pixel Ratio and canvas dimensions in real-time +- **TypeScript**: Fully typed for better development experience +- **Test Matrix Coverage**: Covers all combinations tested in resize/DPR bug fix + +## Architecture + +The basemap-browser uses TypeScript and React function components with a modular architecture: + +``` +src/ +├── types.ts # TypeScript type definitions +├── constants.ts # Shared constants from get-started examples +├── layers.ts # Layer configurations (from get-started examples) +├── examples/ +│ └── index.ts # Example configurations matching get-started +├── app.tsx # Main app (React function component) +├── map-container.tsx # Map rendering (React function components) +└── index.tsx # Entry point +``` + +### Key Design Decisions + +1. **TypeScript Throughout**: All files use TypeScript for type safety +2. **React Function Components**: No class components, uses hooks for state management +3. **Shared Layer Configs**: Layer definitions extracted from get-started examples into `layers.ts` +4. **Type-Safe Examples**: Example configurations are fully typed via `types.ts` + +## Usage + +```bash +# From the examples/basemap-browser directory +yarn +yarn start-local +``` + +Open http://localhost:8080 in your browser. + +## Testing Resize and DPR Changes + +1. **Window Resize Test**: Resize your browser window and verify that: + - Layers redraw correctly + - Canvas dimensions update + - No visual artifacts appear + +2. **Device Pixel Ratio Test**: Move the browser window between screens with different pixel ratios (e.g., from Retina to non-Retina display) and verify that: + - DPR value updates in the control panel + - Layers scale correctly without blur + - Canvas drawing buffer dimensions adjust + +3. **Interleaved vs Overlaid**: Toggle the "Interleaved Mode" checkbox and verify: + - Both modes work correctly + - Resize and DPR changes work in both modes + - Layers render correctly in both modes + +## Test Matrix + +The basemap browser covers these configurations: + +### Google Maps +- ✅ Pure JS + Interleaved: true +- ✅ Pure JS + Interleaved: false +- ✅ React + Interleaved: true +- ✅ React + Interleaved: false + +### Mapbox +- ✅ Pure JS + Interleaved: true +- ✅ Pure JS + Interleaved: false +- ✅ React + Interleaved: true +- ✅ React + Interleaved: false + +### MapLibre +- ✅ Pure JS + Interleaved: true +- ✅ Pure JS + Interleaved: false +- ✅ React + Interleaved: true +- ✅ React + Interleaved: false + +### MapLibre Globe +- ✅ Pure JS + Interleaved: true +- ✅ Pure JS + Interleaved: false +- ✅ React + Interleaved: true +- ✅ React + Interleaved: false + +## Google Maps Setup + +To test Google Maps examples, you need to set environment variables: + +```bash +export GoogleMapsAPIKey="your-api-key" +export GoogleMapsMapId="your-map-id" +``` + +Or add them to your `.env` file. The vite config will automatically inject them. + +## Adding New Examples + +To add a new basemap example: + +1. Add layer configuration to `src/layers.ts` if needed +2. Add example config to `src/examples/index.ts`: + +```typescript +'New Example': { + name: 'New Example', + mapType: 'maplibre', // or 'mapbox' or 'google-maps' + framework: 'react', // or 'pure-js' + mapStyle: MAPLIBRE_STYLE, + initialViewState: { + latitude: 51.47, + longitude: 0.45, + zoom: 4, + bearing: 0, + pitch: 30 + }, + getLayers: getAirportLayers +} +``` + +## Relation to get-started Examples + +This browser extracts the core layer configurations from the get-started examples into reusable functions: + +- Layer configs in `src/layers.ts` match those in `examples/get-started/*/app.js` +- Constants in `src/constants.ts` are shared across all examples +- Example configurations in `src/examples/index.ts` use the same initial view states + +## Known Issues + +- Google Maps in overlaid mode (interleaved: false) may show a blank canvas when entering fullscreen - this is a pre-existing issue unrelated to the resize/DPR fix + +## Related PRs + +- [#9886](https://github.com/visgl/deck.gl/pull/9886) - Canvas resize bug fix (9.2 branch) +- [#9887](https://github.com/visgl/deck.gl/pull/9887) - DPR fix for interleaved mode diff --git a/examples/basemap-browser/index.html b/examples/basemap-browser/index.html new file mode 100644 index 00000000000..88c2cc323d4 --- /dev/null +++ b/examples/basemap-browser/index.html @@ -0,0 +1,70 @@ + + + + + deck.gl Basemap Browser + + + +
+
+ + + diff --git a/examples/basemap-browser/package.json b/examples/basemap-browser/package.json new file mode 100644 index 00000000000..b82e3b517b8 --- /dev/null +++ b/examples/basemap-browser/package.json @@ -0,0 +1,29 @@ +{ + "name": "deckgl-examples-basemap-browser", + "version": "0.0.0", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "start-local": "vite --config ../vite.config.local.mjs" + }, + "dependencies": { + "@deck.gl/core": "*", + "@deck.gl/google-maps": "*", + "@deck.gl/layers": "*", + "@deck.gl/mapbox": "*", + "@vis.gl/react-google-maps": "^1.7.1", + "deck.gl": "*", + "mapbox-gl": "^3.8.0", + "maplibre-gl": "^5.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-map-gl": "^8.0.0" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "typescript": "^4.6.0", + "vite": "^4.0.0" + } +} diff --git a/examples/basemap-browser/src/constants.ts b/examples/basemap-browser/src/constants.ts new file mode 100644 index 00000000000..54d86678b68 --- /dev/null +++ b/examples/basemap-browser/src/constants.ts @@ -0,0 +1,12 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +// Shared constants from get-started examples +export const AIR_PORTS = + 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson'; + +export const MAPBOX_STYLE = 'mapbox://styles/mapbox/light-v9'; +export const MAPLIBRE_STYLE = 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json'; + +export const LONDON_COORDINATES: [number, number] = [-0.4531566, 51.4709959]; diff --git a/examples/basemap-browser/src/control-panel.tsx b/examples/basemap-browser/src/control-panel.tsx new file mode 100644 index 00000000000..5780c4908a8 --- /dev/null +++ b/examples/basemap-browser/src/control-panel.tsx @@ -0,0 +1,168 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import React, {useState, useEffect, useCallback} from 'react'; +import BASEMAP_CATEGORIES from './examples'; +import type {BasemapExample} from './types'; + +type CanvasSize = { + width: number; + height: number; + clientWidth?: number; + clientHeight?: number; + drawingBufferWidth?: number; + drawingBufferHeight?: number; +}; + +type ControlPanelProps = { + onExampleChange: (example: BasemapExample, interleaved: boolean) => void; +}; + +export default function ControlPanel({onExampleChange}: ControlPanelProps) { + const [selectedExample, setSelectedExample] = useState('MapLibre Pure JS'); + const [interleaved, setInterleaved] = useState(true); + const [currentDPR, setCurrentDPR] = useState(window.devicePixelRatio); + const [canvasSize, setCanvasSize] = useState({width: 0, height: 0}); + + const getCurrentExample = useCallback((): BasemapExample | null => { + for (const category of Object.values(BASEMAP_CATEGORIES)) { + if (category[selectedExample]) { + return category[selectedExample]; + } + } + return null; + }, [selectedExample]); + + const updateCanvasInfo = useCallback(() => { + const canvas = document.querySelector('canvas'); + if (canvas) { + // Get WebGL context to read drawing buffer size + const gl = canvas.getContext('webgl2') || canvas.getContext('webgl'); + + setCanvasSize({ + width: canvas.width, + height: canvas.height, + clientWidth: canvas.clientWidth, + clientHeight: canvas.clientHeight, + drawingBufferWidth: gl?.drawingBufferWidth, + drawingBufferHeight: gl?.drawingBufferHeight + }); + } + }, []); + + useEffect(() => { + // Continuously poll for all changes (canvas, DPR, dimensions) + const interval = setInterval(() => { + const newDPR = window.devicePixelRatio; + if (newDPR !== currentDPR) { + setCurrentDPR(newDPR); + } + updateCanvasInfo(); + }, 100); + + return () => { + clearInterval(interval); + }; + }, [currentDPR, updateCanvasInfo]); + + // Load initial example + useEffect(() => { + const example = getCurrentExample(); + if (example) { + onExampleChange(example, interleaved); + } + }, []); // Only on mount + + // Handle example or interleaved changes + useEffect(() => { + const example = getCurrentExample(); + if (example) { + onExampleChange(example, interleaved); + } + }, [selectedExample, interleaved, getCurrentExample, onExampleChange]); + + const example = getCurrentExample(); + + return ( +
+

Basemap Browser

+ +
+
Select Example:
+ +
+ +
+ +
+ +
+

Current State

+
+ Framework: {example?.framework || 'N/A'} +
+
+ Map Type: {example?.mapType || 'N/A'} +
+
+ Interleaved: {interleaved ? 'true' : 'false'} +
+
+ Device Pixel Ratio: {currentDPR.toFixed(2)} +
+ {canvasSize.width > 0 && ( + <> +
+ Canvas Size: {canvasSize.width} x {canvasSize.height} +
+
+ Canvas Client Size: {canvasSize.clientWidth} x {canvasSize.clientHeight} +
+ {canvasSize.drawingBufferWidth && canvasSize.drawingBufferHeight && ( +
+ Drawing Buffer: {canvasSize.drawingBufferWidth} x{' '} + {canvasSize.drawingBufferHeight} +
+ )} + + )} +
+ +
+

Testing Instructions

+
+

+ Test Window Resize: Resize browser window and verify layers redraw correctly. + Watch canvas and drawing buffer dimensions update. +

+

+ Test DPR Change: Move browser window between screens with different pixel ratios + and verify layers scale correctly. Drawing buffer should adjust based on DPR. +

+

+ Test Interleaved vs Overlaid: Toggle interleaved mode and verify both work + correctly. +

+
+
+
+ ); +} diff --git a/examples/basemap-browser/src/examples-pure-js/google-maps.ts b/examples/basemap-browser/src/examples-pure-js/google-maps.ts new file mode 100644 index 00000000000..98abb412eb5 --- /dev/null +++ b/examples/basemap-browser/src/examples-pure-js/google-maps.ts @@ -0,0 +1,104 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {GoogleMapsOverlay} from '@deck.gl/google-maps'; +import type {Layer} from '@deck.gl/core'; + +// Track if we're loading the API +let loadingPromise: Promise | null = null; + +function loadGoogleMapsAPI(apiKey: string): Promise { + // If already loaded, return immediately + if ((window as any).google?.maps) { + return Promise.resolve((window as any).google.maps); + } + + // If already loading, return existing promise + if (loadingPromise) { + return loadingPromise; + } + + // Load the script manually + loadingPromise = new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&v=weekly&libraries=maps`; + script.async = true; + script.defer = true; + + script.onload = () => { + if ((window as any).google?.maps) { + resolve((window as any).google.maps); + } else { + reject(new Error('Google Maps API loaded but google.maps not found')); + } + }; + + script.onerror = () => { + loadingPromise = null; + reject(new Error('Failed to load Google Maps API script')); + }; + + document.head.appendChild(script); + }); + + return loadingPromise; +} + +export function mount( + container: HTMLElement, + getLayers: (interleaved?: boolean) => Layer[], + initialViewState: any, + interleaved: boolean +): () => void { + // eslint-disable-next-line no-process-env + const apiKey = process.env.GoogleMapsAPIKey; + // eslint-disable-next-line no-process-env + const mapId = process.env.GoogleMapsMapId; + + if (!apiKey) { + container.innerHTML = ` +
+

Google Maps Configuration Required

+

Set GoogleMapsAPIKey and GoogleMapsMapId environment variables.

+
+ `; + return () => {}; + } + + let overlay: GoogleMapsOverlay | null = null; + + // Load the API and create the map + loadGoogleMapsAPI(apiKey) + .then((googlemaps: any) => { + const map = new googlemaps.Map(container, { + center: {lat: initialViewState.latitude, lng: initialViewState.longitude}, + zoom: initialViewState.zoom, + heading: initialViewState.bearing || 0, + tilt: initialViewState.pitch || 0, + mapId + }); + + overlay = new GoogleMapsOverlay({ + interleaved, + layers: getLayers(interleaved) + }); + + overlay.setMap(map); + }) + .catch((error: Error) => { + container.innerHTML = ` +
+

Google Maps Error

+

${error.message}

+
+ `; + }); + + return () => { + if (overlay) { + overlay.finalize(); + overlay = null; + } + }; +} diff --git a/examples/basemap-browser/src/examples-pure-js/index.ts b/examples/basemap-browser/src/examples-pure-js/index.ts new file mode 100644 index 00000000000..0a62bc7cda4 --- /dev/null +++ b/examples/basemap-browser/src/examples-pure-js/index.ts @@ -0,0 +1,9 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import * as googleMaps from './google-maps'; +import * as mapbox from './mapbox'; +import * as maplibre from './maplibre'; + +export {googleMaps, mapbox, maplibre}; diff --git a/examples/basemap-browser/src/examples-pure-js/mapbox.ts b/examples/basemap-browser/src/examples-pure-js/mapbox.ts new file mode 100644 index 00000000000..cc3232f3cc4 --- /dev/null +++ b/examples/basemap-browser/src/examples-pure-js/mapbox.ts @@ -0,0 +1,52 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {MapboxOverlay} from '@deck.gl/mapbox'; +import mapboxgl from 'mapbox-gl'; +import type {Layer} from '@deck.gl/core'; + +export function mount( + container: HTMLElement, + getLayers: (interleaved?: boolean) => Layer[], + initialViewState: any, + mapStyle: string, + interleaved: boolean +): () => void { + // eslint-disable-next-line no-process-env + const mapboxToken = process.env.MapboxAccessToken; + + if (!mapboxToken) { + container.innerHTML = ` +
+

Mapbox Configuration Required

+

Set MapboxAccessToken environment variable.

+
+ `; + return () => {}; + } + + (mapboxgl as any).accessToken = mapboxToken; + + const map = new mapboxgl.Map({ + container, + style: mapStyle, + center: [initialViewState.longitude, initialViewState.latitude], + zoom: initialViewState.zoom, + bearing: initialViewState.bearing || 0, + pitch: initialViewState.pitch || 0 + }); + + const deckOverlay = new MapboxOverlay({ + interleaved, + layers: getLayers(interleaved) + }); + + map.addControl(deckOverlay as any); + map.addControl(new mapboxgl.NavigationControl()); + + return () => { + deckOverlay.finalize(); + map.remove(); + }; +} diff --git a/examples/basemap-browser/src/examples-pure-js/maplibre.ts b/examples/basemap-browser/src/examples-pure-js/maplibre.ts new file mode 100644 index 00000000000..ee9fbcdd03c --- /dev/null +++ b/examples/basemap-browser/src/examples-pure-js/maplibre.ts @@ -0,0 +1,45 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {MapboxOverlay} from '@deck.gl/mapbox'; +import maplibregl from 'maplibre-gl'; +import type {Layer} from '@deck.gl/core'; + +// eslint-disable-next-line max-params +export function mount( + container: HTMLElement, + getLayers: (interleaved?: boolean) => Layer[], + initialViewState: any, + mapStyle: string, + interleaved: boolean, + globe?: boolean +): () => void { + const map = new maplibregl.Map({ + container, + style: mapStyle, + center: [initialViewState.longitude, initialViewState.latitude], + zoom: initialViewState.zoom, + bearing: initialViewState.bearing || 0, + pitch: initialViewState.pitch || 0 + }); + + const deckOverlay = new MapboxOverlay({ + interleaved, + layers: getLayers(interleaved) + }); + + map.on('load', () => { + // Set projection before adding overlay (critical for globe + interleaved mode) + if (globe) { + map.setProjection({type: 'globe'} as any); + } + map.addControl(deckOverlay as any); + map.addControl(new maplibregl.NavigationControl()); + }); + + return () => { + deckOverlay.finalize(); + map.remove(); + }; +} diff --git a/examples/basemap-browser/src/examples-react/google-maps-component.tsx b/examples/basemap-browser/src/examples-react/google-maps-component.tsx new file mode 100644 index 00000000000..5e01c6d2e08 --- /dev/null +++ b/examples/basemap-browser/src/examples-react/google-maps-component.tsx @@ -0,0 +1,60 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import React, {useEffect, useMemo} from 'react'; +import {APIProvider, Map as GoogleMap, useMap} from '@vis.gl/react-google-maps'; +import {GoogleMapsOverlay} from '@deck.gl/google-maps'; +import type {BasemapExample} from '../types'; + +// Google Maps DeckGL Overlay - from get-started/react/google-maps +function GoogleMapsDeckOverlay({ + getLayers, + interleaved +}: { + getLayers: (interleaved?: boolean) => any[]; + interleaved: boolean; +}) { + const map = useMap(); + const overlay = useMemo(() => new GoogleMapsOverlay({interleaved}), [interleaved]); + + useEffect(() => { + if (map) { + overlay.setMap(map); + } + return () => overlay.setMap(null); + }, [map, overlay]); + + overlay.setProps({layers: getLayers(interleaved)}); + return null; +} + +type GoogleMapsComponentProps = { + example: BasemapExample; + interleaved: boolean; +}; + +export default function GoogleMapsComponent({example, interleaved}: GoogleMapsComponentProps) { + // eslint-disable-next-line no-process-env + const apiKey = process.env.GoogleMapsAPIKey!; + // eslint-disable-next-line no-process-env + const mapId = process.env.GoogleMapsMapId || 'DEMO_MAP_ID'; + const {initialViewState, getLayers} = example; + + return ( +
+ + + + + +
+ ); +} diff --git a/examples/basemap-browser/src/examples-react/index.ts b/examples/basemap-browser/src/examples-react/index.ts new file mode 100644 index 00000000000..df817b37a9f --- /dev/null +++ b/examples/basemap-browser/src/examples-react/index.ts @@ -0,0 +1,21 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import type {MapType} from '../types'; +import GoogleMapsComponent from './google-maps-component'; +import MapboxComponent from './mapbox-component'; +import MapLibreComponent from './maplibre-component'; + +export function getComponent(mapType: MapType) { + switch (mapType) { + case 'google-maps': + return GoogleMapsComponent; + case 'mapbox': + return MapboxComponent; + case 'maplibre': + return MapLibreComponent; + default: + throw new Error(`Unknown map type: ${mapType}`); + } +} diff --git a/examples/basemap-browser/src/examples-react/mapbox-component.tsx b/examples/basemap-browser/src/examples-react/mapbox-component.tsx new file mode 100644 index 00000000000..1859d155804 --- /dev/null +++ b/examples/basemap-browser/src/examples-react/mapbox-component.tsx @@ -0,0 +1,44 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import React from 'react'; +import {Map as MapboxMap, useControl as useMapboxControl} from 'react-map-gl/mapbox'; +import {MapboxOverlay} from '@deck.gl/mapbox'; +import type {BasemapExample} from '../types'; +import type {MapboxOverlayProps} from '@deck.gl/mapbox'; + +import 'mapbox-gl/dist/mapbox-gl.css'; + +// Set your Mapbox token here or via environment variable +// eslint-disable-next-line no-process-env +const MAPBOX_TOKEN = process.env.MapboxAccessToken; + +// Mapbox Overlay wrapper +function MapboxOverlayWrapper(props: MapboxOverlayProps & {interleaved: boolean}) { + const overlay = useMapboxControl(() => new MapboxOverlay(props)); + overlay.setProps(props); + return null; +} + +type MapboxComponentProps = { + example: BasemapExample; + interleaved: boolean; +}; + +export default function MapboxComponent({example, interleaved}: MapboxComponentProps) { + const {mapStyle, initialViewState, getLayers} = example; + + return ( +
+ + + +
+ ); +} diff --git a/examples/basemap-browser/src/examples-react/maplibre-component.tsx b/examples/basemap-browser/src/examples-react/maplibre-component.tsx new file mode 100644 index 00000000000..714809d339a --- /dev/null +++ b/examples/basemap-browser/src/examples-react/maplibre-component.tsx @@ -0,0 +1,49 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import React from 'react'; +import {Map as MapLibreMap, useControl as useMapLibreControl} from 'react-map-gl/maplibre'; +import {MapboxOverlay} from '@deck.gl/mapbox'; +import type {BasemapExample} from '../types'; +import type {MapboxOverlayProps} from '@deck.gl/mapbox'; + +import 'maplibre-gl/dist/maplibre-gl.css'; + +// MapLibre Overlay wrapper +function MapLibreOverlay(props: MapboxOverlayProps & {interleaved: boolean}) { + const overlay = useMapLibreControl(() => new MapboxOverlay(props)); + overlay.setProps(props); + return null; +} + +type MapLibreComponentProps = { + example: BasemapExample; + interleaved: boolean; +}; + +export default function MapLibreComponent({example, interleaved}: MapLibreComponentProps) { + const {mapStyle, initialViewState, getLayers, globe} = example; + const [overlayReady, setOverlayReady] = React.useState(!globe); + + return ( +
+ { + if (globe) { + // Set projection before rendering overlay (critical for globe + interleaved mode) + e.target.setProjection({type: 'globe'}); + setOverlayReady(true); + } + }} + > + {overlayReady && ( + + )} + +
+ ); +} diff --git a/examples/basemap-browser/src/examples/index.ts b/examples/basemap-browser/src/examples/index.ts new file mode 100644 index 00000000000..9617cd79f4a --- /dev/null +++ b/examples/basemap-browser/src/examples/index.ts @@ -0,0 +1,131 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import type {ExampleCategories} from '../types'; +import {getAirportLayers, getAirportLayersWithGlobe} from '../layers'; +import {MAPBOX_STYLE, MAPLIBRE_STYLE} from '../constants'; + +// Configuration matching get-started examples +const EXAMPLES: ExampleCategories = { + 'Google Maps': { + 'Google Maps Pure JS': { + name: 'Google Maps Pure JS', + mapType: 'google-maps', + framework: 'pure-js', + initialViewState: { + latitude: 51.47, + longitude: 0.45, + zoom: 4, + bearing: 0, + pitch: 30 + }, + getLayers: interleaved => getAirportLayers(interleaved, 'google-maps') + }, + 'Google Maps React': { + name: 'Google Maps React', + mapType: 'google-maps', + framework: 'react', + initialViewState: { + latitude: 51.47, + longitude: 0.45, + zoom: 4, + bearing: 0, + pitch: 30 + }, + getLayers: interleaved => getAirportLayers(interleaved, 'google-maps') + } + }, + Mapbox: { + 'Mapbox Pure JS': { + name: 'Mapbox Pure JS', + mapType: 'mapbox', + framework: 'pure-js', + mapStyle: MAPBOX_STYLE, + initialViewState: { + latitude: 51.47, + longitude: 0.45, + zoom: 4, + bearing: 0, + pitch: 30 + }, + getLayers: interleaved => getAirportLayers(interleaved, 'mapbox') + }, + 'Mapbox React': { + name: 'Mapbox React', + mapType: 'mapbox', + framework: 'react', + mapStyle: MAPBOX_STYLE, + initialViewState: { + latitude: 51.47, + longitude: 0.45, + zoom: 4, + bearing: 0, + pitch: 30 + }, + getLayers: interleaved => getAirportLayers(interleaved, 'mapbox') + } + }, + MapLibre: { + 'MapLibre Pure JS': { + name: 'MapLibre Pure JS', + mapType: 'maplibre', + framework: 'pure-js', + mapStyle: MAPLIBRE_STYLE, + initialViewState: { + latitude: 51.47, + longitude: 0.45, + zoom: 4, + bearing: 0, + pitch: 30 + }, + getLayers: interleaved => getAirportLayers(interleaved, 'maplibre') + }, + 'MapLibre React': { + name: 'MapLibre React', + mapType: 'maplibre', + framework: 'react', + mapStyle: MAPLIBRE_STYLE, + initialViewState: { + latitude: 51.47, + longitude: 0.45, + zoom: 4, + bearing: 0, + pitch: 30 + }, + getLayers: interleaved => getAirportLayers(interleaved, 'maplibre') + }, + 'MapLibre Globe Pure JS': { + name: 'MapLibre Globe Pure JS', + mapType: 'maplibre', + framework: 'pure-js', + mapStyle: MAPLIBRE_STYLE, + globe: true, + initialViewState: { + latitude: 51.47, + longitude: 0.45, + zoom: 0, + bearing: 0, + pitch: 0 + }, + getLayers: interleaved => getAirportLayersWithGlobe(interleaved, 'maplibre') + }, + 'MapLibre Globe React': { + name: 'MapLibre Globe React', + mapType: 'maplibre', + framework: 'react', + mapStyle: MAPLIBRE_STYLE, + globe: true, + initialViewState: { + latitude: 51.47, + longitude: 0.45, + zoom: 0, + bearing: 0, + pitch: 0 + }, + getLayers: interleaved => getAirportLayersWithGlobe(interleaved, 'maplibre') + } + } +}; + +export default EXAMPLES; diff --git a/examples/basemap-browser/src/index.tsx b/examples/basemap-browser/src/index.tsx new file mode 100644 index 00000000000..891dfc038a2 --- /dev/null +++ b/examples/basemap-browser/src/index.tsx @@ -0,0 +1,89 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import React from 'react'; +import {createRoot, type Root} from 'react-dom/client'; +import ControlPanel from './control-panel'; +import type {BasemapExample} from './types'; +import * as pureJSExamples from './examples-pure-js'; +import * as reactExamples from './examples-react'; + +// Two separate React roots +const controlsDiv = document.getElementById('controls')!; +const mapDiv = document.getElementById('map')!; + +const controlRoot = createRoot(controlsDiv); + +// Track current map state +let currentMapCleanup: (() => void) | null = null; +let currentMapRoot: Root | null = null; + +// Load an example into the map div +function loadExample(example: BasemapExample, interleaved: boolean) { + // Defer cleanup to avoid synchronous unmount during React render + setTimeout(() => { + // Clean up previous + if (currentMapCleanup) { + currentMapCleanup(); + currentMapCleanup = null; + } + if (currentMapRoot) { + currentMapRoot.unmount(); + currentMapRoot = null; + } + + // Clear the map div + mapDiv.innerHTML = ''; + + // Mount new example + mountExample(example, interleaved); + }, 0); +} + +function mountExample(example: BasemapExample, interleaved: boolean) { + // Mount new example + if (example.framework === 'pure-js') { + // Pure JS mounts directly, no React involved + switch (example.mapType) { + case 'google-maps': + currentMapCleanup = pureJSExamples.googleMaps.mount( + mapDiv, + example.getLayers, + example.initialViewState, + interleaved + ); + break; + case 'mapbox': + currentMapCleanup = pureJSExamples.mapbox.mount( + mapDiv, + example.getLayers, + example.initialViewState, + example.mapStyle!, + interleaved + ); + break; + case 'maplibre': + currentMapCleanup = pureJSExamples.maplibre.mount( + mapDiv, + example.getLayers, + example.initialViewState, + example.mapStyle!, + interleaved, + example.globe + ); + break; + default: + // Unknown map type + break; + } + } else { + // React mounts to separate root + currentMapRoot = createRoot(mapDiv); + const Component = reactExamples.getComponent(example.mapType); + currentMapRoot.render(); + } +} + +// Render control panel (always React) +controlRoot.render(); diff --git a/examples/basemap-browser/src/layers.ts b/examples/basemap-browser/src/layers.ts new file mode 100644 index 00000000000..b01ef5ae0f8 --- /dev/null +++ b/examples/basemap-browser/src/layers.ts @@ -0,0 +1,84 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {GeoJsonLayer, ArcLayer} from '@deck.gl/layers'; +import type {Layer} from '@deck.gl/core'; +import {AIR_PORTS, LONDON_COORDINATES} from './constants'; +import type {MapType} from './types'; + +// Layer configurations from get-started examples +export function getAirportLayers(interleaved?: boolean, mapType?: MapType): Layer[] { + // In interleaved mode, render the layers under map labels + // Mapbox uses slot, MapLibre uses beforeId + const interleavedProps = interleaved + ? mapType === 'mapbox' + ? {slot: 'middle'} + : {beforeId: 'watername_ocean'} + : {}; + + return [ + new GeoJsonLayer({ + id: 'airports', + data: AIR_PORTS, + filled: true, + pointRadiusMinPixels: 2, + pointRadiusScale: 2000, + getPointRadius: (f: any) => 11 - f.properties.scalerank, + getFillColor: [200, 0, 80, 180], + pickable: true, + autoHighlight: true, + ...interleavedProps + }), + new ArcLayer({ + id: 'arcs', + data: AIR_PORTS, + dataTransform: (d: any) => d.features.filter((f: any) => f.properties.scalerank < 4), + getSourcePosition: () => LONDON_COORDINATES, + getTargetPosition: (f: any) => f.geometry.coordinates, + getSourceColor: [0, 128, 200], + getTargetColor: [200, 0, 80], + getWidth: 1, + ...interleavedProps + }) + ]; +} + +export function getAirportLayersWithGlobe(interleaved?: boolean, mapType?: MapType): Layer[] { + // In interleaved mode, render the layers under map labels + // Mapbox uses slot, MapLibre uses beforeId + const interleavedProps = interleaved + ? mapType === 'mapbox' + ? {slot: 'middle'} + : {beforeId: 'watername_ocean'} + : {}; + + return [ + new GeoJsonLayer({ + id: 'airports', + data: AIR_PORTS, + filled: true, + pointRadiusMinPixels: 2, + pointRadiusScale: 2000, + getPointRadius: (f: any) => 11 - f.properties.scalerank, + getFillColor: [200, 0, 80, 180], + pickable: true, + autoHighlight: true, + ...interleavedProps + }), + new ArcLayer({ + id: 'arcs', + data: AIR_PORTS, + parameters: { + cullMode: 'none' + }, + dataTransform: (d: any) => d.features.filter((f: any) => f.properties.scalerank < 4), + getSourcePosition: () => LONDON_COORDINATES, + getTargetPosition: (f: any) => f.geometry.coordinates, + getSourceColor: [0, 128, 200], + getTargetColor: [200, 0, 80], + getWidth: 1, + ...interleavedProps + }) + ]; +} diff --git a/examples/basemap-browser/src/types.ts b/examples/basemap-browser/src/types.ts new file mode 100644 index 00000000000..2b0f391120b --- /dev/null +++ b/examples/basemap-browser/src/types.ts @@ -0,0 +1,35 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import type {Layer} from '@deck.gl/core'; + +export type MapType = 'google-maps' | 'mapbox' | 'maplibre'; + +export type Framework = 'pure-js' | 'react'; + +export type InitialViewState = { + latitude: number; + longitude: number; + zoom: number; + bearing?: number; + pitch?: number; +}; + +export type BasemapExample = { + name: string; + mapType: MapType; + framework: Framework; + mapStyle?: string; + initialViewState: InitialViewState; + getLayers: (interleaved?: boolean) => Layer[]; + globe?: boolean; +}; + +export type ExampleCategory = { + [key: string]: BasemapExample; +}; + +export type ExampleCategories = { + [category: string]: ExampleCategory; +}; diff --git a/examples/basemap-browser/tsconfig.json b/examples/basemap-browser/tsconfig.json new file mode 100644 index 00000000000..26288273867 --- /dev/null +++ b/examples/basemap-browser/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "jsx": "react", + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true + }, + "include": ["src"] +}