2020-04-27 17:29:46 +10:00
|
|
|
import React, { useRef, useEffect } from "react";
|
|
|
|
|
import { Box } from "theme-ui";
|
|
|
|
|
import interact from "interactjs";
|
2020-04-30 15:45:20 +10:00
|
|
|
import normalizeWheel from "normalize-wheel";
|
2020-04-27 17:29:46 +10:00
|
|
|
|
2020-04-29 18:21:44 +10:00
|
|
|
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
|
|
|
|
|
|
2020-04-30 16:16:12 +10:00
|
|
|
const zoomSpeed = -0.001;
|
2020-04-27 17:29:46 +10:00
|
|
|
const minZoom = 0.1;
|
|
|
|
|
const maxZoom = 5;
|
|
|
|
|
|
2020-04-27 17:40:36 +10:00
|
|
|
function MapInteraction({ map, aspectRatio, isEnabled, children, controls }) {
|
2020-04-27 17:29:46 +10:00
|
|
|
const mapContainerRef = useRef();
|
|
|
|
|
const mapMoveContainerRef = useRef();
|
|
|
|
|
const mapTranslateRef = useRef({ x: 0, y: 0 });
|
|
|
|
|
const mapScaleRef = useRef(1);
|
|
|
|
|
function setTranslateAndScale(newTranslate, newScale) {
|
|
|
|
|
const moveContainer = mapMoveContainerRef.current;
|
|
|
|
|
moveContainer.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px) scale(${newScale})`;
|
|
|
|
|
mapScaleRef.current = newScale;
|
|
|
|
|
mapTranslateRef.current = newTranslate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
function handleMove(event, isGesture) {
|
|
|
|
|
const scale = mapScaleRef.current;
|
|
|
|
|
const translate = mapTranslateRef.current;
|
|
|
|
|
|
|
|
|
|
let newScale = scale;
|
|
|
|
|
let newTranslate = translate;
|
|
|
|
|
|
|
|
|
|
if (isGesture) {
|
|
|
|
|
newScale = Math.max(Math.min(scale + event.ds, maxZoom), minZoom);
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-27 17:40:36 +10:00
|
|
|
if (isEnabled || isGesture) {
|
2020-04-27 17:29:46 +10:00
|
|
|
newTranslate = {
|
|
|
|
|
x: translate.x + event.dx,
|
|
|
|
|
y: translate.y + event.dy,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
setTranslateAndScale(newTranslate, newScale);
|
|
|
|
|
}
|
|
|
|
|
const mapInteract = interact(".map")
|
|
|
|
|
.gesturable({
|
|
|
|
|
listeners: {
|
|
|
|
|
move: (e) => handleMove(e, true),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
.draggable({
|
|
|
|
|
inertia: true,
|
|
|
|
|
listeners: {
|
|
|
|
|
move: (e) => handleMove(e, false),
|
|
|
|
|
},
|
|
|
|
|
cursorChecker: () => {
|
2020-04-27 17:40:36 +10:00
|
|
|
return isEnabled && map ? "move" : "default";
|
2020-04-27 17:29:46 +10:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
.on("doubletap", (event) => {
|
|
|
|
|
event.preventDefault();
|
2020-04-27 17:40:36 +10:00
|
|
|
if (isEnabled) {
|
2020-04-27 17:29:46 +10:00
|
|
|
setTranslateAndScale({ x: 0, y: 0 }, 1);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
mapInteract.unset();
|
|
|
|
|
};
|
2020-04-27 17:40:36 +10:00
|
|
|
}, [isEnabled, map]);
|
2020-04-27 17:29:46 +10:00
|
|
|
|
|
|
|
|
// Reset map transform when map changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setTranslateAndScale({ x: 0, y: 0 }, 1);
|
|
|
|
|
}, [map]);
|
|
|
|
|
|
|
|
|
|
// Bind the wheel event of the map via a ref
|
|
|
|
|
// in order to support non-passive event listening
|
|
|
|
|
// to allow the track pad zoom to be interrupted
|
|
|
|
|
// see https://github.com/facebook/react/issues/14856
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const mapContainer = mapContainerRef.current;
|
|
|
|
|
|
|
|
|
|
function handleZoom(event) {
|
|
|
|
|
// Stop overscroll on chrome and safari
|
|
|
|
|
// also stop pinch to zoom on chrome
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
2020-04-30 15:45:20 +10:00
|
|
|
// Try and normalize the wheel event to prevent OS differences for zoom speed
|
|
|
|
|
const normalized = normalizeWheel(event);
|
|
|
|
|
|
2020-04-27 17:29:46 +10:00
|
|
|
const scale = mapScaleRef.current;
|
|
|
|
|
const translate = mapTranslateRef.current;
|
|
|
|
|
|
2020-04-30 15:45:20 +10:00
|
|
|
const deltaY = normalized.pixelY * zoomSpeed;
|
2020-04-27 17:29:46 +10:00
|
|
|
const newScale = Math.max(Math.min(scale + deltaY, maxZoom), minZoom);
|
|
|
|
|
|
|
|
|
|
setTranslateAndScale(translate, newScale);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mapContainer) {
|
|
|
|
|
mapContainer.addEventListener("wheel", handleZoom, {
|
|
|
|
|
passive: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
if (mapContainer) {
|
|
|
|
|
mapContainer.removeEventListener("wheel", handleZoom);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Box
|
|
|
|
|
className="map"
|
|
|
|
|
sx={{
|
|
|
|
|
flexGrow: 1,
|
|
|
|
|
position: "relative",
|
|
|
|
|
overflow: "hidden",
|
|
|
|
|
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
|
|
|
|
userSelect: "none",
|
|
|
|
|
touchAction: "none",
|
|
|
|
|
}}
|
|
|
|
|
bg="background"
|
|
|
|
|
ref={mapContainerRef}
|
|
|
|
|
>
|
|
|
|
|
<Box
|
|
|
|
|
sx={{
|
|
|
|
|
position: "relative",
|
|
|
|
|
top: "50%",
|
|
|
|
|
left: "50%",
|
|
|
|
|
transform: "translate(-50%, -50%)",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Box ref={mapMoveContainerRef}>
|
|
|
|
|
<Box
|
|
|
|
|
sx={{
|
|
|
|
|
width: "100%",
|
|
|
|
|
height: 0,
|
|
|
|
|
paddingBottom: `${(1 / aspectRatio) * 100}%`,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2020-04-29 18:21:44 +10:00
|
|
|
<MapInteractionProvider
|
|
|
|
|
value={{
|
|
|
|
|
translateRef: mapTranslateRef,
|
|
|
|
|
scaleRef: mapScaleRef,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</MapInteractionProvider>
|
2020-04-27 17:29:46 +10:00
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
{controls}
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default MapInteraction;
|