Skip to content

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:

  1. Choose which books to put on the shelf (add layers/basemaps).
  2. Organize them.
  3. Decide which ones are open and readable (make layers/basemaps visible).
  4. 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:

  1. Layers: This section lists the 3D datasets (like building models, terrain data, etc.) that are currently available or visible on the map.
  2. 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:

  1. User Interaction: You click the checkbox next to a layer in the Layers Panel UI (ListItem).
  2. UI Component Event: The ListItem component detects the click and calls its onChange prop.
  3. Panel Handles Event: The LayersControlPanel receives the event and calls its onLayerSelect prop (passing the layer data).
  4. State Update Logic: The code that rendered LayersPanel and passed onLayerSelect as a prop handles this. This code typically uses the application's state management system (like Redux) to update the central list of selectedLayerIds. For example, it might dispatch an action like ADD_SELECTED_LAYER or REMOVE_SELECTED_LAYER.
  5. Global State Changes: The state management system updates the list of selectedLayerIds.
  6. 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 like DeckGlWrapper) re-renders.
  7. UI Reflects State: The LayersPanel receives the new list of selectedLayerIds as props. The LayersControlPanel gets these updated props, and the ListItem's selected prop is updated, causing the checkbox on the screen to visually reflect the layer's new selection state (checked or unchecked).
  8. Map Reflects State: Simultaneously, the Map Visualization component (DeckGlWrapper or ArcgisWrapper 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 updated selectedLayerIds list from the state.
  9. 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)