import react, { useRef } from 'react'; import { useAtom } from 'jotai'; import { Point } from './types'; import './Handler.css'; import { handlersConfig } from './config'; import { relativeCoordinateSystemAtom } from './Map'; /** * * See also {@link components/map/Map!relativeCoordinateSystemAtom}. * */ export interface HandlersProperties {} /** * * @param props * @returns A div with the following handlers * * mouseLeave, mouseDown and mouseUp to track the mouse state * * mouseMove to shift the map if the mouse is down * * doubleClick to zoom in * * wheel to zoom in and out * * touchStart, touchEnd and touchCancel to track touch state * * touchMove to shift the map (single finger) or shift and zoom (two fingers). * * Communication with the parent `` is done through {@link components/map/Map!relativeCoordinateSystemAtom}. */ export const Handlers: react.FC = ( props: HandlersProperties ) => { const [, transformMap] = useAtom(relativeCoordinateSystemAtom); const genericHandler = (event: any) => { // console.log(`Log - Event: ${event.type}`); // if (event.clientX !== undefined) { // console.log( // `Mouse : ${event.clientX}, ${event.clientY}, target: ${event.target}` // ); // console.log( // `mouseState: ' ${JSON.stringify(mouseState)} (+${ // Date.now() - mouseState.timestamp // }ms) ` // ); // return; //} }; /** * * Mouse handlers * */ const initialMouseState = { down: false, starting: { x: -1, y: -1 }, timestamp: 0, }; const mouseState = useRef(initialMouseState); const mouseLeaveHandler = (event: any) => { genericHandler(event); mouseState.current = initialMouseState; }; const mouseDownHandler = (event: any) => { genericHandler(event); mouseState.current = { down: true, starting: { x: event.clientX, y: event.clientY }, timestamp: 0, }; }; const mouseUpHandler = (event: any) => { genericHandler(event); mouseState.current = initialMouseState; }; const mouseMoveHandler = (event: any) => { if ( mouseState.current.down && Date.now() - mouseState.current.timestamp > handlersConfig.mouseMoveThrottleDelay ) { genericHandler(event); if (mouseState.current.down) { transformMap({ deltaShift: { x: event.clientX - mouseState.current.starting.x, y: event.clientY - mouseState.current.starting.y, }, deltaZoom: null, zoomCenter: null, }); mouseState.current = { down: true, starting: { x: event.clientX, y: event.clientY, }, timestamp: Date.now(), }; } } }; /** * * Double click * */ const doubleClickHandler = (event: any) => { genericHandler(event); transformMap({ deltaShift: null, deltaZoom: Math.SQRT2, zoomCenter: { x: event.clientX, y: event.clientY, }, }); }; /** * * Wheel handler * */ const initialWheelState = { timestamp: 0, }; const wheelState = useRef(initialWheelState); const wheelEventHandler = (event: any) => { genericHandler(event); if ( event.deltaMode === WheelEvent.DOM_DELTA_PIXEL && Date.now() - wheelState.current.timestamp > handlersConfig.wheelThrottleDelay ) { transformMap({ deltaShift: null, deltaZoom: event.deltaY < 0 ? Math.SQRT2 : Math.SQRT1_2, zoomCenter: { x: event.clientX, y: event.clientY, }, }); wheelState.current = { timestamp: Date.now(), }; } }; /** * * Touch handlers * */ interface TouchState { state: 'up' | 'pointer' | 'double'; touches: Point[]; distance: number; timestamp: number; } const initialTouchState: TouchState = { state: 'up', touches: [], distance: -1, timestamp: 0, }; const touchState = useRef(initialTouchState); const touchCancelHandler = (event: any) => { genericHandler(event); touchState.current = initialTouchState; }; const touchEndHandler = touchCancelHandler; const touchStartHandler = (event: any) => { genericHandler(event); if (event.touches.length === 2) { touchState.current = { state: 'double', touches: [ { x: event.touches[0].screenX, y: event.touches[0].screenY }, { x: event.touches[1].screenX, y: event.touches[1].screenY }, ], distance: Math.sqrt( (event.touches[0].screenX - event.touches[1].screenX) ** 2 + (event.touches[0].screenY - event.touches[1].screenY) ** 2 ), timestamp: Date.now(), }; } else if (event.touches.length === 1) { touchState.current = { state: 'pointer', touches: [{ x: event.touches[0].screenX, y: event.touches[0].screenY }], timestamp: Date.now(), distance: -1, }; } }; const touchMoveHandler = (event: any) => { if ( touchState.current.state === 'double' && Date.now() - touchState.current.timestamp > handlersConfig.doubleTouchMoveThrottleDelay ) { if (event.touches.length === 2) { genericHandler(event); const newDistance = Math.sqrt( (event.touches[0].screenX - event.touches[1].screenX) ** 2 + (event.touches[0].screenY - event.touches[1].screenY) ** 2 ); const factor = newDistance / touchState.current.distance; // console.log(`+++++++++ ZOOM Factor is ${factor} ++++++++++`); touchState.current = { state: 'double', touches: [ { x: event.touches[0].screenX, y: event.touches[0].screenY }, { x: event.touches[1].screenX, y: event.touches[1].screenY }, ], distance: newDistance, timestamp: Date.now(), }; const previousCenter = { x: (touchState.current.touches[0].x + touchState.current.touches[1].x) / 2, y: (touchState.current.touches[0].y + touchState.current.touches[1].y) / 2, }; const currentCenter = { x: (event.touches[0].screenX + event.touches[1].screenX) / 2, y: (event.touches[0].screenY + event.touches[1].screenY) / 2, }; transformMap({ deltaShift: { x: currentCenter.x - previousCenter.x, y: currentCenter.y - previousCenter.y, }, deltaZoom: factor, zoomCenter: { x: currentCenter.x - previousCenter.x, y: currentCenter.y - previousCenter.y, }, }); } } else if ( touchState.current.state === 'pointer' && Date.now() - touchState.current.timestamp > handlersConfig.singleTouchMoveThrottleDelay ) { if (event.touches.length === 1) { genericHandler(event); transformMap({ deltaShift: { x: event.touches[0].screenX - touchState.current.touches[0].x, y: event.touches[0].screenY - touchState.current.touches[0].y, }, deltaZoom: null, zoomCenter: null, }); touchState.current = { state: 'pointer', touches: [ { x: event.touches[0].screenX, y: event.touches[0].screenY, }, ], timestamp: Date.now(), distance: -1, }; } } }; return (
); }; export default Handlers;