314 lines
8.2 KiB
TypeScript
314 lines
8.2 KiB
TypeScript
import react, { useRef } from 'react';
|
|
import { useAtom } from 'jotai';
|
|
|
|
import { Point } from './types';
|
|
import './Handler.css';
|
|
import { handlersConfig } from './config';
|
|
import { relativeCoordinateSystemAtom, Transformation } from './LiveMap';
|
|
|
|
/**
|
|
*
|
|
* See also {@link components/map/Map!relativeCoordinateSystemAtom}.
|
|
*
|
|
*/
|
|
export interface HandlersProperties {
|
|
transformMap: (t: Transformation) => void;
|
|
}
|
|
/**
|
|
*
|
|
* @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 `<Map>` is done through {@link components/map/Map!relativeCoordinateSystemAtom}.
|
|
*/
|
|
export const Handlers: react.FC<HandlersProperties> = (
|
|
props: HandlersProperties
|
|
) => {
|
|
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 &&
|
|
(event.clientX - mouseState.current.starting.x) ** 2 +
|
|
(event.clientY - mouseState.current.starting.y) ** 2 >
|
|
100
|
|
) {
|
|
genericHandler(event);
|
|
if (mouseState.current.down) {
|
|
props.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);
|
|
props.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
|
|
) {
|
|
props.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<TouchState>(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,
|
|
};
|
|
props.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);
|
|
props.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 (
|
|
<div
|
|
className='handler'
|
|
role='presentation'
|
|
onMouseDown={mouseDownHandler}
|
|
onMouseMove={mouseMoveHandler}
|
|
onMouseUp={mouseUpHandler}
|
|
onMouseLeave={mouseLeaveHandler}
|
|
onDoubleClick={doubleClickHandler}
|
|
onWheel={wheelEventHandler}
|
|
onTouchEnd={touchEndHandler}
|
|
onTouchCancel={touchCancelHandler}
|
|
onTouchStart={touchStartHandler}
|
|
onTouchMove={touchMoveHandler}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default Handlers;
|