import { Controller } from './Controller.js' import { CoordinateSystem } from './CoordinateSystem.js'; import { addSignals } from './Signals.js' /** * ControllerPanZoom handles pan, zoom, and interaction events in a canvas element to manipulate camera parameters. * It supports multiple interaction methods including: * - Mouse drag for panning * - Mouse wheel for zooming * - Touch gestures (pinch to zoom) * - Double tap to zoom * * The controller maintains state for ongoing pan and zoom operations and can be configured * to use different coordinate systems (HTML or GL) for calculations. * * @extends Controller * @fires ControllerPanZoom#nowheel - Emitted when a wheel event is received but ctrl key is required and not pressed */ class ControllerPanZoom extends Controller { /** * Creates a new ControllerPanZoom instance. * @param {Camera} camera - The camera object to control * @param {Object} [options] - Configuration options * @param {number} [options.zoomAmount=1.2] - The zoom multiplier for wheel/double-tap events * @param {boolean} [options.controlZoom=false] - If true, requires Ctrl key to be pressed for zoom operations * @param {boolean} [options.useGLcoords=false] - If true, uses WebGL coordinate system instead of HTML * @param {number} [options.panDelay] - Delay for pan animations * @param {number} [options.zoomDelay] - Delay for zoom animations */ constructor(camera, options) { super(options); this.camera = camera; this.zoomAmount = 1.2; //for wheel or double tap event this.controlZoom = false; //require control+wheel to zoom this.panning = false; //true if in the middle of a pan this.initialTransform = null; this.startMouse = null; this.zooming = false; //true if in the middle of a pinch this.initialDistance = 0.0; this.useGLcoords = false; if (options) Object.assign(this, options); } /** * Handles the start of a pan operation * @private * @param {PointerEvent} e - The pointer event that initiated the pan */ panStart(e) { if (!this.active || this.panning || !this.activeModifiers.includes(this.modifierState(e))) return; this.panning = true; this.startMouse = CoordinateSystem.fromCanvasHtmlToViewport({ x: e.offsetX, y: e.offsetY }, this.camera, this.useGLcoords); let now = performance.now(); this.initialTransform = this.camera.getCurrentTransform(now); this.camera.target = this.initialTransform.copy(); //stop animation. e.preventDefault(); } /** * Updates camera position during a pan operation * @private * @param {PointerEvent} e - The pointer event with new coordinates */ panMove(e) { if (!this.panning) return; let m = this.initialTransform; const p = CoordinateSystem.fromCanvasHtmlToViewport({ x: e.offsetX, y: e.offsetY }, this.camera, this.useGLcoords); let dx = (p.x - this.startMouse.x); let dy = (p.y - this.startMouse.y); this.camera.setPosition(this.panDelay, m.x + dx, m.y + dy, m.z, m.a); } /** * Ends the current pan operation * @private * @param {PointerEvent} e - The pointer event that ended the pan */ panEnd(e) { this.panning = false; } /** * Calculates the Euclidean distance between two points * @private * @param {Object} e1 - First point with x, y coordinates * @param {Object} e2 - Second point with x, y coordinates * @returns {number} The distance between the points */ distance(e1, e2) { return Math.sqrt(Math.pow(e1.x - e2.x, 2) + Math.pow(e1.y - e2.y, 2)); } /** * Initializes a pinch zoom operation * @private * @param {TouchEvent} e1 - First touch point * @param {TouchEvent} e2 - Second touch point */ pinchStart(e1, e2) { this.zooming = true; this.initialDistance = Math.max(30, this.distance(e1, e2)); e1.preventDefault(); //e2.preventDefault(); //TODO this is optional? } /** * Updates zoom level during a pinch operation * @private * @param {TouchEvent} e1 - First touch point * @param {TouchEvent} e2 - Second touch point */ pinchMove(e1, e2) { if (!this.zooming) return; let rect1 = e1.target.getBoundingClientRect(); let offsetX1 = e1.clientX - rect1.left; let offsetY1 = e1.clientY - rect1.top; let rect2 = e2.target.getBoundingClientRect(); let offsetX2 = e2.clientX - rect2.left; let offsetY2 = e2.clientY - rect2.top; const scale = this.distance(e1, e2); // FIXME CHECK ON TOUCH SCREEN //const pos = this.camera.mapToScene((offsetX1 + offsetX2)/2, (offsetY1 + offsetY2)/2, this.camera.getCurrentTransform(performance.now())); const pos = CoordinateSystem.fromCanvasHtmlToScene({ x: (offsetX1 + offsetX2) / 2, y: (offsetY1 + offsetY2) / 2 }, this.camera, this.useGLcoords); const dz = scale / this.initialDistance; this.camera.deltaZoom(this.zoomDelay, dz, pos.x, pos.y); this.initialDistance = scale; e1.preventDefault(); } /** * Ends the current pinch zoom operation * @private * @param {TouchEvent} e - The touch event that ended the pinch * @param {number} x - The x coordinate of the pinch center * @param {number} y - The y coordinate of the pinch center * @param {number} scale - The final scale factor */ pinchEnd(e, x, y, scale) { this.zooming = false; e.preventDefault(); } /** * Handles mouse wheel events for zooming * @private * @param {WheelEvent} e - The wheel event * @fires ControllerPanZoom#nowheel */ mouseWheel(e) { if (this.controlZoom && !e.ctrlKey) { this.emit('nowheel'); return; } let delta = -e.deltaY / 53; //const pos = this.camera.mapToScene(e.offsetX, e.offsetY, this.camera.getCurrentTransform(performance.now())); const pos = CoordinateSystem.fromCanvasHtmlToScene({ x: e.offsetX, y: e.offsetY }, this.camera, this.useGLcoords); const dz = Math.pow(this.zoomAmount, delta); this.camera.deltaZoom(this.zoomDelay, dz, pos.x, pos.y); e.preventDefault(); } /** * Handles double tap events for zooming * @private * @param {PointerEvent} e - The pointer event representing the double tap */ fingerDoubleTap(e) { } fingerDoubleTap(e) { if (!this.active || !this.activeModifiers.includes(this.modifierState(e))) return; //const pos = this.camera.mapToScene(e.offsetX, e.offsetY, this.camera.getCurrentTransform(performance.now())); const pos = CoordinateSystem.fromCanvasHtmlToScene({ x: e.offsetX, y: e.offsetY }, this.camera, this.useGLcoords); const dz = this.zoomAmount; this.camera.deltaZoom(this.zoomDelay, dz, pos.x, pos.y); } } addSignals(ControllerPanZoom, 'nowheel'); export { ControllerPanZoom }