Skip to content

Chapter 6: Comparison Mode#

Welcome back to the loaders.gl-showcases tutorial! In Chapter 5: Bookmarks, we saw how the application uses its Global State Management to save and restore specific views and settings. Building on that ability to manage different application states, we'll now explore Comparison Mode.

What is Comparison Mode?#

Often when working with 3D data, you need to compare things side-by-side. Maybe you have two different versions of the same building model, or you want to see how a dataset looks with different rendering settings applied, or you simply want to compare performance metrics like loading times or memory usage between two scenarios.

Trying to do this by switching back and forth between views or loading and unloading layers manually would be difficult and slow. This is where Comparison Mode becomes invaluable.

As the concept description explains:

This is a specific mode of the application designed for comparing two different 3D datasets or different views of the same dataset side-by-side. It uses two synchronized map views and provides tools to help analyze the differences, such as loading times and memory usage statistics. It's like putting two versions of a blueprint next to each other to spot how they differ.

Comparison Mode sets up two separate, synchronized map views so you can easily see differences visually and analyze performance statistics for each side.

The Central Use Case: Comparing Two Versions of a Dataset#

A prime example is comparing two versions of a 3D tileset (like a city model). Perhaps a new version has been released, and you want to check: 1. Are there visual differences? Are buildings placed correctly? Are details missing or added? 2. How does the loading performance compare? Does the new version load faster or use less memory?

Comparison Mode allows you to load version A on the left side and version B on the right side, synchronize their camera views, and visually inspect them simultaneously. You can also use the built-in tools to see loading times and memory consumption for each side.

Key Concepts#

Comparison Mode is built around a few core ideas:

  • Two Map Views: The most visible aspect is having the main map area split into two distinct panels, each showing its own 3D scene.
  • Synchronization: The camera view (pan, zoom, tilt) is typically synchronized between the two map panels. Moving the camera in one view automatically updates the camera in the other view, ensuring you're looking at the same geographical area from the same angle on both sides.
  • Comparison Modes: The application supports comparing:
    • Across Layers: Loading different 3D datasets on the left and right sides. This is useful for comparing completely different models or versions.
    • Within Layer: Loading the same 3D dataset on both sides, but potentially applying different settings (like compression options, debug visualizations, or filtering) to each side. This helps compare the effect of settings rather than the datasets themselves.
  • Performance Statistics: Tools are available within Comparison Mode to display statistics like loading times and memory usage specifically for the layers loaded in each of the comparison panels.

How to Use Comparison Mode#

You typically enter Comparison Mode from the application's main interface, often via a specific route or button that triggers this mode. The main comparison view is handled by the Comparison page component (src/pages/comparison/comparison.tsx).

Starting and Stopping Comparison#

  • Look for a button labeled "Compare" or similar. Clicking this button will likely switch the application's layout to the two-panel comparison view.
  • Inside the comparison view, you'll see a distinct "Compare" button, likely in the center dividing the two panels. When in "Start" mode, clicking this button begins the comparison process, often tracking load times and other metrics. The button might change to "Comparing" or "Stop" during this process.
  • Clicking the button again (when it's showing "Comparing" or "Stop") will end the current comparison run and potentially show results, or return the button to the "Start" state, allowing you to begin a new comparison run.

The component handling this central button is CompareButton (src/components/comparison/compare-button/compare-button.tsx). It uses the compareButtonMode state (which can be CompareButtonMode.Start or CompareButtonMode.Comparing) to change its appearance and behavior.

Loading Data into Each Side#

How you load data depends on the comparison mode ("Across Layers" or "Within Layer").

  • Across Layers: Each side (left and right) acts like a miniature version of the main application view in terms of Layer Management. You'll see a Layers Panel available for the left side and potentially one for the right side. You use the "Insert Layer" functionality within each side's panel to load a different dataset into that specific side.
  • Within Layer: You load the same dataset into the left side using its Layers Panel. The right side will automatically use the same dataset. However, you'll use settings panels (like the ComparisonParamsPanel) available on each side to apply different rendering options or debug settings to the same layer on the left versus the right.

The code manages this by having separate state variables for layers on the left (layersLeftSide, activeLayersIdsLeftSide) and layers on the right (layersRightSide, activeLayersIdsRightSide) in the Comparison component, and passing the appropriate lists to the ComparisonSide components. For "Within Layer" mode, the right side might receive the same staticLayers as the left but different configuration props.

Analyzing Statistics#

Once a comparison run is complete (e.g., after clicking the "Compare" button and waiting for layers to load), panels become available on each side to view statistics like loading time and memory usage. These panels use components like MemoryUsagePanel (src/components/memory-usage-panel/memory-usage-panel.tsx) to display the collected data (comparisonStats stored in the Comparison component's state). The central CompareButton also gains a download icon to export these results (downloadStats).

Under the Hood: How Comparison Mode Works#

Comparison Mode is a great example of how the application orchestrates multiple components and leverages Global State Management.

The main Comparison component (src/pages/comparison/comparison.tsx) acts as the conductor. It:

  1. Manages the overall state for the comparison mode (e.g., compareButtonMode, hasBeenCompared, comparisonStats).
  2. Renders two instances of the ComparisonSide component (<ComparisonSide mode={mode} side={ComparisonSideMode.left} ... /> and <ComparisonSide mode={mode} side={ComparisonSideMode.right} ... />), separated by a visual Devider.
  3. Manages the list of layers/active layer IDs for each side (layersLeftSide, activeLayersIdsLeftSide, layersRightSide, activeLayersIdsRightSide).
  4. Uses the application's global Redux state for the shared view state (globalViewState = useAppSelector(selectViewState)). This is key for synchronization.
  5. Coordinates loading and statistics tracking using the ComparisonLoadManager.

Each ComparisonSide component (src/components/comparison/comparison-side/comparison-side.tsx):

  1. Receives props from the parent Comparison component, including its side (left or right), the current mode (withinLayer or acrossLayers), the layers/active layer IDs relevant to its side, and handlers for events like onTilesetLoaded or onChangeLayers.
  2. Renders a single instance of the map visualization wrapper (DeckGlWrapper or ArcgisWrapper), just like the main application view ([Chapter 2]).
  3. Passes the appropriate layer list (getLayers3d()) and the globally shared globalViewState to its map wrapper.
  4. Handles map events (like onTilesetLoad) to update its internal state (e.g., tracking loaded tilesets) and calls the provided handlers (like onTilesetLoaded) to communicate back to the parent Comparison component and the ComparisonLoadManager.
  5. Conditionally renders UI panels (Layers Panel, ComparisonParamsPanel, MemoryUsagePanel) based on its side, the overall mode, and the currently active button (activeButton).

Synchronization of the camera view is handled automatically because both map wrappers read from the same globalViewState in the Redux store. When the user interacts with either map, the map wrapper's onViewStateChange handler dispatches an action to update the viewState in the global state. This state change immediately notifies both ComparisonSide components, which in turn update their MapWrapper components with the new shared view state, causing both maps to move in sync.

Layer management for each side utilizes Redux as well. While the Comparison component manages the top-level lists (layersLeftSide, layersRightSide, activeLayersIdsLeftSide, activeLayersIdsRightSide), the visibility of individual sublayers within a dataset (especially for "Within Layer" comparison) is often managed by dispatching actions to Redux slices (like flattened-sublayers-slice.ts), ensuring that the state correctly reflects which sublayers are visible on which side.

Performance statistics are tracked during a comparison run by the ComparisonLoadManager (src/utils/comparison-load-manager.ts). When a tileset loads on either the left or right side, the onTilesetLoaded handler in ComparisonSide calls the manager's resolveLeftSide or resolveRightSide method, passing the relevant statistics. The manager records the loading time and stores the stats. Once both sides have finished loading, the manager dispatches a custom 'loaded' event. The Comparison component listens for this event and compiles the collected stats into the comparisonStats state variable, making them available for display in the MemoryUsagePanel.

Here's a simplified sequence diagram illustrating the process of starting a comparison and loading layers:

sequenceDiagram
    Participant User
    Participant CompareButton
    Participant ComparisonPage
    Participant ComparisonSideLeft
    Participant ComparisonSideRight
    Participant MapWrapperLeft
    Participant MapWrapperRight
    Participant ComparisonLoadManager
    Participant GlobalState

    User->>CompareButton: Clicks "Start Compare"
    CompareButton->>ComparisonPage: Calls toggleCompareButtonMode handler
    ComparisonPage->>ComparisonPage: Updates compareButtonMode state to "Comparing"
    ComparisonPage->>ComparisonLoadManager: Calls startLoading()
    ComparisonPage->>ComparisonPage: Updates loadNumber state (triggers load)
    ComparisonPage->>ComparisonSideLeft: Passes updated state & props (loadNumber, mode, layers, viewState)
    ComparisonPage->>ComparisonSideRight: Passes updated state & props (loadNumber, mode, layers, viewState)
    ComparisonSideLeft->>MapWrapperLeft: Renders/Updates with layers & viewState
    MapWrapperLeft->>MapWrapperLeft: Starts loading/rendering layers
    ComparisonSideRight->>MapWrapperRight: Renders/Updates with layers & viewState
    MapWrapperRight->>MapWrapperRight: Starts loading/rendering layers
    MapWrapperLeft->>ComparisonSideLeft: Calls onTilesetLoad/onTilesetLoaded when ready
    ComparisonSideLeft->>ComparisonPage: Calls onTilesetLoaded handler with stats
    ComparisonPage->>ComparisonLoadManager: Calls resolveLeftSide(stats)
    MapWrapperRight->>ComparisonSideRight: Calls onTilesetLoad/onTilesetLoaded when ready
    ComparisonSideRight->>ComparisonPage: Calls onTilesetLoaded handler with stats
    ComparisonPage->>ComparisonLoadManager: Calls resolveRightSide(stats)
    ComparisonLoadManager->>ComparisonPage: Dispatches 'loaded' event (if both resolved)
    ComparisonPage->>ComparisonPage: Compiles stats, updates comparisonStats state
    ComparisonPage->>ComparisonPage: Updates compareButtonMode state to "Start" (and hasBeenCompared)
    ComparisonPage->>ComparisonSideLeft: Passes updated hasBeenCompared state
    ComparisonPage->>ComparisonSideRight: Passes updated hasBeenCompared state
    ComparisonSideLeft->>User: MemoryUsagePanel becomes visible
    ComparisonSideRight->>User: MemoryUsagePanel becomes visible

Code Snippets#

Here's a simplified look at the Comparison page component rendering the two sides:

// Simplified snippet from src/pages/comparison/comparison.tsx
export const Comparison = ({ mode }: ComparisonPageProps) => {
  // ... state variables for layers, stats, button mode, etc. ...
  const loadManagerRef = useRef<ComparisonLoadManager>(
    new ComparisonLoadManager()
  );
  const globalViewState = useAppSelector(selectViewState); // Shared view state

  // ... useEffect hooks to manage state ...

  const toggleCompareButtonMode = () => {
    setCompareButtonMode((prev) => {
      if (prev === CompareButtonMode.Start) {
        loadManagerRef.current.startLoading(); // Start tracking time
        // ... update other states ...
        return CompareButtonMode.Comparing; // Change button state
      }
      loadManagerRef.current.stopLoading();
      return CompareButtonMode.Start; // Change button state back
    });
  };

  const onChangeLayersHandler = (
    layers: LayerExample[],
    activeIds: string[],
    side: ComparisonSideMode
  ) => {
    // Update the correct side's layer state
    if (side === ComparisonSideMode.left) {
      setLayersLeftSide(layers);
      setActiveLayersIdsLeftSide(activeIds);
    } else if (side === ComparisonSideMode.right) {
      setLayersRightSide(layers);
      setActiveLayersIdsRightSide(activeIds);
    }
    setPreventTransitions(false); // Allow smooth transitions after manual change
  };

  return (
    <Container $layout={layout}>
      {/* Left Comparison Side */}
      <ComparisonSide
        mode={mode}
        side={ComparisonSideMode.left}
        compareButtonMode={compareButtonMode}
        loadingTime={loadManagerRef.current.leftLoadingTime}
        hasBeenCompared={hasBeenCompared}
        showLayerOptions // Layers Panel visible on left
        showComparisonSettings={mode === ComparisonMode.withinLayer} // Settings panel visible if withinLayer
        staticLayers={layersLeftSide} // Pass layers specific to left side
        activeLayersIds={activeLayersIdsLeftSide} // Pass active layers for left side
        preventTransitions={preventTransitions}
        onTilesetLoaded={(stats: StatsMap) => {
          loadManagerRef.current.resolveLeftSide(stats); // Report left side load complete
          setLeftSideLoaded(true); // Indicate left side is ready
        }}
        onChangeLayers={(layers, activeIds) => {
          onChangeLayersHandler(layers, activeIds, ComparisonSideMode.left);
        }}
        // ... other props and handlers ...
        pointToTileset={pointToTileset} // Passed down for "Point to Layer" feature
      />

      {/* The Divider and Compare Button */}
      <Devider $layout={layout} />
      <CompareButton
        compareButtonMode={compareButtonMode}
        downloadStats={compareButtonMode === CompareButtonMode.Start && hasBeenCompared}
        disableButton={disableButton.includes(true)}
        disableDownloadButton={!hasBeenCompared}
        onCompareModeToggle={toggleCompareButtonMode}
        onDownloadClick={downloadClickHandler}
      />

      {/* Right Comparison Side */}
      <ComparisonSide
        mode={mode}
        side={ComparisonSideMode.right}
        compareButtonMode={compareButtonMode}
        loadingTime={loadManagerRef.current.rightLoadingTime}
        loadTileset={leftSideLoaded} // Right side waits for left in Across Layers mode
        hasBeenCompared={hasBeenCompared}
        showLayerOptions={mode === ComparisonMode.acrossLayers} // Layers Panel visible on right only if acrossLayers
        showComparisonSettings={mode === ComparisonMode.withinLayer} // Settings panel visible if withinLayer
        staticLayers={
          mode === ComparisonMode.withinLayer ? layersLeftSide : layersRightSide
        } // Right side uses left layers or its own based on mode
        activeLayersIds={
          mode === ComparisonMode.withinLayer
            ? activeLayersIdsLeftSide
            : activeLayersIdsRightSide
        } // Right side uses left active layers or its own based on mode
        preventTransitions={preventTransitions}
        // ... other props and handlers ...
        onTilesetLoaded={(stats: StatsMap) => {
          loadManagerRef.current.resolveRightSide(stats); // Report right side load complete
        }}
        onChangeLayers={(layers, activeIds) => {
          onChangeLayersHandler(layers, activeIds, ComparisonSideMode.right);
        }}
        pointToTileset={pointToTileset} // Passed down
      />

      {/* ... other panels like Bookmarks or Map Controls ... */}
    </Container>
  );
};

This shows how the Comparison page component orchestrates the two ComparisonSide components, passing them the necessary data and callbacks to manage their specific state and interactions, while relying on the globally managed viewState for synchronization. Notice how the staticLayers and activeLayersIds props passed to the right side depend on the mode, implementing the "Within Layer" vs "Across Layers" logic. The onTilesetLoaded handlers are crucial for the ComparisonLoadManager to track the loading process.

Inside each ComparisonSide, the component selects its relevant layer state using useAppSelector based on its side prop:

// Simplified snippet from src/components/comparison/comparison-side/comparison-side.tsx
export const ComparisonSide = ({
  mode,
  side,
  // ... other props ...
}: ComparisonSideProps) => {
  // Select layers/sublayers from Redux state based on which side this component represents
  const flattenedSublayers = useAppSelector(
    side === ComparisonSideMode.left ? selectLeftLayers : selectRightLayers
  );
  const bslSublayers = useAppSelector(
    side === ComparisonSideMode.left
      ? selectLeftSublayers
      : selectRightSublayers
  );
  const globalViewState = useAppSelector(selectViewState); // Get the shared global view state

  // ... state for active button, examples, loaded tilesets, etc. ...

  const getLayers3d = () => {
    // Prepare layers for the MapWrapper based on selected/visible layers for *this* side
    return flattenedSublayers
      .filter((sublayer) => sublayer.visibility)
      .map((sublayer) => ({
        id: sublayer.id,
        url: sublayer.url,
        // ... other layer properties ...
      }));
  };

  // ... event handlers like onTilesetLoadHandler, onTileLoad, etc. ...

  return (
    <Container $layout={layout}>
      {/* Render the Map Wrapper for this side */}
      <MapWrapper
        id={sideId} // Unique ID for this side's map container
        layers3d={getLayers3d()} // Pass layers specific to this side
        viewState={globalViewState.main} // Pass the SHARED global view state
        // onViewStateChange={...} // This handler in MapWrapper would dispatch action to update globalViewState
        // ... other props like onTilesetLoad, onAfterRender, etc. ...
      />
      {/* ... conditionally rendered panels based on mode, side, and activeButton ... */}
    </Container>
  );
};

This shows how the ComparisonSide component uses Redux selectors to get the layer data specific to its side (selectLeftLayers/selectRightLayers) and the shared global view state (selectViewState). It then prepares the layers and passes them, along with the shared view state, to its MapWrapper component instance, enabling the side-by-side display and synchronized camera movement. The rendering of panels within ComparisonSide further demonstrates how the UI is tailored based on the comparison mode and the side.

Conclusion#

In this chapter, we explored Comparison Mode, a specialized feature allowing side-by-side analysis of 3D datasets or views. We learned that it uses two synchronized map views, enabled by rendering two instances of the map component and sharing a single Global State for the camera view. We saw that the mode ("Within Layer" or "Across Layers") determines how layers and settings are applied to each side, and that the application tracks performance statistics during comparison runs using tools like ComparisonLoadManager. The main Comparison page component orchestrates the two ComparisonSide components, passing them the necessary data and handlers to display, interact with, and report on their respective views.

Now that we've seen how loaders.gl-showcases visualizes and compares data, including powerful features like synchronization and performance tracking, let's look at how it integrates with specific geospatial platforms, starting with ArcGIS.

Chapter 7: ArcGIS Integration


Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/components/comparison/compare-button/compare-button.tsx), 2(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/components/comparison/comparison-side/comparison-side.tsx), 3(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/pages/comparison/comparison.tsx), 4(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/utils/comparison-load-manager.ts)