import { Rectangle, useMap, Popup } from 'react-leaflet'; import React, { useRef, useState } from 'react'; // use new ReactLeafletDriftMarker instead of DriftMarker import ReactLeafletDriftMarker from 'react-leaflet-drift-marker'; import { MarkerProps, Bounds } from './Types'; import { LeafletMouseEvent, LatLngBounds } from 'leaflet'; export interface BoundsDriftMarkerProps extends MarkerProps { bounds: Bounds; duration: number; // A class to add to the popup element popupClass?: string; } // Which direction the popup should come out from the marker export type PopupOrientation = 'up' | 'down' | 'left' | 'right'; /* after testing various approaches, it is found that sending mouse event props from story, like done in previous approach, * seems to conflict with other mouse event like grey-box. Also, using function form of the top-marker event did not work either. * The only way to work it out is to directly set function contents (e.g., e.target....) inside onMouseOver/Out * For this reason, marker's props are adjusted without sending mouse event functions, but implemented here directly. */ export default function BoundsDriftMarker({ position, bounds, icon, duration, showPopup, popupContent, popupClass, }: BoundsDriftMarkerProps) { const [displayBounds, setDisplayBounds] = useState(false); const map = useMap(); const boundingBox = new LatLngBounds([ [bounds.southWest.lat, bounds.southWest.lng], [bounds.northEast.lat, bounds.northEast.lng], ]); const markerRef = useRef(); const popupRef = useRef(); const popupOrientationRef = useRef('up'); // Update popupOrientationRef based on whether the marker is close to the map edge. // Does not actually change the popup, just the ref. // at react-leaflet-drift-marker, there is no leafletElement // rather, it seems to be exposed to upper level // i.e., current.leafletElement._icon -> current._icon const updatePopupOrientationRef = () => { if (popupContent && map) { // Figure out if we're close to the map edge const mapRect = map.getContainer().getBoundingClientRect(); //DKDK need as ? // const markerRect = markerRef.current.leafletElement._icon.getBoundingClientRect() as DOMRect; const markerRect = markerRef.current._icon.getBoundingClientRect(); const markerCenterX = (markerRect.left + markerRect.right) / 2; if (markerRect.top - mapRect.top < popupContent.size.height) { popupOrientationRef.current = 'down'; } else if (markerCenterX - mapRect.left < popupContent.size.width / 2) { popupOrientationRef.current = 'right'; } else if (mapRect.right - markerCenterX < popupContent.size.width / 2) { popupOrientationRef.current = 'left'; } else { popupOrientationRef.current = 'up'; } } }; // Change the popup's orientation const orientPopup = (orientation: PopupOrientation) => { if (popupRef.current) { const popupDOMNode = popupRef.current._container; //DKDK need as ? // const popupDOMNode = popupRef.current.leafletElement // ._container as HTMLElement; if (popupDOMNode) { popupDOMNode.classList.remove( 'popup-up', 'popup-down', 'popup-left', 'popup-right' ); const angle = { up: 0, right: 90, down: 180, left: -90, }[orientation]; // Have to add rotate here rather than in CSS to preserve the existing // transform, which is variable if (!popupDOMNode.style.transform.includes('rotate')) popupDOMNode.style.transform += ` rotate(${angle}deg)`; popupDOMNode.classList.add('popup-' + orientation); } } }; // Leaflet likes to change the popup's styling often, which throws off our // custom orientation styling. To fix this, we will watch for changes to the // popup's styling. If the transform property has no rotation, then our // changes have been wiped. In that case, redo them. const observer = new MutationObserver((mutationRecord) => { const popupDOMNode = mutationRecord[0].target as HTMLElement; if (!popupDOMNode.style.transform.includes('rotate')) { // When this observer callback is invoked, we end up getting to this // point multiple times in quick succession. This doesn't seem to cause a // performance issue currently, but it could be worth optimizing in the // future. orientPopup(popupOrientationRef.current); } }); const handlePopupOpen = () => { // Orient the popup correctly updatePopupOrientationRef(); orientPopup(popupOrientationRef.current); // Watch for changes to the popup's styling const popupDOMNode = popupRef.current._container as HTMLElement; observer.observe(popupDOMNode, { attributeFilter: ['style'] }); }; const handlePopupClose = () => { observer.disconnect(); // Have to do this again because styling is changed again on close orientPopup(popupOrientationRef.current); }; const popup = popupContent && ( handlePopupOpen()} onClose={() => handlePopupClose()} > {popupContent.content} ); const handleMouseOver = (e: LeafletMouseEvent) => { e.target._icon.classList.add('top-marker'); // marker on top if (showPopup && popupContent) { e.target.openPopup(); } else { // there is a conflict with popup so bounds only shows no popup case setDisplayBounds(true); // Display bounds rectangle } }; const handleMouseOut = (e: LeafletMouseEvent) => { e.target._icon.classList.remove('top-marker'); // remove marker on top if (showPopup && popupContent) { e.target.closePopup(); } else { setDisplayBounds(false); // Remove bounds rectangle } }; const handleClick = (e: LeafletMouseEvent) => { // Sometimes clicking throws off the popup's orientation, so reorient it orientPopup(popupOrientationRef.current); // Default popup behavior is to open on marker click // Prevent by immediately closing it e.target.closePopup(); }; const handleDoubleClick = () => { if (map) { map.fitBounds(boundingBox); } }; // DriftMarker misbehaves if icon=undefined is provided // is this the most elegant way? const optionalIconProp = icon ? { icon } : {}; return ( // handleClick(e), mouseover: (e: LeafletMouseEvent) => handleMouseOver(e), mouseout: (e: LeafletMouseEvent) => handleMouseOut(e), dblclick: handleDoubleClick, }} {...optionalIconProp} > {displayBounds ? ( ) : null} {showPopup && popup} ); }