# How mapStackController.js Handles Switching Between Different Basemaps in Cesium

> Learn how mapStackController js manages basemap switching in Cesium. Discover race protection and activation routines for 3D tiles and globe imagery.

- Repository: [Bilawal Sidhu/gods-eye-view](https://github.com/bilawalsidhu/gods-eye-view)
- Tags: how-to-guide
- Published: 2026-09-11

---

**`MapStackController` orchestrates basemap transitions by validating stack availability, implementing generation-counter race protection, and delegating to specialized activation routines for photorealistic 3D tiles or traditional globe imagery layers.**

The `MapStackController` class in the [bilawalsidhu/gods-eye-view](https://github.com/bilawalsidhu/gods-eye-view) repository serves as the central authority for managing Cesium viewer basemaps. It maintains a registry of available map stacks, handles asynchronous provider loading, and ensures UI state remains synchronized during transitions between satellite imagery, OSM, Esri, and Google Photorealistic 3D tilesets.

## Defining Available Basemap Stacks

At initialization, the controller loads a static array of basemap configurations from [`src/mapStackController.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/mapStackController.js) lines 19-57.

```javascript
const MAP_STACKS = [
  {
    id: 'google-photoreal',
    label: 'Google Photorealistic 3D Tiles',
    kind: 'photoreal',
    requiresGoogleTileset: true
  },
  {
    id: 'bing-aerial',
    label: 'Bing Maps Aerial',
    kind: 'globe',
    provider: 'bing'
  },
  // ... additional stacks
];

```

Each entry specifies metadata including `id`, `label`, `kind` (either **photoreal** or **globe**), and prerequisite flags such as `requiresIonToken` or `requiresGoogleTileset`.

## State Tracking and Race Condition Protection

The constructor (lines 90-110) initializes critical state variables:

- **`_activeId`**: Currently active stack identifier
- **`_switchGen`**: Monotonic generation counter for race protection
- **`_isSwitching`**: Boolean flag indicating transition in progress
- **`_imageryProviders`**: Cache for instantiated providers

**Generation-based cancellation** prevents stale async operations from overwriting newer selections. Every call to `setStack()` increments `_switchGen`, creating a unique token for that transition. Subsequent async callbacks verify their generation matches the current `_switchGen`; if not, they abort immediately (lines 214-218 and 292-294). This **M7 pattern** ensures that a slow-loading Esri provider cannot replace a more recently selected Bing basemap.

## The Core Switching API

The public `setStack(id, {silent})` method (lines 203-255) provides the primary interface for switching between different basemaps:

```javascript
await controller.setStack('bing-aerial');

```

**Execution flow:**
1. **Validation**: Checks `isStackAvailable(id)` (lines 195-201) to verify prerequisites (Ion tokens, Google tileset access)
2. **Generation increment**: Captures current generation number
3. **State transition**: Sets `_isSwitching = true` and emits "switching" event unless `silent` option is passed
4. **Delegation**: Routes to `_activatePhotoreal()` for 3D tiles or `_activateGlobeStack()` for imagery layers
5. **Error handling**: Catches failures and triggers fallback to Google Photorealistic 3D or OSM
6. **Finalization**: Updates `_activeId`, clears switching state, emits "ready" or "error" event

## Activating Photorealistic vs. Globe Stacks

The controller handles two distinct rendering modes:

### Photorealistic 3D Tiles (Google)

`_activatePhotoreal()` (lines 269-285) manages transitions to Google Photorealistic 3D Tiles:

- Hides existing imagery layers via `viewer.imageryLayers.removeAll()`
- Disables the Cesium globe by setting `viewer.scene.globe.show = false`
- Displays the Google tileset entity
- Leaves terrain configuration unchanged (photoreal stacks render their own elevation data)

### Traditional Globe Imagery (Bing, Esri, OSM)

`_activateGlobeStack()` (lines 288-305) handles standard imagery providers:

1. Retrieves or creates provider via `_getImageryProvider()` (lines 336-376)
2. Removes previous imagery layers
3. Instantiates new `ImageryLayer` with the provider
4. Synchronizes Esri attribution through `_syncEsriAttribution()` (lines 319-334)
5. Enables Cesium globe rendering
6. Applies appropriate terrain via `_setWorldTerrainEnabled()` (lines 441-458)

## Provider Caching and Error Recovery

`_getImageryProvider` implements intelligent caching to avoid reconstructing expensive tile providers. It supports:
- **Cesium Ion** imagery with token authentication
- **Esri** tiled map services with manual attribution injection
- **OpenStreetMap** as keyless fallback

### Esri Failure Handling

Because Cesium ignores the `credit` option for ArcGIS sources, `_syncEsriAttribution` manually adds static on-screen credits when Esri layers activate. If tile requests fail twice, `_watchEsriProvider` (lines 387-410) automatically triggers fallback to OSM, ensuring the map never remains blank.

### Terrain Selection

`_setWorldTerrainEnabled` toggles between:
- **Cesium World Terrain** (requires valid Ion token)
- **Re:Earth terrain** or `EllipsoidTerrainProvider` (keyless fallback)

This method also respects the generation counter, aborting terrain loads superseded by newer basemap selections.

## Querying Controller State

The `getState()` method (lines 258-267) returns a comprehensive snapshot:

```javascript
const state = controller.getState();
// Returns: { activeId, activeStack, stacks, status, error }

```

This enables UI components to build dropdown menus reflecting availability:

```javascript
const options = controller.getStacks().map(s => ({
  id: s.id,
  label: s.label,
  disabled: !s.available,
  title: s.unavailableReason || ''
}));

```

## Practical Implementation Examples

### Basic Basemap Switching

```javascript
import { MapStackController } from './src/mapStackController.js';

const controller = new MapStackController(viewer, {
  cesiumToken: import.meta.env.VITE_CESIUM_ION_TOKEN,
  onChange: (state) => console.log('Basemap changed:', state.activeStack.label),
  onError: (msg) => console.error('Map error:', msg)
});

// Switch to Bing Aerial imagery
await controller.setStack('bing-aerial');

```

### Reacting to State Changes

```javascript
controller._onChange = (state) => {
  document.getElementById('basemap-label').textContent = state.activeStack.label;
  document.getElementById('loading-indicator').style.display = 
    state.status === 'switching' ? 'block' : 'none';
};

```

### Handling Provider Failures

```javascript
controller._onError = (message, stackId) => {
  showToast(`Basemap error: ${message}`);
  // Controller automatically falls back to OSM or Google Photoreal
};

```

## Related Architecture Files

- **[`src/renderGovernor.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/renderGovernor.js)**: Supplies `governorRequestRender` to trigger Cesium redraws after layer switches
- **`src/keySetupCore.mjs`**: Generates human-readable requirement messages when Ion or Google credentials are missing, consumed by `isStackAvailable()`

## Summary

- **MAP_STACKS** defines all available basemaps with prerequisite metadata in [`src/mapStackController.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/src/mapStackController.js) lines 19-57
- **`setStack()`** provides the async public API for switching between different basemaps with validation and event emission
- **_switchGen** counter prevents race conditions when users rapidly switch between slow-loading providers
- **`_activatePhotoreal`** disables the globe and shows Google 3D tiles, while **`_activateGlobeStack`** manages traditional imagery layers with Esri attribution handling
- **Automatic fallback** to OSM or Google Photorealistic 3D occurs when providers fail or tokens are missing
- **Terrain synchronization** ensures elevation data matches the active basemap requirements

## Frequently Asked Questions

### How does the controller prevent basemap flickering during rapid switches?

The implementation uses a generation counter (`_switchGen`) that increments with every `setStack()` call. Each async operation (provider loading, terrain initialization) captures the generation number at invocation and validates it upon completion. If the generation has changed, the operation aborts, ensuring only the most recent basemap selection renders.

### What happens if the Esri basemap fails to load tiles?

[`mapStackController.js`](https://github.com/bilawalsidhu/gods-eye-view/blob/main/mapStackController.js) monitors Esri providers through `_watchEsriProvider` (lines 387-410). After two tile load failures, it automatically triggers a fallback to OpenStreetMap. Additionally, `_syncEsriAttribution` manually manages on-screen credits when Esri layers activate or deactivate, working around Cesium's limitation with ArcGIS credit options.

### Can I switch basemaps without triggering change events?

Yes. Pass `{ silent: true }` as the second argument to `setStack()`:

```javascript
await controller.setStack('esri-world-imagery', { silent: true });

```

This prevents the "switching" and "ready" events from firing, useful for programmatic initialization or background restoration of saved state.

### How do I check if a specific basemap is available before switching?

Call `isStackAvailable(id)` (lines 195-201) to verify prerequisites:

```javascript
const canUsePhotoreal = controller.isStackAvailable('google-photoreal');
// Returns false if Google tileset not configured, with reason in unavailableReason

```

This method checks for required Ion tokens, Google tileset access, and other dependencies defined in the stack configuration.