Skip to content

Chapter 5: Bookmarks#

Welcome back to the loaders.gl-showcases tutorial! In Chapter 4: Global State Management, we learned about the central "control room" of the application, where important information like the camera view and visible layers is stored and kept consistent. Now, we'll explore a feature that directly uses this powerful concept: Bookmarks.

What are Bookmarks?#

Imagine you're exploring a large 3D city model on the map, and you find a really interesting viewpoint – maybe a specific angle showing a cluster of buildings, with certain layers visible (like the terrain and building footprints) and maybe even some debug options turned on. You might want to save this exact view and setup so you can easily return to it later, or share it with someone else.

Manually recreating the camera position, turning on the right layers, and setting the same options each time would be tedious. This is where Bookmarks come in.

As the concept description says:

This feature allows users to save the current state of the application, including the camera position, loaded layers, and active debug or comparison settings. Saved states can be reloaded later, providing quick access to specific viewpoints or configurations. It's like saving different camera angles and scene setups in a film production to revisit them later.

Bookmarks capture a snapshot of the application's configuration at a specific moment, allowing you to jump back to it instantly.

The Central Use Case: Saving and Restoring a View#

The core task is straightforward: 1. Find a view on the map that you like. 2. Click a button to save it. 3. Later, select the saved bookmark from a list. 4. The application immediately changes the map view and settings back to how they were when you saved it.

This feature relies heavily on the application's ability to read and write its Global State.

Key Concepts#

Bookmarks involve a few core ideas:

  • Capturing State: A bookmark needs to record the critical parts of the application's Global State – at minimum, the View State (camera position) and the list of currently visible Layers. It can also include debug settings, comparison mode state, etc.
  • Visual Representation: Bookmarks are often shown as a list of small images (thumbnails) that give you a preview of the saved view.
  • Storage: The saved state and thumbnail need to be stored temporarily within the application while it's running, and potentially exported/imported to a file for saving across sessions or sharing.
  • Actions: The user needs controls to:
    • Add (Save) a new bookmark.
    • Select (Load) an existing bookmark.
    • Delete a bookmark.
    • Export/Import bookmarks.

How to Use (The Bookmarks Panel)#

The main way you interact with bookmarks is through the Bookmarks Panel. You'll typically find a button in the application's interface to open this panel. The component responsible for the main panel structure is BookmarksPanel (src/components/bookmarks-panel/bookmarks-panel.tsx).

Inside the panel, you'll see:

  1. A button to add a new bookmark (often a "+" icon).
  2. A list or "slider" showing your saved bookmarks, usually with their thumbnails.
  3. Options for managing bookmarks (like editing, deleting, exporting, importing).

Let's look at how saving, selecting, and managing bookmarks work from the user's perspective.

1. Saving a Bookmark#

When you have the view and settings you want to save:

  • Open the Bookmarks Panel.
  • Click the "Insert layer" button (or similar button with a "+").

The application will capture the current state and add a new entry to the bookmark list, often with a thumbnail screenshot of the map view.

2. Selecting/Loading a Bookmark#

To return to a saved view:

  • Open the Bookmarks Panel.
  • Find the bookmark you want in the list (using the thumbnail as a guide).
  • Click on the bookmark's entry in the list.

The application will instantly change the camera view, update the visible layers in the Layers Panel, and restore any other settings that were saved with that bookmark.

3. Deleting a Bookmark#

If you want to remove a bookmark:

  • Open the Bookmarks Panel.
  • Access the editing mode (often via an options menu).
  • Click the trash/delete icon associated with the bookmark you want to remove.
  • Confirm the deletion if prompted.

The bookmark will be removed from the list.

4. Exporting and Importing Bookmarks#

Bookmarks can often be saved to a file (exported) and loaded from a file (imported). This is useful for persistence or sharing.

  • Open the Bookmarks Panel.
  • Click the "Options" icon (three dots).
  • Choose "Download bookmarks" to save them to a file (typically JSON).
  • Choose "Upload bookmarks" to load bookmarks from a previously saved file.

This allows you to keep your bookmarks even after closing and reopening the application, or to share collections of bookmarks with others.

Under the Hood: How Bookmarks Work#

Bookmarks rely heavily on the Global State Management we discussed in the previous chapter.

When you save a bookmark:

  1. The application reads the current state from the Redux store. This includes:
    • The current View State (camera position, zoom, pitch, etc.) from the viewState slice.
    • The list of active/visible layers from the layers/sublayers state slices.
    • Potentially the state of other panels, like debug options or comparison settings.
  2. It takes a screenshot of the current map view to create a thumbnail image. This often uses libraries like html2canvas (src/utils/deck-thumbnail-utils.ts).
  3. It bundles this captured state data (camera position, layer IDs, settings, thumbnail) into a single object, often confirming it matches a defined structure like the one in src/constants/json-schemas/bookmarks.ts.
  4. It dispatches an action (using useAppDispatch) to add this new bookmark object to a list of bookmarks stored in the Global State (likely within a dedicated bookmarks slice).

When you select a bookmark:

  1. You click on a bookmark entry in the UI (SliderListItem.tsx within Slider.tsx).
  2. The UI component calls the onSelect handler provided by the BookmarksPanel (src/components/bookmarks-panel/bookmarks-panel.tsx).
  3. The BookmarksPanel dispatches actions (using useAppDispatch) to update the application's Global State. These actions restore the state values saved in the selected bookmark:
    • An action updates the viewState slice with the saved camera position.
    • An action updates the layers/sublayers state slices to match the saved list of visible layers.
    • Actions update other state slices (debug options, etc.) if saved.
  4. Because the Global State has changed, the components that read from these state slices automatically re-render:
    • The Map Visualization component (DeckGlWrapper.tsx or ArcgisWrapper.tsx) updates the camera view and redraws with the newly selected layers.
    • The Layers Panel (LayersPanel.tsx) updates the checkboxes and lists to show the layers currently active according to the restored state.
    • Other panels (like Debug) also update to reflect their saved settings.

Here's a simplified flow for saving and loading:

sequenceDiagram
    Participant User
    Participant BookmarksPanelUI
    Participant GlobalState
    Participant MapScreenshotUtil
    Participant MapVisualization
    Participant LayersPanelUI

    User->>BookmarksPanelUI: Clicks Save Bookmark (+)
    BookmarksPanelUI->>GlobalState: Reads current View State, visible Layers, etc.
    BookmarksPanelUI->>MapScreenshotUtil: Requests screenshot for thumbnail
    MapScreenshotUtil-->>BookmarksPanelUI: Provides thumbnail image
    BookmarksPanelUI->>GlobalState: Dispatches action (ADD_BOOKMARK) with captured state + thumbnail
    GlobalState->>GlobalState: Stores new bookmark in state list
    GlobalState-->>BookmarksPanelUI: State change notifies UI
    BookmarksPanelUI->>User: Bookmark list updates with new entry

    User->>BookmarksPanelUI: Clicks on a saved bookmark
    BookmarksPanelUI->>GlobalState: Dispatches actions (SET_VIEW_STATE, SET_VISIBLE_LAYERS, etc.) with bookmark's saved state
    GlobalState->>GlobalState: Updates View State, Layer visibility, etc.
    GlobalState-->>MapVisualization: State change notifies map
    GlobalState-->>LayersPanelUI: State change notifies layers panel
    MapVisualization->>User: Map view changes to bookmark's view state, shows saved layers
    LayersPanelUI->>User: Layers list updates to show saved layer visibility

Code Snippet Examples#

The BookmarksPanel.tsx component manages the overall display and handles the user interactions by calling functions passed as props (onAddBookmark, onSelectBookmark, onDeleteBookmark, etc.). These functions are where the actual state updates happen via Redux actions, likely defined outside the component in a Redux slice file for bookmarks (conceptually, similar to view-state-slice.ts or flattened-sublayers-slice.ts).

Let's look at a very simplified view of how a bookmark is displayed in the list using SliderListItem:

// Simplified snippet from src/components/slider/slider-list-item.tsx
export const SliderListItem = ({
  id,
  selected,
  url, // This is the URL for the thumbnail image
  editingMode,
  onSelect, // Function to call when clicked
  onDelete, // Function to call when delete is clicked
  // ... other props
}) => {
  // ... state for deleting confirmation ...
  const layout = useAppLayout();
  const isMobileLayout = layout !== Layout.Desktop;

  return (
    <ListItem
      id={id}
      url={url} // Use the thumbnail URL as background image
      selected={selected}
      editingMode={editingMode}
      isMobile={isMobileLayout}
      onClick={onSelect} // Call onSelect when the item is clicked
      // ... event handlers for hover, layout props ...
    >
      {/* ... Conditional rendering for delete button based on editingMode ... */}
      {editingMode && (
        <TrashIconContainer onClick={onDeleteBookmarkClickHandler}>
          <TrashIcon /> {/* The delete button */}
        </TrashIconContainer>
      )}
      {/* ... potentially bookmark title/number for non-image bookmarks ... */}
    </ListItem>
    {/* ... Delete Confirmation modal ... */}
  );
};

This snippet shows that SliderListItem receives the bookmark's id, url (for the thumbnail), its selection status (selected), and functions for onSelect and onDelete. When the user clicks on the list item, the onClick handler triggers onSelect, signaling to the parent component (Slider and ultimately BookmarksPanel) that this bookmark should be loaded. If editingMode is on and the user clicks the trash icon, onDeleteBookmarkClickHandler is called, which might trigger the delete confirmation and eventually call onDelete.

The actual Slider component (src/components/slider/slider.tsx) is responsible for rendering the list of SliderListItems horizontally (or vertically for other slider types like floors), managing the scrolling, and handling the left/right arrow button navigation for desktop layouts. It maps over the data (the array of bookmark objects) and renders a SliderListItem for each one, passing the relevant data and handlers.

The structure of the data saved in a bookmark is defined by the JSON schema in src/constants/json-schemas/bookmarks.ts. This schema specifies that each bookmark object should include an id, imageUrl (the thumbnail), viewState (which itself contains main and minimap view states, matching the structure in the Redux viewState slice), and lists of layersLeftSide/layersRightSide and activeLayersIdsLeftSide/activeLayersIdsRightSide (for comparison mode, linked to Layer Management and Comparison Mode).

// Simplified snippet from src/constants/json-schemas/bookmarks.ts
export const bookmarksSchemaJson: Draft202012Schema = {
  // ... schema metadata ...
  type: "array", // The main structure is an array of bookmarks
  items: {       // Each item in the array is a bookmark object
    type: "object",
    properties: {
      id: { type: "string" },
      imageUrl: { type: "string" }, // Base64 or URL for thumbnail
      viewState: { // Nested object matching the Redux ViewStateState structure
        type: "object",
        properties: {
          main: { $ref: "#/$defs/ViewState" }, // Reference to ViewState definition
          minimap: { $ref: "#/$defs/ViewState" },
        },
        // ... required viewState properties ...
      },
      layersLeftSide: { $ref: "#/$defs/LayerExample" }, // Reference to LayerExample definition
      layersRightSide: { $ref: "#/$defs/LayerExample" },
      activeLayersIdsLeftSide: { // Array of IDs for active layers
        type: "array",
        items: { type: "string" },
      },
      activeLayersIdsRightSide: { // Array of IDs for active layers in comparison mode
        type: "array",
        items: { type: "string" },
      },
      // ... other optional properties ...
    },
    required: ["id", "imageUrl"], // Minimum required properties
  },
  // ... $defs for ViewState, LayerExample structures ...
};

This schema acts as a blueprint, ensuring that when bookmarks are saved or loaded from a file, they have the expected format. Functions in bookmarks-utils.ts are used to parse and validate JSON files against this schema when importing.

Functions in deck-thumbnail-utils.ts handle the technical details of capturing the image, potentially combining views for Comparison Mode.

// Simplified snippet from src/utils/deck-thumbnail-utils.ts
import html2canvas from "html2canvas";

// Function to create a thumbnail from a DOM element (like the map canvas)
export const createViewerBookmarkThumbnail = async (
  containerId // ID of the HTML element containing the map
): Promise<string | null> => {
  const domElement = document.querySelector(containerId); // Find the map element
  if (!domElement) {
    return null;
  }
  // Use html2canvas to take a screenshot of the element
  const canvas = await html2canvas(domElement as HTMLElement);
  // Create a thumbnail of the desired size from the screenshot
  const thumbnail = createThumbnail(canvas, 144, 81); // Defined thumbnail size
  if (!thumbnail) {
    return null;
  }
  // Return the thumbnail as a base64 data URL string
  return thumbnail.toDataURL("image/png");
};

// Helper to crop/resize the canvas
const createThumbnail = (canvas: HTMLCanvasElement, width: number, height: number): HTMLCanvasElement | null => {
  // ... logic to calculate crop area and draw to new canvas ...
  return outputCanvas;
};

// ... function to merge thumbnails for comparison mode ...

This demonstrates how html2canvas is used to grab a visual representation of the map, which is then resized to create the small thumbnail image stored with the bookmark data.

In summary, bookmarks work by serializing (converting to data) key parts of the application's Global State along with a visual thumbnail. When a bookmark is selected, the saved data is deserialized and used to update the Global State, triggering updates throughout the application's UI and the map visualization.

Conclusion#

In this chapter, we learned about Bookmarks, a convenient feature for saving and restoring specific views and configurations of the application. We saw that bookmarks achieve this by capturing and storing a snapshot of the application's Global State, including the View State (camera position) and visible Layers. The Bookmarks Panel provides the user interface for adding, selecting, deleting, exporting, and importing these saved states, often using visual thumbnails generated by capturing the map view.

Understanding bookmarks reinforces the importance of Global State Management – by having a central source of truth, features like bookmarks can easily capture and restore the entire application's setup.

Next, we'll explore Comparison Mode, another feature that utilizes the application's state management to display and compare different views or datasets side-by-side.

Chapter 6: Comparison Mode


Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/components/bookmarks-panel/bookmarks-panel.tsx), 2(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/components/slider/slider-list-item.tsx), 3(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/components/slider/slider.tsx), 4(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/constants/json-schemas/bookmarks.ts), 5(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/utils/bookmarks-utils.ts), 6(https://github.com/visgl/loaders.gl-showcases/blob/3403a56b6839455092211a95c5cd695f20ea6c7e/src/utils/deck-thumbnail-utils.ts)