import React, { useRef, useEffect, useState } from "react"; import { Box } from "theme-ui"; import ReactResizeDetector from "react-resize-detector"; import { Stage, Layer, Image, Group } from "react-konva"; import Konva from "konva"; import useMapImage from "../../hooks/useMapImage"; import usePreventOverscroll from "../../hooks/usePreventOverscroll"; import useStageInteraction from "../../hooks/useStageInteraction"; import useImageCenter from "../../hooks/useImageCenter"; import usePreventContextMenu from "../../hooks/usePreventContextMenu"; import { getGridMaxZoom } from "../../helpers/grid"; import KonvaBridge from "../../helpers/KonvaBridge"; import { MapInteractionEmitter, MapInteractionProvider, } from "../../contexts/MapInteractionContext"; import { useMapStage } from "../../contexts/MapStageContext"; import { GridProvider } from "../../contexts/GridContext"; import { useKeyboard } from "../../contexts/KeyboardContext"; import shortcuts from "../../shortcuts"; import { Map, MapToolId } from "../../types/Map"; import { MapState } from "../../types/MapState"; type SelectedToolChangeEventHanlder = (tool: MapToolId) => void; type MapInteractionProps = { map: Map | null; mapState: MapState | null; children?: React.ReactNode; controls: React.ReactNode; selectedToolId: MapToolId; onSelectedToolChange: SelectedToolChangeEventHanlder; }; function MapInteraction({ map, mapState, children, controls, selectedToolId, onSelectedToolChange, }: MapInteractionProps) { const [mapImage, mapImageStatus] = useMapImage(map); const [mapLoaded, setMapLoaded] = useState(false); useEffect(() => { if (!map || !mapState || mapState.mapId !== map.id) { setMapLoaded(false); } else if (mapImageStatus === "loaded") { setMapLoaded(true); } }, [mapImageStatus, map, mapState]); const [stageWidth, setStageWidth] = useState(1); const [stageHeight, setStageHeight] = useState(1); const [stageScale, setStageScale] = useState(1); const [preventMapInteraction, setPreventMapInteraction] = useState(false); // Avoid state udpates when panning the map by using a ref and updating the konva element directly const stageTranslateRef = useRef({ x: 0, y: 0 }); const mapStageRef = useMapStage(); const mapLayerRef = useRef(null); const mapImageRef = useRef(null); function handleResize(width?: number, height?: number) { if (width && height && width > 0 && height > 0) { setStageWidth(width); setStageHeight(height); } } const containerRef = useRef(null); usePreventOverscroll(containerRef); usePreventContextMenu(containerRef); const [mapWidth, mapHeight] = useImageCenter( map, mapStageRef, stageWidth, stageHeight, stageTranslateRef, setStageScale, mapLayerRef, containerRef ); const previousSelectedToolRef = useRef(selectedToolId); const [currentMouseButtons, setCurentMouseButtons] = useState(0); const [interactionEmitter] = useState(new MapInteractionEmitter()); useStageInteraction( mapStageRef, stageScale, setStageScale, stageTranslateRef, mapLayerRef, getGridMaxZoom(map?.grid), selectedToolId, preventMapInteraction, { onPinchStart: () => { // Change to move tool when pinching and zooming previousSelectedToolRef.current = selectedToolId; onSelectedToolChange("move"); }, onPinchEnd: () => { onSelectedToolChange(previousSelectedToolRef.current); }, onDrag: (props) => { const { first, last, buttons } = props; if (buttons !== currentMouseButtons) { setCurentMouseButtons(buttons); } if (first) { interactionEmitter.emit("dragStart", props); } else if (last) { interactionEmitter.emit("dragEnd", props); } else { interactionEmitter.emit("drag", props); } }, } ); function handleKeyDown(event: KeyboardEvent) { // Change to move tool when pressing space if (shortcuts.move(event) && selectedToolId === "move") { // Stop active state on move icon from being selected event.preventDefault(); } if (map && shortcuts.move(event) && selectedToolId !== "move") { event.preventDefault(); previousSelectedToolRef.current = selectedToolId; onSelectedToolChange("move"); } if (!event.repeat && shortcuts.move(event) && selectedToolId === "move") { previousSelectedToolRef.current = "move"; } } function handleKeyUp(event: KeyboardEvent) { if (shortcuts.move(event) && selectedToolId === "move") { onSelectedToolChange(previousSelectedToolRef.current); } } useKeyboard(handleKeyDown, handleKeyUp); function getCursorForTool(tool: MapToolId) { if (currentMouseButtons === 2) { return "crosshair"; } else if (currentMouseButtons > 2) { return "move"; } switch (tool) { case "move": return "move"; case "fog": case "drawing": return "crosshair"; case "measure": case "pointer": case "note": case "select": return "crosshair"; default: return "default"; } } const mapInteraction = { stageScale, stageWidth, stageHeight, setPreventMapInteraction, mapWidth, mapHeight, interactionEmitter, }; return ( ( {children} )} > {mapLoaded && children} {controls} ); } export default MapInteraction;