Chapter 3: Layer Management#
Welcome back to the loaders.gl-showcases
tutorial! In Chapter 2: Map Visualization, we explored the "stage" where our 3D geographical data is displayed using libraries like Deck.gl or ArcGIS. But how do we decide what data shows up on that stage? How do we swap one dataset for another, or look at a different background map? That's where Layer Management comes in.
What is Layer Management?#
Imagine your map visualization (Chapter 2) is a bookshelf. You have many different books (3D datasets, background maps, etc.) that you could put on the shelf. Layer Management is the system that lets you:
- Choose which books to put on the shelf (add layers/basemaps).
- Organize them.
- Decide which ones are open and readable (make layers/basemaps visible).
- Decide which ones to put away (remove layers/basemaps).
As the concept description says:
This abstraction handles adding, removing, and controlling the different 3D datasets (layers) and background maps (basemaps) you can see. It's like choosing which books to put on a bookshelf and deciding which ones are open and visible at any given time. You can load new datasets, switch between different layers, and sometimes even control parts within a single complex layer.
It's the core control center for the content you see on the map.
The Central Use Case: Controlling What You See#
A common task when exploring geographical data is switching between different views or datasets. For instance, you might want to:
- See a 3D model of buildings on top of satellite imagery.
- Switch to viewing the same buildings on a simple street map background.
- Hide the buildings temporarily to see just the terrain.
- Add a new 3D model (like a bridge) to the scene alongside the buildings.
Layer Management is the system that allows the application to handle these requests from the user and update the map visualization accordingly.
The Control Panel: The Layers Panel#
The main place you interact with Layer Management in the loaders.gl-showcases
application is through the Layers Panel. You can usually open it by clicking a button in the main interface. Let's look at the component responsible for this panel: src/components/layers-panel/layers-panel.tsx
.
This panel typically has two main sections, often presented as tabs:
- Layers: This section lists the 3D datasets (like building models, terrain data, etc.) that are currently available or visible on the map.
- Map Options: This section lets you choose the background map, also known as the basemap (like street maps, satellite imagery, topographic maps).
Let's see how these sections are represented in the LayersPanel
component structure:
// Simplified snippet from src/components/layers-panel/layers-panel.tsx
export const LayersPanel = ({
// ... props like layers, selectedLayerIds, basemaps, etc.
layers, // List of data layers
selectedLayerIds, // IDs of currently visible data layers
// ... basemap related props (handled via Redux hooks, see Chapter 4)
onLayerSelect, // Function to call when layer selection changes
onLayerInsert, // Function to call when adding a new layer
onLayerDelete, // Function to call when deleting a layer
// ... other props
}) => {
const [tab, setTab] = useState<Tabs>(Tabs.Layers); // State to track active tab
return (
<PanelContainer id={id} $layout={layout}>
<PanelHeader $panel={Panels.Layers}>
{/* Tabs to switch between Layers and Map Options */}
<Tab $active={tab === Tabs.Layers} onClick={() => setTab(Tabs.Layers)}>Layers</Tab>
<Tab $active={tab === Tabs.MapOptions} onClick={() => setTab(Tabs.MapOptions)}>Map Options</Tab>
</PanelHeader>
{/* ... Close button ... */}
<PanelHorizontalLine />
<PanelContent>
{tab === Tabs.Layers && (
// Component for the 'Layers' tab content
<LayersControlPanel
layers={layers} // Pass the list of layers
selectedLayerIds={selectedLayerIds} // Pass which layers are selected
onLayerSelect={onLayerSelect} // Pass the function to handle selection changes
onLayerInsertClick={() => setShowLayerInsertPanel(true)} // Handle Insert Layer button click
// ... other layer control props (delete, settings, etc.)
type={ListItemType.Checkbox} // Use checkboxes for data layers
onSceneInsertClick={() => setShowSceneInsertPanel(true)} // Handle Insert Scene button click
deleteLayer={onLayerDelete} // Pass the delete function
onPointToLayer={() => {}} // Simplified prop
hasSettings={false} // Simplified prop
onLayerSettingsClick={() => {}} // Simplified prop
/>
)}
{tab === Tabs.MapOptions && (
// Component for the 'Map Options' tab content
<MapOptionPanel
// ... props for map options
insertBaseMap={() => setShowInsertMapPanel(true)} // Handle Insert Base Map button click
pageId={"single"} // Simplified prop
/>
)}
</PanelContent>
{/* ... Modals for Insert Panels, Warnings, Layer Settings, etc. ... */}
</PanelContainer>
);
};
This simplified view shows that LayersPanel
acts as the container, managing which tab is visible and showing other related components like LayersControlPanel
(for data layers) and MapOptionPanel
(for basemaps). It receives the current list of layers and their selection status (layers
, selectedLayerIds
) via props, likely from the application's central state (Chapter 4). It also provides functions (onLayerSelect
, onLayerInsert
, etc.) that are called when the user interacts with the controls inside the panel.
Managing Data Layers (The "Layers" Tab)#
The LayersControlPanel
component (src/components/layers-panel/layers-control-panel.tsx
) is responsible for displaying the list of available data layers and allowing you to toggle their visibility.
Each individual entry in the list (representing a single layer or a group of layers) is rendered using the ListItem
component (src/components/layers-panel/list-item/list-item.tsx
).
Layer visibility is typically controlled using a checkbox for each layer. A group of layers might have a checkbox that controls all layers within that group, sometimes showing an "indeterminate" state if only some layers in the group are visible.
// Simplified snippet from src/components/layers-panel/layers-control-panel.tsx
export const LayersControlPanel = ({
layers, // The list of layers (LayerExample objects)
selectedLayerIds, // IDs of currently selected/visible layers
onLayerSelect, // Function to call when a layer is selected/unselected
// ... other props
}) => {
// Helper function to determine if a layer/group is selected (selected, unselected, indeterminate)
const isListItemSelected = (layer, parentLayer?) => {
// ... logic to check if layer.id is in selectedLayerIds ...
// ... logic for group selection state ...
};
// Recursive function to render the list, handling nested layers (groups)
const renderLayers = (layers: LayerExample[], parentLayer?) => {
return layers.map((layer: LayerExample) => {
const childLayers = layer?.layers ?? [];
const isSelected = isListItemSelected(layer, parentLayer); // Get selection state
return (
<Fragment key={layer.id}>
<ListItem
id={layer.id}
title={layer.name}
subtitle={layer.type}
type={parentLayer ? ListItemType.Checkbox : type} // Use checkbox for nested/leaf layers
selected={isSelected} // Pass the selection state
onChange={() => { onLayerSelect(layer); }} // Call onLayerSelect when clicked/changed
// ... other props for options menu, etc.
/>
{/* Recursively render child layers if they exist */}
{childLayers.length ? (
<ChildrenContainer>
{renderLayers(childLayers, layer)}
</ChildrenContainer>
) : null}
{/* ... Delete Confirmation modal ... */}
</Fragment>
);
});
};
return (
<LayersContainer>
{/* Render the list of layers */}
<LayersList>{renderLayers(layers)}</LayersList>
{/* Buttons to insert new layers/scenes */}
<InsertButtons>
<ActionIconButton Icon={PlusIcon} onClick={onLayerInsertClick}>Insert layer</ActionIconButton>
{/* ... Insert Scene button ... */}
</InsertButtons>
{/* ... potentially other elements ... */}
</LayersContainer>
);
};
The LayersControlPanel
component receives the layers
data (which can be nested to represent groups) and the selectedLayerIds
(a list of IDs of layers that should currently be visible). It then renders a ListItem
for each layer. The ListItem
component displays the layer's name and type and includes a checkbox whose state (selected
) is determined by the isListItemSelected
function based on the selectedLayerIds
.
When you click a checkbox next to a layer's name, the ListItem
component calls its onChange
prop, which in turn calls the onLayerSelect
function provided by LayersPanel
. This onLayerSelect
function is crucial β it's the bridge that tells the rest of the application that a layer's visibility needs to change.
You also see "Insert layer" and "Insert scene" buttons. Clicking these opens a modal (handled within LayersPanel
) allowing you to specify the URL or file for a new dataset to add to the list of available layers. The onLayerInsert
function (passed to LayersPanel
) is called once the new layer data is provided, which adds it to the application's state. The "Delete layer" option (accessible via the three-dot menu on each list item) calls onLayerDelete
to remove a layer.
Managing Basemaps (The "Map Options" Tab)#
The MapOptionPanel
component (src/components/layers-panel/map-options-panel.tsx
) handles the selection of the background map. It typically lists different available basemap styles (like satellite, street, etc.) and allows you to choose one.
// Simplified snippet from src/components/layers-panel/map-options-panel.tsx
export const MapOptionPanel = ({
pageId,
insertBaseMap, // Function to call when adding a custom basemap
}) => {
return (
<MapOptionsContainer id="map-options-container">
<MapOptionTitle>Base Map</MapOptionTitle>
{/* Component listing available basemaps */}
<BasemapListPanel group={BaseMapGroup.Maplibre} />
{pageId !== PageId.comparison && (
<BasemapListPanel group={BaseMapGroup.ArcGIS} />
)}
{pageId !== PageId.comparison && (
<BasemapListPanel group={BaseMapGroup.Terrain} />
)}
{/* Button to insert a custom basemap */}
<InsertButtons>
<ActionIconButton Icon={PlusIcon} size={ButtonSize.Small} onClick={insertBaseMap}>
Insert Base Map
</ActionIconButton>
</InsertButtons>
</MapOptionsContainer>
);
};
The MapOptionPanel
uses BasemapListPanel
to display different categories of basemaps. Inside BasemapListPanel
, individual basemap options are also represented using ListItem
, but this time with type={ListItemType.Radio}
. Radio buttons ensure that only one basemap can be selected at a time within a group (or globally, depending on implementation).
When you select a basemap using its radio button, the ListItem
calls its onChange
prop. This call eventually leads to an action being dispatched (we'll see how in Chapter 4: Global State Management) to update the application's state, marking that specific basemap as the currently selected one.
The "Insert Base Map" button functions similarly to "Insert layer," allowing you to add a custom basemap URL to the available options. This also updates the application's state via the insertBaseMap
function.
Under the Hood: Connecting Layers Panel to the Map#
How does clicking a checkbox in the Layers Panel actually change what you see on the map visualization (Chapter 2)? The answer lies in Global State Management, which we'll cover in detail in the next chapter.
Here's a simplified flow:
- User Interaction: You click the checkbox next to a layer in the Layers Panel UI (
ListItem
). - UI Component Event: The
ListItem
component detects the click and calls itsonChange
prop. - Panel Handles Event: The
LayersControlPanel
receives the event and calls itsonLayerSelect
prop (passing the layer data). - State Update Logic: The code that rendered
LayersPanel
and passedonLayerSelect
as a prop handles this. This code typically uses the application's state management system (like Redux) to update the central list ofselectedLayerIds
. For example, it might dispatch an action likeADD_SELECTED_LAYER
orREMOVE_SELECTED_LAYER
. - Global State Changes: The state management system updates the list of
selectedLayerIds
. - Application Re-renders: Because the application's state has changed, the main part of the application that renders the user interface (including the
LayersPanel
and the Map Visualization components likeDeckGlWrapper
) re-renders. - UI Reflects State: The
LayersPanel
receives the new list ofselectedLayerIds
as props. TheLayersControlPanel
gets these updated props, and theListItem
'sselected
prop is updated, causing the checkbox on the screen to visually reflect the layer's new selection state (checked or unchecked). - Map Reflects State: Simultaneously, the Map Visualization component (
DeckGlWrapper
orArcgisWrapper
from Chapter 2) also receives updated props based on the new state. Specifically, the list of layers to display (layers3d
prop) is rebuilt based on the updatedselectedLayerIds
list from the state. - Map Rendering Update: The Map Visualization component tells the underlying library (Deck.gl or ArcGIS) to render the scene using the new list of layers. If a layer was added to the
selectedLayerIds
, it's now included in the list passed to the map library and becomes visible. If it was removed, it's no longer included and disappears from the map.
Here's a simplified sequence diagram:
sequenceDiagram
Participant User
Participant LayersPanelUI
Participant GlobalState
Participant MapVisualization
User->>LayersPanelUI: Clicks layer checkbox
LayersPanelUI->>LayersPanelUI: ListItem calls onChange -> LayersControlPanel calls onLayerSelect
LayersPanelUI->>GlobalState: Dispatches action to update selected layer IDs
GlobalState->>GlobalState: Updates the list of selected layer IDs
GlobalState-->>LayersPanelUI: State update triggers UI re-render
GlobalState-->>MapVisualization: State update triggers Map re-render (with new layer list)
LayersPanelUI->>User: Checkbox state updates visually
MapVisualization->>User: Map displays/hides the layer
The actual management of the layer list and selected IDs happens in the application's state management logic. For instance, the base-maps-slice.ts
file shows how basemaps are stored and how actions like setSelectedBaseMap
update which basemap ID is selected. Similarly, other state slices (like flattened-sublayers-slice.ts
for sublayer visibility, or a potential layers-slice
if one exists for top-level layers) manage the state for data layers.
The key takeaway is that the Layers Panel UI components are primarily responsible for displaying the current state and triggering updates to that state based on user interaction. The Map Visualization components are responsible for reading that state and rendering the appropriate layers.
Managing Parts within a Layer (Sublayers)#
Some complex datasets, like Building Scene Layers (BSL), are not just a single block of data. They can contain different categories or "sublayers" within them (e.g., walls, roofs, structural elements). Layer Management extends to controlling the visibility of these individual sublayers within a single main layer.
If a selected layer has sublayers, clicking the options menu (three dots) might open a Layer Settings panel (src/components/layers-panel/layer-settings-panel.tsx
). This panel often uses components like SublayerWidget
(src/components/layers-panel/sublayer-widget.tsx
) to list and control the visibility of these internal sublayers.
// Simplified snippet from src/components/layers-panel/sublayer-widget.tsx
export const SublayerWidget = ({
sublayer, // An object representing the sublayer
onUpdateSublayerVisibility, // Function to call when sublayer visibility changes
// ... other props
}) => {
// ... logic to determine selection state (selected, unselected, indeterminate) ...
const selected = getSelectedState(sublayer);
const toggleSublayer = () => {
// Logic to update the sublayer's visibility property
sublayer.setVisibility(!(sublayer.visibility ?? false));
// Call the handler to signal that state needs updating
onUpdateSublayerVisibility(sublayer);
};
return (
<GroupContainer key={sublayer.id} needIndentation={hasParent}>
<ListItem
id={sublayer.id.toString()}
title={sublayer.name}
type={ListItemType.Checkbox} // Use checkbox for sublayers too
selected={selected} // Pass selection state
onChange={() => { toggleSublayer(); }} // Call toggleSublayer when clicked
// ... other props for expanding groups of sublayers ...
/>
{/* Recursively render child sublayers if they exist */}
{/* ... expanded && childLayers.map(...) ... */}
</GroupContainer>
);
};
The SublayerWidget
works much like ListItem
and LayersControlPanel
, but for sublayers. It takes a sublayer
object and a function onUpdateSublayerVisibility
. When you click the checkbox for a sublayer, toggleSublayer
updates the sublayer's internal visibility flag and calls onUpdateSublayerVisibility
, which again triggers a state update and subsequent re-rendering of the map to show/hide that specific part of the main layer.
Conclusion#
In this chapter, we learned about Layer Management, the system that allows us to control which datasets and background maps are visible on the map visualization. We saw that the Layers Panel is the primary interface for this, using components like LayersControlPanel
, MapOptionPanel
, and ListItem
to display lists and controls for data layers and basemaps. We understood that interacting with these controls triggers updates that are handled by the application's central state management, which in turn tells the Map Visualization components what to display. We also briefly touched upon managing sublayers within complex datasets.
This leads us directly to the next crucial concept: Global State Management. How does the application keep track of which layers are selected, which basemap is active, and other important settings that affect the entire application? We'll explore this in the next chapter.
Chapter 4: Global State Management
Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/components/layers-panel/layers-control-panel.tsx), 2(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/components/layers-panel/layers-panel.tsx), 3(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/components/layers-panel/list-item/list-item.tsx), 4(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/components/layers-panel/map-options-panel.tsx), 5(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/components/layers-panel/sublayer-widget.tsx), 6(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/redux/slices/base-maps-slice.ts), 7(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/redux/slices/flattened-sublayers-slice.ts), 8(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/utils/layer-utils.ts)