To add undo/redo support to your application, you can work with LuciadRIA’s Undoable and UndoManager API.
                           This guide explains the basic concepts of undo/redo in LuciadRIA, and gives you some concrete examples.
                        
Main principles of undo/redo in LuciadRIA
LuciadRIA doesn’t make any assumptions on how undo/redo works in your application. You decide when to create Undoables,
                           what they do, and how they’re managed. The samples show you examples of common use cases, such as undo/redo
                           for selection, creation, editing, and removal of features.
                        
Creating Undoables
Typically, you create an Undoable in response to an event.
                           Common events are:
                        
- 
                              
                              The SelectionChangeevent on theMapwhen a user selects aFeature.
- 
                              
                              The ModelChangedevent on aModelwhen a user creates aFeature.
- 
                              
                              The ModelChangedevent on aModel, when the user deletes a feature. Note that theModelChangedevent only gives you an identifier. If you need access to the completeFeatureinstance to implement redo, create theUndoablewhere you remove it from the model, in a "Delete" context menu action handler for example.
- 
                              
                              The EditShapeevent of anEditControllerwhen a user edits aFeature.
- 
                              
                              The Restartedevent of anEditControllerorCreateControllerwhen used to cancel an edit or create interaction, cf. Cancelling create and edit interactions.
You can implement undo/redo for other events too. For example, you can add undo/redo support
                           for layer visibility changes, by listening to the VisibilityChanged event on Layer.
                        
As an example, let’s add undo/redo support for selection.
                           First, we implement a SelectionUndoable that can undo/redo selections.
                        
SelectionUndoableimport {Map} from "@luciad/ria/view/Map.js";
import {Undoable} from "@luciad/ria/view/undo/Undoable.js";
import {FeatureLayer} from "@luciad/ria/view/feature/FeatureLayer.js";
import {TileSet3DLayer} from "@luciad/ria/view/tileset/TileSet3DLayer.js";
import {Layer} from "@luciad/ria/view/Layer.js";
import {Feature} from "@luciad/ria/model/feature/Feature.js";
import {WithIdentifier} from "@luciad/ria/model/WithIdentifier.js";
type Selection = {
  layer: Layer,
  selected: WithIdentifier[]
}[];
/**
 * An undoable for selection
 */
class SelectionUndoable extends Undoable {
  private readonly _map: Map;
  private readonly _selectionBefore: Selection;
  private readonly _selectionAfter: Selection;
  constructor(id: string, label: string, map: Map, selectionBefore: Selection, selectionAfter: Selection) {
    super(id, label);
    this._map = map;
    this._selectionBefore = [...selectionBefore];
    this._selectionAfter = [...selectionAfter];
  }
  redo(): void {
    this.applySelection(this._selectionAfter);
  }
  undo(): void {
    this.applySelection(this._selectionBefore);
  }
  private applySelection(selection: Selection): void {
    const objectsToSelect = selection.map(sel => {
      return {
        layer: sel.layer as FeatureLayer | TileSet3DLayer,
        objects: sel.selected as Feature[]
      };
    });
    this._map.selectObjects(objectsToSelect);
  }
}Now that we have our SelectionUndoable, we must instantiate it whenever the selection changes on the Map.
                           Once created, we add it to an UndoManager. See Working with the UndoManager for more information.
                        
SelectionUndoable in response to SelectionChange events
                        import {UndoManager} from "@luciad/ria/view/undo/UndoManager.js";
const SAMPLE_UNDO_MANAGER = new UndoManager();
/**
 * Adds selection undo/redo support to a Map
 * @param map The map to add undo/redo support to
 */
export const addSelectionUndoSupport = (map: Map) => {
  let currentSelection = [...map.selectedObjects];
  let idCounter = 0;
  return map.on("SelectionChanged", () => {
    const id = "" + idCounter++;
    const label = "selection change";
    const undoable = new SelectionUndoable(id, label, map, currentSelection, map.selectedObjects);
    SAMPLE_UNDO_MANAGER.push(undoable);
    currentSelection = [...map.selectedObjects];
  });
}The UndoManager also holds a label for the Undoable and an id. You can use the label for translation, and the id to differentiate between items on the undoStack.
                        
Working with the UndoManager
The UndoManager manages an undoStack and a redoStack of Undoables. It has undo() and redo() methods that
                           can be called by a button in the UI, or a keyboard shortcut. For example, to wire CTRL+Z to undo() and CTRL+Y to redo(),
                           we can write:
                        
UndoManager.undo() and UndoManager.redo() (from toolbox/ria/core/util/SampleUndoSupport.ts)
                        // wire CTRL+Z to undo and CTRL+Y to redo. For Mac users, wire CMD+Z to undo and CMD+SHIFT+Z to redo.
window.addEventListener("keydown", (e) => {
  if (document.activeElement instanceof HTMLInputElement) {
    // input/text field has focus, undo/redo should affect the text and not the map
    return;
  }
  // ctrl+z or cmd+z (mac) to undo
  const isMac = window.navigator.platform.indexOf("Mac") >= 0 || window.navigator.userAgent.indexOf("Mac") >= 0;
  const isUndoKey = isMac ? (e.key === "z" && (e.metaKey && !e.shiftKey)) : (e.key === "z" && e.ctrlKey);
  if (isUndoKey) {
    SAMPLE_UNDO_MANAGER.undo();
    e.preventDefault();
  }
  // ctrl+y or cmd+shift+z (mac) to redo
  const isRedoKey = isMac ? (e.key === "z" && e.metaKey && e.shiftKey) : (e.key === "y" && e.ctrlKey);
  if (isRedoKey) {
    SAMPLE_UNDO_MANAGER.redo();
    e.preventDefault();
  }
});UndoManager.undo() and UndoManager.redo() take an Undoable from the stack and call Undoable.undo() or Undoable.redo().
                           For our SelectionUndoable, this causes the selection to change.
                        
Preventing event loops
One last issue that we must handle is the prevention of event loops. In our selection example, SelectionUndoable.undo() and redo() change
                           the Map selection, which creates another, unwanted SelectionUndoable.
                           To prevent such an event loop, we must stop listening to selection events while we’re applying the Undoable.
                           To stop listening, we make use of a simple Lock object.
                           We lock the lock in the undo() and redo() method, and prevent the "SelectionChange" listener from creating new SelectionUndoable instances.
                        
interface Lock {
  locked: boolean;
}
class SelectionUndoable /*...*/ {
   private readonly _lock: Lock;
   constructor(..., lock: Lock) {
     // ...
     this._lock = lock;
   }
   applySelection(selection: Selection) {
     // avoid creating a new SelectionUndoable in the map.selectObjects call below
     this._lock.locked = true;
     this._map.selectObjects(selection);
     this._lock.locked = false;
   }
}
const selectionLock = {locked: false};
map.on("SelectionChanged", () => {
  if (!selectionLock.locked) {
    // create new SelectionUndoable(..., selectionLock)
  }
}import {Feature} from "@luciad/ria/model/feature/Feature.js";
import {WithIdentifier} from "@luciad/ria/model/WithIdentifier.js";
import {FeatureLayer} from "@luciad/ria/view/feature/FeatureLayer.js";
import {Layer} from "@luciad/ria/view/Layer.js";
import {Map} from "@luciad/ria/view/Map.js";
import {TileSet3DLayer} from "@luciad/ria/view/tileset/TileSet3DLayer.js";
import {Undoable} from "@luciad/ria/view/undo/Undoable.js";
import {UndoManager} from "@luciad/ria/view/undo/UndoManager.js";
type Selection = {
  layer: Layer,
  selected: WithIdentifier[]
}[];
interface Lock {
  locked: boolean;
}
/**
 * An undoable for selection
 */
class SelectionUndoable extends Undoable {
  private readonly _map: Map;
  private readonly _selectionBefore: Selection;
  private readonly _selectionAfter: Selection;
  private readonly _lock: Lock;
  constructor(id: string, label: string, map: Map, selectionBefore: Selection, selectionAfter: Selection, lock: Lock) {
    super(id, label);
    this._map = map;
    this._selectionBefore = [...selectionBefore];
    this._selectionAfter = [...selectionAfter];
    this._lock = lock;
  }
  redo(): void {
    this.applySelection(this._selectionAfter);
  }
  undo(): void {
    this.applySelection(this._selectionBefore);
  }
  private applySelection(selection: Selection): void {
    const objectsToSelect = selection.map(sel => {
      return {
        layer: sel.layer as FeatureLayer | TileSet3DLayer,
        objects: sel.selected as Feature[]
      };
    });
    // avoid creating a new SelectionUndoable in the map.selectObjects call below
    this._lock.locked = true;
    this._map.selectObjects(objectsToSelect);
    this._lock.locked = false;
  }
}
const SAMPLE_UNDO_MANAGER = new UndoManager();
/**
 * Adds selection undo/redo support to a Map
 * @param map The map to add undo/redo support to
 */
export const addSelectionUndoSupport = (map: Map) => {
  let currentSelection = [...map.selectedObjects];
  let idCounter = 0;
  const selectionLock = {locked: false};
  return map.on("SelectionChanged", () => {
    const id = "" + idCounter++;
    const label = "selection change"
    const undoable = new SelectionUndoable(id, label, map, currentSelection, map.selectedObjects, selectionLock);
    SAMPLE_UNDO_MANAGER.push(undoable);
    currentSelection = [...map.selectedObjects];
  });
}More examples
For more examples, check out:
- 
                              
                              The module toolbox/ria/core/util/SampleUndoSupportimplements commonly used undo/redo operations in LuciadRIA samples, such as undo/redo for selection, creation, deletion, and editing of features.
- 
                              
                              The 'Create and Edit` sample shows you how to wire undo/redo buttons in the UI to an UndoManager.
- 
                              
                              The 'Geolocate' sample shows you how to use the undo/redo API with a custom controller implementation.