Chapter 3: 3D Geographic Visualization#
Welcome back! In Chapter 1: Application Views, we learned how our application uses different "views" or screens to manage the user experience. In Chapter 2: User Input Handling (Files & Interaction), we explored how the application receives data and commands from the user, like uploading files or clicking buttons.
Now that we can get data into the application, how do we show it to the user in a meaningful way, especially when that data is related to geographic locations and needs to be displayed in 3D? This is where 3D Geographic Visualization comes in.
What's the Problem?#
Imagine you've uploaded a photo of a building, and our app has figured out where the photo was taken and possibly the outline of the building itself. Simply showing the building outline on a flat 2D map isn't enough. You want to see:
- The actual 3D shape of the building, maybe even extruded to its correct height.
- Where exactly on the map the photo was taken.
- Perhaps even see other related geographic data like a point cloud (a collection of 3D points representing a scanned area) overlaid on the scene.
- You might want to look at this scene from different angles: a traditional map view, a straight-down view (orthographic), or even from the perspective of where the photo was taken (first-person).
To do this, we need a powerful way to render geographic data, not just as simple points or lines, but as interactive 3D objects within a map context.
What is 3D Geographic Visualization?#
In this project, 3D Geographic Visualization refers to the process of taking geographic datasets (like coordinates, building outlines, point clouds) and displaying them as interactive 3D objects on a map. It's about creating a virtual scene where you can explore spatial data with depth and perspective.
Our project uses two main libraries to achieve this:
- Mapbox GL JS: This library provides the interactive base map layer. It handles showing the streets, terrain, satellite imagery, etc. Think of it as the canvas or the base world onto which we'll draw our data.
- Deck.gl: This is a powerful data visualization library built on WebGL. It's specifically designed for rendering large, complex datasets on maps. Deck.gl draws our custom data (buildings, icons, point clouds) on top of the Mapbox base map.
Deck.gl works using the concept of Layers. You define different types of data you want to visualize (buildings, icons, point clouds), and for each type, you create a specific Deck.gl Layer. Deck.gl then takes all these layers and renders them together to create the final visualization.
How it Works in This Project: The DeckglWrapper
#
The core component responsible for handling the 3D visualization is DeckglWrapper
(src/components/deckgl-wrapper.tsx
). This component acts as the bridge between our application's data and state (like which view is active) and the rendering power of Deck.gl and Mapbox.
The MapResultView
and MapShowcaseView
components (Chapter 1) are the ones that decide what to display. They gather the necessary data (like the processed GeoJSON from a photo upload or data fetched from an API) and then pass it down to DeckglWrapper
to handle the actual rendering.
Here's a simplified look at how it's used in MapResultView
:
// Inside MapResultView component in src/views/map-result-view.tsx (simplified)
// ... state and effect hooks ...
const [layers, setLayers] = useState<Layer[]>([]);
// ... other state and handler functions ...
useEffect(() => {
// This effect runs when 'geo' data changes (e.g., after a photo upload)
if (geo) {
// Use a helper function to create Deck.gl layers from the geo data
const buildingLayers = createBuilding(geo.geojson, geo.cameraGPSData);
// Update the state with the new list of layers
setLayers(buildingLayers);
// ... logic to set the map view state based on geo data ...
}
}, [geo]); // Dependency array: re-run this effect if 'geo' changes
return (
// ... other UI elements ...
<MapWrapper component="main">
{/* ... tooltip component ... */}
<DeckglWrapper
parentViewState={viewState} // Pass the camera view state
view={view} // Pass the desired view type ("map", "firstPerson", etc.)
layers={layers} // Pass the array of Deck.gl layers to render
onHover={onHoverHandler} // Pass a handler for mouse hover events on layers
/>
</MapWrapper>
// ... other UI elements ...
);
In this snippet:
MapResultView
keeps track of the Deck.gllayers
it needs to display using component state (useState
).- When the
geo
prop (containing processed geographic data) changes, auseEffect
hook runs. - Inside the effect, it calls a helper function
createBuilding
(more on this later) with the GeoJSON and camera data. This function returns an array of Deck.glLayer
objects. MapResultView
updates itslayers
state with the array received fromcreateBuilding
.- Crucially, in its
return
block,MapResultView
renders theDeckglWrapper
component and passes thelayers
state array, the currentview
type (Chapter 1), and the cameraviewState
(which determines the camera's position, zoom, pitch, etc.) as props.
This shows the pattern: Get data -> Create layers -> Pass layers and view settings to DeckglWrapper
.
Inside the DeckglWrapper
#
Now, let's peek inside DeckglWrapper
to see how it uses these props to render the 3D scene.
// Inside DeckglWrapper component in src/components/deckgl-wrapper.tsx (simplified)
import { DeckGL } from "@deck.gl/react";
import { MapView, FirstPersonView, MapController, FirstPersonController, Layer } from "@deck.gl/core";
import { TerrainLayer } from "@deck.gl/geo-layers";
// ... other imports and state ...
export const DeckglWrapper = ({
parentViewState, // View state passed from parent (MapResultView/MapShowcaseView)
view, // Current view type ("map", "firstPerson", etc.)
layers, // Array of custom data layers (buildings, icons, etc.)
onHover, // Hover handler
}: DeckglWrapperProps) => {
// State to manage the camera view
const [viewState, setViewState] = useState<MultiviewMapViewState>(/* initial view state */);
// Effect to update internal viewState when parentViewState changes
useEffect(() => {
if (parentViewState !== null) {
setViewState(parentViewState);
}
}, [parentViewState]);
// Effect to adjust pitch/maxPitch based on the selected view type
useEffect(() => {
// Logic to update viewState.mapView based on 'view' prop
// For example, set pitch to 0 and maxPitch to 60 for "orthographic" view
// ... simplified logic ...
setViewState(/* updated view state */);
}, [view]); // Re-run effect when 'view' prop changes
// Define the available camera views (Map view and First Person view)
const VIEWS = useMemo(
() =>
view === "map" || view === "orthographic"
? [new MapView({ id: "mapView", controller: MapController })] // Standard map controls
: [new FirstPersonView({ id: "firstPersonView", controller: FirstPersonController })], // First-person controls
[view] // Recreate views if the 'view' type changes
);
// Common layers like the base terrain
const [commonLayers, setCommonLayers] = useState<Layer[]>([]);
useEffect(() => {
// Add a TerrainLayer for base elevation
const deckglTerrainLayer = new TerrainLayer({ /* ... terrain layer config ... */ });
setCommonLayers([deckglTerrainLayer]);
}, []); // Empty dependency array: run only once on mount
// Handler for view state changes (user interaction like panning/zooming)
const onViewStateChangeHandler = (parameters: ViewStateChangeParameters) => {
const { viewState: deckViewState } = parameters;
// Update the corresponding part of the internal view state (mapView or firstPersonView)
// ... simplified logic ...
setViewState(/* updated view state */);
};
return (
<DeckGL
// Pass the currently active view state (mapView or firstPersonView) to DeckGL
viewState={
view === "map" || view === "orthographic"
? viewState.mapView
: viewState.firstPersonView
}
onViewStateChange={onViewStateChangeHandler} // Listen for user view changes
onHover={onHover} // Pass the hover handler
// Combine common layers (like terrain) with custom data layers (buildings, icons, etc.)
layers={[...commonLayers, ...layers]}
views={VIEWS} // Tell DeckGL which view types are available
// ... widgets like zoom, fullscreen, compass ...
// ... lighting effects ...
/>
);
};
Key things happening in DeckglWrapper
:
- It uses the main
<DeckGL>
component provided by@deck.gl/react
. This is the component that sets up the WebGL context and handles the rendering loop. - It maintains its own state (
viewState
) for the camera, but initializes it and keeps it in sync withparentViewState
passed from the parent component (MapResultView
orMapShowcaseView
). This allows the parent to control the initial view (e.g., centering on a specific building) while still letting the user interact with the map (panning, zooming). - It defines different
VIEWS
usingMapView
andFirstPersonView
classes from@deck.gl/core
. These objects tell Deck.gl how the camera should behave for different view types (e.g.,MapView
uses standard map controls,FirstPersonView
uses controls suitable for walking around). - It includes a base
TerrainLayer
to add realistic elevation to the map background. This is part of thecommonLayers
. - It passes the combined array of
commonLayers
and thelayers
prop (our custom data layers) to thelayers
prop of the<DeckGL>
component. This tells Deck.gl what to draw. - It passes the appropriate
viewState
(eitherviewState.mapView
orviewState.firstPersonView
based on theview
prop) to the<DeckGL>
component. This tells Deck.gl where the camera is and which way it's looking. - The
onViewStateChange
prop on<DeckGL>
is connected to ouronViewStateChangeHandler
function. Deck.gl calls this function whenever the user interacts with the map controls (pans, zooms). Our handler updates the internalviewState
so the camera position persists. - The
onHover
prop allows us to react when the user's mouse hovers over a pickable layer (like the building or camera icons).
Essentially, DeckglWrapper
is the conductor: it gets the data layers and desired perspective from the parent and uses Deck.gl to render them correctly on the Mapbox base map.
Creating the Layers: createBuilding
#
How do we turn raw geographic data, like a GeoJSON outline of a building, into Deck.gl Layer
objects? This is handled by helper functions like createBuilding
(src/utils/deckgl-utils.ts
).
Let's look at a simplified version of how createBuilding
might create just the extruded building polygon layer:
// Inside src/utils/deckgl-utils.ts (simplified)
import { PolygonLayer } from "@deck.gl/layers";
// ... other imports ...
export const createBuilding = (
buildingGeojson: any, // GeoJSON data for the building
cameraGPSData: any, // Data about the photo location
nameSuffix?: string // Optional suffix for layer IDs
): Layer[] => {
const layers: Layer[] = [];
// Get the building coordinates and height from the GeoJSON
const buildingCoordinates = buildingGeojson.features[0].geometry.coordinates[0];
const buildingHeight = parseFloat(buildingGeojson.features[0].properties?.relativeheightmaximum);
const baseElevation = parseFloat(buildingGeojson.features[0].properties!.absoluteheightminimum);
// Prepare the polygon data format expected by PolygonLayer
const polygonData = [
{
contour: buildingCoordinates.map((coord: any) => {
// Add base elevation to each coordinate
return [...coord, baseElevation];
}),
},
];
// Create a PolygonLayer for the extruded building
const storey = new PolygonLayer({
id: nameSuffix ? `geojson-storey-building-${nameSuffix}` : "geojson-storey-building",
data: polygonData, // The polygon data
extruded: true, // Enable 3D extrusion
wireframe: true, // Show the wireframe outline
getPolygon: (d) => d.contour, // How to get the polygon coordinates from the data object
getFillColor: [249, 180, 45, 255], // Color of the extruded polygon
getElevation: buildingHeight, // How high to extrude it
opacity: 1,
});
layers.push(storey);
// ... create other layers like ground footprint, camera icon, marker ...
return layers; // Return the array of created layers
};
This function takes the processed buildingGeojson
data. It extracts the coordinates and height. It then creates a PolygonLayer
instance from @deck.gl/layers
.
id
: A unique identifier for the layer.data
: The data array that the layer will visualize.extruded: true
: This is the key part that makes the polygon 3D. Instead of a flat shape, it will be extended vertically.getPolygon
: A function that tells the layer how to find the polygon coordinates within each data object.getElevation
: A function or value that tells the layer how high to extrude the polygon. We use thebuildingHeight
derived from the GeoJSON.
The function createBuilding
actually creates several layers (ground footprint, camera icon, marker) and returns them all in an array, which is then passed to DeckglWrapper
.
Similarly, other types of data are visualized using different layer types:
- Photo locations are shown using an
IconLayer
(@deck.gl/layers
) inMapShowcaseView
. ThegetIcon
function is configured to use the photo URL itself as the icon source. - Point cloud data from LAZ files is visualized using a
PointCloudLayer
(@deck.gl/layers
) also inMapShowcaseView
.
Camera Perspectives (Views)#
As mentioned, the application supports different ways of looking at the 3D scene. This is controlled by the view
prop passed to DeckglWrapper
and how DeckglWrapper
uses MapView
and FirstPersonView
.
View Type | Description | Controller | Typical Use Case |
---|---|---|---|
map |
Traditional map view, can pan, zoom, orbit, and tilt. | MapController |
General exploration, overview. |
orthographic |
Straight-down, parallel projection (no perspective distortion). Can pan/zoom. | MapController |
Measuring, architectural plans, top view. |
firstPerson |
View from a point within the scene, like walking around or from a camera. | FirstPersonController |
Immersive view, simulating photo perspective. |
The DeckglWrapper
switches between using MapView
or FirstPersonView
based on the view
prop, changing how user interaction (like mouse drag) affects the camera movement and how the scene is projected. The keyboard hook (Chapter 2) is an example of how the user can trigger these view changes.
Flow of Visualization#
Here's a simplified sequence diagram showing how data flows to become a 3D visualization after, say, processing geo data for a building:
sequenceDiagram
participant MapResultView
participant createBuilding
participant DeckglWrapper
participant DeckGL
MapResultView->MapResultView: Receives 'geo' data update
MapResultView->createBuilding: Calls createBuilding(geo.geojson, geo.cameraGPSData)
createBuilding->createBuilding: Creates PolygonLayer, IconLayer, etc.
createBuilding-->MapResultView: Returns array of Deck.gl Layers
MapResultView->MapResultView: Updates 'layers' state
MapResultView->DeckglWrapper: Renders DeckglWrapper with layers prop
DeckglWrapper->DeckglWrapper: Combines parent layers with common layers (terrain)
DeckglWrapper->DeckGL: Passes combined layers and active viewState to DeckGL component
DeckGL->Browser: Renders the 3D scene on the map
Note over DeckglWrapper, DeckGL: User interaction updates DeckGL view state, DeckglWrapper updates its state accordingly
This shows how MapResultView
prepares the data for visualization, createBuilding
translates that data into Deck.gl layers, and DeckglWrapper
uses the core DeckGL
component to display everything.
Conclusion#
In this chapter, we explored the concept of 3D Geographic Visualization in our project. We learned how Deck.gl is used to render complex data layers on top of a Mapbox GL JS base map. We saw that visualization is built using Layers (like PolygonLayer
for buildings, IconLayer
for photos, PointCloudLayer
for LAZ data) and how these layers are created from processed geographic data using helper functions like createBuilding
. Finally, we understood how the DeckglWrapper
component acts as the central orchestrator, managing the rendering and switching between different camera Views (map
, orthographic
, firstPerson
) based on user selection.
With the ability to handle user input and visualize geographic data in 3D, the next logical step is to display more detailed information about the buildings or objects being visualized.
Next Chapter: Building Information Display
Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/components/deckgl-wrapper.tsx), 2(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/types/map-view-state.ts), 3(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/utils/deckgl-utils.ts), 4(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/views/map-result-view.tsx), 5(https://github.com/buildvoc/mapbox-gl_deck.gl_turf.js-ts/blob/3d8a4a53d878db3324af6466e0f99e5fb072bbe7/src/views/map-showcase-view.tsx)