Files
grungnet/src/components/map/MapInteraction.js

184 lines
5.4 KiB
JavaScript
Raw Normal View History

2020-05-21 16:46:50 +10:00
import React, { useRef, useEffect, useState, useContext } from "react";
import { Box } from "theme-ui";
2020-05-21 16:46:50 +10:00
import { useGesture } from "react-use-gesture";
import ReactResizeDetector from "react-resize-detector";
import useImage from "use-image";
import { Stage, Layer, Image } from "react-konva";
2020-05-21 16:46:50 +10:00
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
import useDataSource from "../../helpers/useDataSource";
import { mapSources as defaultMapSources } from "../../maps";
2020-05-21 16:46:50 +10:00
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
import MapStageContext from "../../contexts/MapStageContext";
import AuthContext from "../../contexts/AuthContext";
2020-04-30 22:08:03 +10:00
2020-04-30 16:16:12 +10:00
const zoomSpeed = -0.001;
const minZoom = 0.1;
const maxZoom = 5;
2020-05-22 13:47:11 +10:00
function MapInteraction({ map, children, controls, selectedToolId }) {
2020-05-21 16:46:50 +10:00
const mapSource = useDataSource(map, defaultMapSources);
const [mapSourceImage] = useImage(mapSource);
2020-05-21 16:46:50 +10:00
const [stageWidth, setStageWidth] = useState(1);
const [stageHeight, setStageHeight] = useState(1);
const [stageScale, setStageScale] = useState(1);
const [stageTranslate, setStageTranslate] = useState({ x: 0, y: 0 });
2020-05-22 13:47:11 +10:00
// "none" | "first" | "dragging" | "last"
const [stageDragState, setStageDragState] = useState("none");
2020-05-21 16:46:50 +10:00
const [preventMapInteraction, setPreventMapInteraction] = useState(false);
2020-05-22 13:47:11 +10:00
const [mapDragPosition, setMapDragPosition] = useState({ x: 0, y: 0 });
2020-05-21 16:46:50 +10:00
const stageWidthRef = useRef(stageWidth);
const stageHeightRef = useRef(stageHeight);
const stageScaleRef = useRef(stageScale);
const stageTranslateRef = useRef(stageTranslate);
useEffect(() => {
2020-05-21 16:46:50 +10:00
if (map) {
const mapHeight = stageWidthRef.current * (map.height / map.width);
setStageTranslate({ x: 0, y: -(mapHeight - stageHeightRef.current) / 2 });
}
}, [map]);
2020-05-22 13:47:11 +10:00
// Convert a client space XY to be normalized to the map image
function getMapDragPosition(xy) {
const [x, y] = xy;
const container = containerRef.current;
const mapImage = mapImageRef.current;
if (container && mapImage) {
const containerRect = container.getBoundingClientRect();
const mapRect = mapImage.getClientRect();
const offsetX = x - containerRect.left - mapRect.x;
const offsetY = y - containerRect.top - mapRect.y;
const normalizedX = offsetX / mapRect.width;
const normalizedY = offsetY / mapRect.height;
return { x: normalizedX, y: normalizedY };
}
}
2020-05-21 16:46:50 +10:00
const bind = useGesture({
onWheel: ({ delta }) => {
const newScale = Math.min(
Math.max(stageScale - delta[1] * zoomSpeed, minZoom),
maxZoom
);
setStageScale(newScale);
stageScaleRef.current = newScale;
},
2020-05-22 13:47:11 +10:00
onDrag: ({ delta, xy, first, last }) => {
if (preventMapInteraction) {
return;
}
setMapDragPosition(getMapDragPosition(xy));
setStageDragState(first ? "first" : last ? "last" : "dragging");
const [dx, dy] = delta;
if (selectedToolId === "pan") {
2020-05-21 16:46:50 +10:00
const newTranslate = {
2020-05-22 13:47:11 +10:00
x: stageTranslate.x + dx / stageScale,
y: stageTranslate.y + dy / stageScale,
2020-05-21 16:46:50 +10:00
};
setStageTranslate(newTranslate);
stageTranslateRef.current = newTranslate;
}
},
2020-05-22 13:47:11 +10:00
onDragEnd: () => {
setStageDragState("none");
},
2020-05-21 16:46:50 +10:00
});
function handleResize(width, height) {
setStageWidth(width);
setStageHeight(height);
stageWidthRef.current = width;
stageHeightRef.current = height;
}
function getCursorForTool(tool) {
switch (tool) {
case "pan":
return "move";
case "fog":
case "brush":
case "shape":
return "crosshair";
default:
return "default";
}
}
2020-05-21 16:46:50 +10:00
const containerRef = useRef();
usePreventOverscroll(containerRef);
2020-05-21 16:46:50 +10:00
const mapWidth = stageWidth;
const mapHeight = map ? stageWidth * (map.height / map.width) : stageHeight;
2020-05-21 16:46:50 +10:00
const mapStageRef = useContext(MapStageContext);
2020-05-22 13:47:11 +10:00
const mapImageRef = useRef();
2020-05-21 16:46:50 +10:00
const auth = useContext(AuthContext);
2020-05-21 16:46:50 +10:00
const mapInteraction = {
stageTranslate,
stageScale,
stageWidth,
stageHeight,
2020-05-22 13:47:11 +10:00
stageDragState,
2020-05-21 16:46:50 +10:00
setPreventMapInteraction,
mapWidth,
mapHeight,
2020-05-22 13:47:11 +10:00
mapDragPosition,
2020-05-21 16:46:50 +10:00
};
return (
<Box
sx={{
flexGrow: 1,
position: "relative",
cursor: getCursorForTool(selectedToolId),
}}
2020-05-21 16:46:50 +10:00
ref={containerRef}
{...bind()}
className="map"
>
2020-05-21 16:46:50 +10:00
<ReactResizeDetector handleWidth handleHeight onResize={handleResize}>
<Stage
width={stageWidth}
height={stageHeight}
scale={{ x: stageScale, y: stageScale }}
x={stageWidth / 2}
y={stageHeight / 2}
offset={{ x: stageWidth / 2, y: stageHeight / 2 }}
ref={mapStageRef}
>
<Layer x={stageTranslate.x} y={stageTranslate.y}>
<Image
image={mapSourceImage}
width={mapWidth}
height={mapHeight}
id="mapImage"
2020-05-22 13:47:11 +10:00
ref={mapImageRef}
/>
2020-05-21 16:46:50 +10:00
{/* Forward auth context to konva elements */}
<AuthContext.Provider value={auth}>
<MapInteractionProvider value={mapInteraction}>
{children}
</MapInteractionProvider>
</AuthContext.Provider>
</Layer>
</Stage>
</ReactResizeDetector>
<MapInteractionProvider value={mapInteraction}>
{controls}
</MapInteractionProvider>
</Box>
);
}
export default MapInteraction;