diff --git a/assets/icons/edge_selection-arrow.svg b/assets/icons/edge_selection-arrow.svg
new file mode 100644
index 00000000..e4c4aafd
--- /dev/null
+++ b/assets/icons/edge_selection-arrow.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/assets/icons/edge_selection.svg b/assets/icons/edge_selection.svg
new file mode 100644
index 00000000..e5285e63
--- /dev/null
+++ b/assets/icons/edge_selection.svg
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/assets/icons/skewer_selection-arrow.svg b/assets/icons/skewer_selection-arrow.svg
new file mode 100644
index 00000000..75695d0c
--- /dev/null
+++ b/assets/icons/skewer_selection-arrow.svg
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/assets/icons/skewer_selection_black.svg b/assets/icons/skewer_selection_black.svg
new file mode 100644
index 00000000..9cc1d200
--- /dev/null
+++ b/assets/icons/skewer_selection_black.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/src/js/Aladin.js b/src/js/Aladin.js
index 853f9335..c9d88500 100644
--- a/src/js/Aladin.js
+++ b/src/js/Aladin.js
@@ -74,6 +74,7 @@ import { SimbadPointer } from "./gui/Button/SimbadPointer";
import { ColorPicker } from "./gui/Button/ColorPicker";
import { GridEnabler } from "./gui/Button/GridEnabler";
import { CooFrame } from "./gui/Input/CooFrame";
+import { SelectionMode } from "./gui/Button/SelectionMode";
import { Circle } from "./shapes/Circle";
import { Ellipse } from "./shapes/Ellipse";
import { Polyline } from "./shapes/Polyline";
@@ -110,6 +111,8 @@ import { Polyline } from "./shapes/Polyline";
* CSS class for that button is `aladin-grid-control`
* @property {boolean} [showSettingsControl=false] - Whether to show the settings control toolbar.
* CSS class for that button is `aladin-settings-control`
+ * @property {boolean} [showSelectionModeControl=false] - Whether to show the selection mode menu opener button.
+ * CSS class for that button is `aladin-selectionMode-control`
* @property {boolean} [showColorPickerControl=false] - Whether to show the color picker tool.
* CSS class for that button is `aladin-colorPicker-control`
* @property {boolean} [showShareControl=false] - Whether to show the share control toolbar.
@@ -155,6 +158,7 @@ import { Polyline } from "./shapes/Polyline";
* @property {boolean} [pixelateCanvas=true] - Whether to pixelate the canvas.
* @property {boolean} [manualSelection=false] - When set to true, no selection will be performed, only events will be generated.
* @property {string} [mode] - Interface theme, can be either 'dark' or 'light'. If not set, the mode will be retrieved from your browser preference or your localStorage.
+ * @property {string} [selectionMode="edge"] - When set to 'skewer' footprints are selected by clicking inside them. For 'edge' footprints are selected by clicking on their edges.
* @property {Object} [selector] - More options for the the selector.
* @property {string} [selector.color] - Color of the selector, defaults to the color of the reticle. Can be a hex color or a function returning a hex color.
* @property {number} [selector.lineWidth=2] - Width of the selector line.
@@ -674,15 +678,19 @@ export let Aladin = (function () {
widgets["simbad"] = simbad
}
- // Add the projection control
// Add the coo grid control
if (options.showCooGridControl) {
let grid = new GridEnabler(this);
widgets["grid"] = grid;
}
- // Add the projection control
- // Add the coo grid control
+ // Show selection mode control
+ if (options.showSelectionModeControl) {
+ let selectionMode = new SelectionMode(this);
+ widgets["selectionMode"] = selectionMode
+ }
+
+ // Add the color picker control
if (options.showColorPickerControl) {
let picker = new ColorPicker(this);
widgets["picker"] = picker;
@@ -698,6 +706,7 @@ export let Aladin = (function () {
this.toolbar.add(name, widget);
}
+ // Add the projection control
if (options.showProjectionControl) {
this.projBtn = new ProjectionActionButton(this);
this.addUI(this.projBtn);
@@ -784,6 +793,7 @@ export let Aladin = (function () {
showSimbadPointerControl: false,
showCooGridControl: false,
showSettingsControl: false,
+ showSelectionModeControl: false,
showColorPickerControl: false,
// Share toolbar
showShareControl: false,
diff --git a/src/js/Selector.js b/src/js/Selector.js
index bd46dbb9..cfac364d 100644
--- a/src/js/Selector.js
+++ b/src/js/Selector.js
@@ -27,15 +27,16 @@ import { PolySelect } from "./FiniteStateMachine/PolySelect";
import { LineSelect } from "./FiniteStateMachine/LineSelect";
import { RectSelect } from "./FiniteStateMachine/RectSelect";
import { ALEvent } from "./events/ALEvent";
+import { Utils } from './Utils';
/******************************************************************************
* Aladin Lite project
- *
+ *
* Class Selector
- *
+ *
* A selector
- *
+ *
* Author: Matthieu Baumann[CDS]
- *
+ *
*****************************************************************************/
export class Selector {
@@ -121,7 +122,7 @@ export class Selector {
continue;
}
sources = cat.getSources();
-
+
for (var l = 0; l < sources.length; l++) {
s = sources[l];
@@ -172,4 +173,60 @@ export class Selector {
return objList;
}
+
+ /**
+ * Retrieves objects skewered by the cursor position or specified coordinates. An object is
+ * skewered if it is a shape that contains the specified coordinate, or is a catalog object within 3 pixels
+ * of the specified coordinate.
+ *
+ * If e is a mouse event (as opposed to an object with x and y values), the mouse coordinates
+ * of the event are used.
+ *
+ * This is implemented by simulating the interactive selection of a circle region with a 3 pixel radius)
+ * around the given coordinates and returns all catalog sources and overlay items intersecting with it.
+ *
+ * @param {Event|Object} e - Mouse coordinate via mouse event or object with x and y properties
+ * @param {Object} view - The Aladin View instance containing catalogs and overlays
+ * @returns {Array} Array of object lists, where each subarray contains objects
+ * from a single catalog or overlay that intersect with the selection region.
+ * Returns empty array if no objects are found.
+ */
+ static getSkewerObjects(e, view) {
+ // Get the xy from the event
+ let xymouse;
+ if (e instanceof Event) {
+ xymouse = Utils.relMouseCoords(e);
+ } else {
+ xymouse = e;
+ }
+ const x = xymouse.x;
+ const y = xymouse.y;
+
+ // Perform a selection using a circle around x, y as if drawn by dragging 3 pixels.
+ const r2 = 9;
+ const r = Math.sqrt(r2);
+
+ let selectorObject = {
+ x, y, r,
+ label: 'circle',
+ contains(s) {
+ let dx = (s.x - x)
+ let dy = (s.y - y);
+
+ return dx*dx + dy*dy <= r2;
+ },
+ bbox() {
+ return {
+ x: x - r,
+ y: y - r,
+ w: 2*r,
+ h: 2*r
+ }
+ }
+ };
+
+ let objList = Selector.getObjects(selectorObject, view);
+
+ return objList;
+ }
}
\ No newline at end of file
diff --git a/src/js/View.js b/src/js/View.js
index 5fac86fb..83c7e664 100644
--- a/src/js/View.js
+++ b/src/js/View.js
@@ -273,6 +273,13 @@ export let View = (function () {
this.selector = new Selector(this, this.options.selector);
this.manualSelection = (this.options && this.options.manualSelection) || false;
+ // Selection mode
+ this.selectionMode = View.SELECTION_MODE_EDGE;
+ if (this.options.selectionMode === 'skewer') {
+ this.selectionMode = View.SELECTION_MODE_SKEWER;
+ }
+
+
// current reference image survey displayed
this.imageLayers = new Map();
@@ -359,6 +366,10 @@ export let View = (function () {
View.TOOL_SIMBAD_POINTER = 2;
View.TOOL_COLOR_PICKER = 3;
+ // Selection modes
+ View.SELECTION_MODE_EDGE = 0;
+ View.SELECTION_MODE_SKEWER = 1;
+
// TODO: should be put as an option at layer level
View.DRAW_SOURCES_WHILE_DRAGGING = true;
View.DRAW_MOCS_WHILE_DRAGGING = true;
@@ -514,6 +525,13 @@ export let View = (function () {
}
View.prototype.setMode = function (mode, params) {
+
+ // Undo the specialized cursors, if any.
+ const prevMode = this.mode;
+ if (prevMode == View.TOOL_SIMBAD_POINTER) {
+ this.catalogCanvas.classList.remove('aladin-sp-cursor');
+ }
+
// hide the picker tooltip
this.colorPickerTool.domElement.style.display = "none";
// in case we are in the selection mode
@@ -546,6 +564,14 @@ export let View = (function () {
ALEvent.MODE.dispatchedTo(this.aladin.aladinDiv, {mode});
};
+ View.prototype.setSelectionMode = function (selectionMode) {
+ this.selectionMode = selectionMode;
+ };
+
+ View.prototype.getSelectionMode = function () {
+ return this.selectionMode;
+ };
+
View.prototype.setCursor = function (cursor) {
if (this.catalogCanvas.style.cursor == cursor) {
return;
@@ -713,12 +739,10 @@ export let View = (function () {
var showContextMenu = true;
var xystart;
- var handleSelect = function(xy, tolerance) {
+ var handleSelect = function(xy, tolerance, withModifierKey=false) {
tolerance = tolerance || 5;
var objs = view.closestObjects(xy.x, xy.y, tolerance);
- view.unselectObjects();
-
if (objs) {
var objClickedFunction = view.aladin.callbacksByEventName['objectClicked'];
var footprintClickedFunction = view.aladin.callbacksByEventName['footprintClicked'];
@@ -757,11 +781,14 @@ export let View = (function () {
if (shapes.length > 0) {
objs.push(shapes)
}
- view.selectObjects(objs);
+ view.selectObjects(objs, withModifierKey);
view.lastClickedObject = objs;
} else {
+
+ view.unselectObjects();
+
// If there is a past clicked object
if (view.lastClickedObject) {
// TODO: do we need to keep that triggering ?
@@ -772,6 +799,95 @@ export let View = (function () {
}
}
}
+
+ /**
+ * Perform a skewer-based selection of objects at the specified mouse position.
+ *
+ * @param {Event|Object} e - Mouse coordinate via mouse event or object with x and y properties
+ * @param {boolean} withModifierKey - If true, toggles selection (adds/removes from existing selection);
+ * if false, replaces current selection
+ */
+ var handleSkewerSelect = function(e, withModifierKey) {
+
+ const objList = Selector.getSkewerObjects(e, view);
+ view.selectObjects(objList, withModifierKey);
+ }
+
+ /**
+ * Make the selected objects appear hovered and make all other objects appear unhovered,
+ * firing the appropriate callbacks for both cases.
+ *
+ * The also sets the cursor to a pointer to indicate that some object(s) would be
+ * select on click in the current mouse position.
+ *
+ * @param {Array} objects - Array of objects to apply hover state to
+ * @param {Object} xymouse - Mouse coordinates with x and y properties
+ */
+ var hoverObjects = function(objects, xymouse) {
+ var objHoveredFunction = view.aladin.callbacksByEventName['objectHovered'];
+ var footprintHoveredFunction = view.aladin.callbacksByEventName['footprintHovered'];
+
+ view.setCursor('pointer');
+
+ for (let o of objects) {
+
+ if (typeof objHoveredFunction === 'function' && (!view.lastHoveredObject || !view.lastHoveredObject.includes(o))) {
+ var ret = objHoveredFunction(o, xymouse);
+ }
+
+ if (o.isFootprint()) {
+ if (typeof footprintHoveredFunction === 'function' && (!view.lastHoveredObject || !view.lastHoveredObject.includes(o))) {
+ var ret = footprintHoveredFunction(o, xymouse);
+ }
+ }
+
+ if (!view.lastHoveredObject || !view.lastHoveredObject.includes(o)) {
+ o.hover();
+ }
+ }
+
+ // unhover the objects in lastHoveredObjects that are not in closest anymore
+ if (view.lastHoveredObject) {
+ var objHoveredStopFunction = view.aladin.callbacksByEventName['objectHoveredStop'];
+
+ for (let lho of view.lastHoveredObject) {
+ if (!objects.includes(lho)) {
+ lho.unhover();
+
+ if (typeof objHoveredStopFunction === 'function') {
+ objHoveredStopFunction(lho, xymouse);
+ }
+ }
+ }
+ }
+ view.lastHoveredObject = objects;
+ }
+
+ /**
+ * Removes hover state from all previously hovered objects and fires the
+ * apropriate callbacks.
+ *
+ * The also resets the cursor to the default to indicate that no objects would be
+ * selected on click in the current mouse position.
+ *
+ * @param {Object} xymouse - Mouse coordinates with x and y properties
+ */
+ var unhoverObjects = function(xymouse) {
+ view.setCursor('default');
+ if (view.lastHoveredObject) {
+ var objHoveredStopFunction = view.aladin.callbacksByEventName['objectHoveredStop'];
+ for (let lho of view.lastHoveredObject) {
+ lho.unhover();
+
+ if (typeof objHoveredStopFunction === 'function') {
+ objHoveredStopFunction(lho, xymouse);
+ }
+ }
+ }
+
+ view.lastHoveredObject = null;
+ }
+
var touchStartTime;
Utils.on(view.catalogCanvas, "mousedown touchstart", function (e) {
e.stopPropagation();
@@ -918,6 +1034,7 @@ export let View = (function () {
// reacting on 'click' rather on 'mouseup' is more reliable when panning the view
Utils.on(view.catalogCanvas, "mouseup mouseout touchend touchcancel", function (e) {
const xymouse = Utils.relMouseCoords(e);
+ const withModifierKey = e.ctrlKey || e.metaKey;
ALEvent.CANVAS_EVENT.dispatchedTo(view.aladinDiv, {
state: {
@@ -1006,11 +1123,19 @@ export let View = (function () {
const elapsedTime = Date.now() - touchStartTime;
if (elapsedTime < 100) {
view.updateObjectsLookup();
- handleSelect(xymouse, 15);
+ if (view.selectionMode === View.SELECTION_MODE_SKEWER) {
+ handleSkewerSelect(e, withModifierKey)
+ } else {
+ handleSelect(xymouse, 15, withModifierKey);
+ }
}
}
} else {
- handleSelect(xymouse);
+ if (view.selectionMode === View.SELECTION_MODE_EDGE) {
+ handleSelect(xymouse, 5, withModifierKey);
+ } else {
+ handleSkewerSelect(e, withModifierKey);
+ }
}
}
@@ -1193,71 +1318,31 @@ export let View = (function () {
lastMouseMovePos = pos;
}
- // closestObjects is very costly, we would like to not do it
- // especially if the objectHovered function is not defined.
- var closests = view.closestObjects(xymouse.x, xymouse.y, 5);
+ if (view.selectionMode === View.SELECTION_MODE_EDGE) {
+ // We're in edge selection mode for footprints. closestObjects() will find those footprints by closeness to a footprint edge.
+ // closestObjects is very costly, we would like to not do it
+ // especially if the objectHovered function is not defined.
+ var closests = view.closestObjects(xymouse.x, xymouse.y, 5);
- if (closests) {
- var objHoveredFunction = view.aladin.callbacksByEventName['objectHovered'];
- var footprintHoveredFunction = view.aladin.callbacksByEventName['footprintHovered'];
+ if (closests) {
+ hoverObjects(closests, xymouse);
+ } else {
+ unhoverObjects(view, xymouse);
+ }
+ } else if (view.selectionMode === View.SELECTION_MODE_SKEWER) {
+ // We're in skewer mode. Let's see what would be selected.
+ const skewerTargetsByLayer = Selector.getSkewerObjects(e, view);
+ const skewerObjects = skewerTargetsByLayer.flat();
- view.setCursor('pointer');
-
- for (let o of closests) {
-
- if (typeof objHoveredFunction === 'function' && (!view.lastHoveredObject || !view.lastHoveredObject.includes(o))) {
- var ret = objHoveredFunction(o, xymouse);
- }
-
- if (o.isFootprint()) {
- if (typeof footprintHoveredFunction === 'function' && (!view.lastHoveredObject || !view.lastHoveredObject.includes(o))) {
- var ret = footprintHoveredFunction(o, xymouse);
- }
- }
-
- if (!view.lastHoveredObject || !view.lastHoveredObject.includes(o)) {
- o.hover();
- }
+ if (skewerObjects.length > 0) {
+ hoverObjects(skewerObjects, xymouse);
+ } else {
+ unhoverObjects(view, xymouse);
}
- // unhover the objects in lastHoveredObjects that are not in closest anymore
- if (view.lastHoveredObject) {
- var objHoveredStopFunction = view.aladin.callbacksByEventName['objectHoveredStop'];
-
- for (let lho of view.lastHoveredObject) {
- if (!closests.includes(lho)) {
- lho.unhover();
-
- if (typeof objHoveredStopFunction === 'function') {
- objHoveredStopFunction(lho, xymouse);
- }
- }
- }
- }
- view.lastHoveredObject = closests;
- } else {
- view.setCursor('default');
- if (view.lastHoveredObject) {
- var objHoveredStopFunction = view.aladin.callbacksByEventName['objectHoveredStop'];
-
- /*if (typeof objHoveredStopFunction === 'function') {
- // call callback function to notify we left the hovered object
- var ret = objHoveredStopFunction(view.lastHoveredObject, xymouse);
- }
-
- view.lastHoveredObject.unhover();*/
- for (let lho of view.lastHoveredObject) {
- lho.unhover();
-
- if (typeof objHoveredStopFunction === 'function') {
- objHoveredStopFunction(lho, xymouse);
- }
- }
- }
-
- view.lastHoveredObject = null;
}
+
if (e.type === "mousemove") {
return;
}
@@ -1391,6 +1476,7 @@ export let View = (function () {
view.displayHpxGrid = false;
view.displayCatalog = false;
+ view.skewerEnabled = false;
};
View.prototype.requestRedrawAtDate = function (date) {
@@ -1636,6 +1722,9 @@ export let View = (function () {
return imageData;
};
+ /**
+ * Unselects all currently selected objects.
+ */
View.prototype.unselectObjects = function() {
if (this.manualSelection) {
return;
@@ -1654,11 +1743,24 @@ export let View = (function () {
this.requestRedraw();
}
- View.prototype.selectObjects = function(selection) {
+ /**
+ * Selects the specified objects in the view.
+ *
+ * If withModifierKey is true, it modifies the existing selection (adds/removes).
+ * Otherwise, it replaces the current selection.
+ *
+ * @param {Array|Object} selection - The objects to select, either an array or a selector object.
+ * @param {boolean} [withModifierKey=false] - Whether to modify (versus replace) the existing selections.
+ */
+ View.prototype.selectObjects = function(selection, withModifierKey=false) {
if (this.manualSelection) {
return;
}
+ if (Array.isArray(selection) && withModifierKey) {
+ selection = this.computeModifiedSelection(selection)
+ }
+
// unselect the previous selection
this.unselectObjects();
@@ -1736,6 +1838,113 @@ export let View = (function () {
}
}
+ View.prototype._getLayerForObj = function(obj) {
+ let layer = null;
+ if (obj.getCatalog) {
+ layer = obj.getCatalog()
+ } else {
+ layer = obj.overlay
+ }
+ return layer
+ }
+
+ View.prototype._copySelectionsToStage = function(selections, stage, overlays, exclude) {
+ for (const group of selections) {
+ for (const obj of group) {
+ const objExcluded = exclude.includes(obj)
+ if (!objExcluded) {
+ const layer = this._getLayerForObj(obj)
+ const idx = overlays.findIndex(item => item.uuid === layer.uuid);
+ if (idx >= 0) {
+ stage[idx].push(obj)
+ } else {
+ console.warn("Layer not found for selected obj: " + obj)
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Computes the full set of selections that should result if the specified (pending) objects were
+ * selected with a modifier key pressed.
+ *
+ * If there are existing selections, it adds pending items that aren't selected,
+ * or removes all pending items if they are all already selected.
+ *
+ * Organizes selections by overlay layers as expected by selectObjects.
+ *
+ * @param {Array} pending - Array of array of objects to potentially add/remove from selection.
+ * @returns {Array} The modified selection array.
+ */
+ View.prototype.computeModifiedSelection = function(pending) {
+ const current = this.selection
+ let modSelection = pending
+ if (current && current.length > 0) {
+ // There are some items already selected.
+ // We will be adding all the pending selections that are not already selected,
+ // UNLESS all of the pending selections are already selected, in which case
+ // they will all be unselected.
+ const toAdd = []
+ let mightRemove = []
+ modSelection = [] // We will build a new selection list from the current and pending selections
+
+ // stage will have one row for each existing overlay in which to collect all desired selections.
+ const overlays = this.aladin.getOverlays()
+ const stage = new Array(overlays.length).fill(null).map(() => []);
+
+ // Put already-selected items in mightRemove and not-yet-selected items in toAdd.
+ for (const group of pending) {
+ for (const obj of group) {
+ if (obj.isSelected) {
+ mightRemove.push(obj)
+ } else {
+ toAdd.push(obj)
+ }
+ }
+ }
+
+ // If there is anything in toAdd, then clear mightRemove since they will be left selected.
+ if (toAdd.length > 0) {
+ mightRemove = []
+ }
+
+ // Copy current selections to stage except for anything in mightRemove
+ this._copySelectionsToStage(current, stage, overlays, mightRemove)
+
+ // Copy toAdd selections to stage
+ this._copySelectionsToStage([toAdd], stage, overlays, [])
+
+ // Build new modified selections list from stage.
+ // I can preserve the layer order, but I don't know how to preserve the order within
+ // layers without looping through all objects. Hopefully that order doesn't matter.
+ for (let i=0; i 0) {
+ // We have selected objects in this layer so will add the layer (or list of overlays) to modSelection
+
+ if (overlays[i].type === 'catalog') {
+ // The layer is a catalog so we add one entry for all its selections
+ const catLayer = []
+ modSelection.push(catLayer)
+ for (const obj of stage[i]) {
+ catLayer.push(obj)
+ }
+
+ } else {
+ // Assume it's a graphicalOverlay and add separate entries for each selected obj
+ // (That is the way objects in graphicalOverlays are currently selected. If they start
+ // being selected all in one list per overlay, then that can change here.)
+ for (const obj of stage[i]) {
+ modSelection.push([obj])
+ }
+ }
+ }
+ }
+
+ }
+ return modSelection;
+ }
+
View.prototype.getVisibleCells = function (norder) {
return this.wasm.getVisibleCells(norder);
};
@@ -2429,3 +2638,5 @@ export let View = (function () {
return View;
})();
+
+
diff --git a/src/js/gui/Button/SelectionMode.js b/src/js/gui/Button/SelectionMode.js
new file mode 100644
index 00000000..2b5ab0b6
--- /dev/null
+++ b/src/js/gui/Button/SelectionMode.js
@@ -0,0 +1,149 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright 2013 - UDS/CNRS
+// The Aladin Lite program is distributed under the terms
+// of the GNU Lesser General Public License version 3
+// or (at your option) any later version.
+//
+// This file is part of Aladin Lite.
+//
+// Aladin Lite is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Aladin Lite is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with Aladin Lite. If not, see .
+//
+
+import { CtxMenuActionButtonOpener } from "./CtxMenuOpener";
+import skewerSelectionIconArrow from '../../../../assets/icons/skewer_selection-arrow.svg';
+import skewerSelectionIcon from '../../../../assets/icons/skewer_selection_black.svg';
+import edgeSelectionIconArrow from '../../../../assets/icons/edge_selection-arrow.svg';
+import edgeSelectionIcon from '../../../../assets/icons/edge_selection.svg';
+import { View } from "../../View.js";
+
+/******************************************************************************
+ * Aladin Lite project
+ *
+ * File gui/Button/SelectionMode.js
+ *
+ * Class representing a button for bringing up a menu for choosing selection mode.
+ * The appearance of the button changes depending on which selection mode (View.getSelectionMode())
+ * is active.
+ *
+ * There are two possible selection modes, Edge and Skewer, which affect how footprints are
+ * interactively selected.
+ *
+ * In Edge mode (View.SELECTION_MODE_EDGE), footprints are selected by clicking on their edges.
+ *
+ * In Skewer mode (View.SELECTION_MODE_SKEWER), footprints are selecting by clicking anywhere
+ * inside the footprint.
+ *
+ * Using a modifier key (Cmd on Mac, Ctrl otherwise) during select toggles the potential selections:
+ * - If any of the potential selections are not already selected, those objects are added to the current selections.
+ * - If all of the potential selections are already selected, then they are deselected.
+ *
+ * This uses the CSS class aladin-selectionMode-control.
+ *
+ * Author: Tom Donaldson (STScI)
+ *
+ *****************************************************************************/
+ export class SelectionMode extends CtxMenuActionButtonOpener {
+ /**
+ * Class representing a button for bringing up a menu for choosing selection mode.
+ * @param {Aladin} aladin - The aladin instance.
+ */
+ constructor(aladin, options) {
+
+ // If we're on Mac, the modifier key will be Cmd instead of Ctrl.
+ let modifierKey = 'Ctrl';
+ const userAgent = window.navigator.userAgent.toLowerCase();
+ if (userAgent.indexOf('mac') > -1) {
+ modifierKey = 'Cmd';
+ }
+
+ // Set the initial button icon based on the current View selection mode.
+ const initialMode = aladin.view.getSelectionMode();
+ let initialIcon = edgeSelectionIconArrow;
+ if (initialMode === View.SELECTION_MODE_SKEWER) {
+ initialIcon = skewerSelectionIconArrow;
+ }
+
+ super({
+ icon: {
+ size: 'medium',
+ monochrome: true,
+ url: initialIcon,
+ },
+ classList: ['aladin-selectionMode-control'],
+ tooltip: {
+ content: 'Choose the selection mode (' + modifierKey + ' for multiselect)',
+ position: { direction: 'top right', top: '10%', left: '80%' },
+ },
+ ctxMenu: undefined,
+ ...options
+ }, aladin);
+
+ this.aladin = aladin;
+ this.modifierKey = modifierKey;
+ let ctxMenu = this._buildLayout()
+ this.update({ctxMenu})
+ }
+
+ setCustomIcon(icon) {
+ this.update({icon: {
+ size: 'medium',
+ monochrome: true,
+ url: icon
+ }})
+ }
+
+ _buildLayout() {
+ let self = this;
+ let aladin = this.aladin;
+
+ return [
+ {
+ label: {
+ icon: {
+ url: skewerSelectionIcon,
+ monochrome: true,
+ },
+ tooltip: {
+ content: 'Click inside shapes to select. Multiselect with ' + self.modifierKey + '.',
+ position: { direction: 'top right', left: '50%' },
+ },
+ content: "Skewer Selection",
+ },
+ action: (e) => {
+ aladin.view.setSelectionMode(View.SELECTION_MODE_SKEWER);
+ self.setCustomIcon(skewerSelectionIconArrow);
+ },
+ },
+ {
+ label: {
+ icon: {
+ url: edgeSelectionIcon,
+ monochrome: true,
+ },
+ tooltip: {
+ content: 'Click on objects to select. Multiselect with ' + self.modifierKey + '.',
+ position: { direction: 'top right', left: '60%' },
+ },
+ content: "Edge Selection",
+ },
+ action: (e) => {
+ aladin.view.setSelectionMode(View.SELECTION_MODE_EDGE);
+ self.setCustomIcon(edgeSelectionIconArrow);
+ },
+ },
+ ]
+ }
+
+}
+
diff --git a/tutorials/UI.md b/tutorials/UI.md
index 00db33ed..66e20dcd 100644
--- a/tutorials/UI.md
+++ b/tutorials/UI.md
@@ -11,6 +11,7 @@ There are distincts CSS class names for users wanting to personnalize the defaul
* `aladin-simbadPointer-control` targets the Simbad pointer control button
* `aladin-grid-control` targets the coordinate grid trigger button
* `aladin-settings-control` targets the settings menu opener button
+* `aladin-selectionMode-control` targets the selection mode menu opener button
* `aladin-share-control` targets the share menu opener button
* `aladin-projection-control` targets the projection selector button
* `aladin-stack-box` targets the stack box