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:
- Manages the overall state for the comparison mode (e.g.,
compareButtonMode
,hasBeenCompared
,comparisonStats
). - Renders two instances of the
ComparisonSide
component (<ComparisonSide mode={mode} side={ComparisonSideMode.left} ... />
and<ComparisonSide mode={mode} side={ComparisonSideMode.right} ... />
), separated by a visualDevider
. - Manages the list of layers/active layer IDs for each side (
layersLeftSide
,activeLayersIdsLeftSide
,layersRightSide
,activeLayersIdsRightSide
). - Uses the application's global Redux state for the shared view state (
globalViewState = useAppSelector(selectViewState)
). This is key for synchronization. - Coordinates loading and statistics tracking using the
ComparisonLoadManager
.
Each ComparisonSide
component (src/components/comparison/comparison-side/comparison-side.tsx
):
- Receives props from the parent
Comparison
component, including itsside
(left
orright
), the currentmode
(withinLayer
oracrossLayers
), the layers/active layer IDs relevant to its side, and handlers for events likeonTilesetLoaded
oronChangeLayers
. - Renders a single instance of the map visualization wrapper (
DeckGlWrapper
orArcgisWrapper
), just like the main application view ([Chapter 2]). - Passes the appropriate layer list (
getLayers3d()
) and the globally sharedglobalViewState
to its map wrapper. - Handles map events (like
onTilesetLoad
) to update its internal state (e.g., tracking loaded tilesets) and calls the provided handlers (likeonTilesetLoaded
) to communicate back to the parentComparison
component and theComparisonLoadManager
. - Conditionally renders UI panels (Layers Panel,
ComparisonParamsPanel
,MemoryUsagePanel
) based on itsside
, the overallmode
, 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.
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)