Skip to content

Commit

Permalink
feat: SRS-555: Implementing CrossHairToolTip marker and showing Circl…
Browse files Browse the repository at this point in the history
…e for the specified radius search (#250)

* SRS-698: CrossToolTipMarker for Radius Search

* Circle implementation for the radius set up by the user

* Making SiteMarkers disappear on clicking crosshaircursor

* Addressing few review comments
  • Loading branch information
nupurdixit13 authored Jan 10, 2025
1 parent a916184 commit 5f37d98
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 22 deletions.
46 changes: 46 additions & 0 deletions frontend/src/app/features/map/CircleLayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Circle, useMap, useMapEvents } from 'react-leaflet';
import { useState } from 'react';
import { LatLngTuple, LeafletMouseEvent, Map } from 'leaflet';
import { useMapCrosshairsCursor } from '../../hooks/useMapCrossHairCursor';
import { CrosshairsTooltipMarker } from './CrossHairToolTipMarker';

interface CircleLayerProps {
radius: number;
onCrossHairClick: () => void;
}

export function CircleLayer({
radius,
onCrossHairClick,
}: Readonly<CircleLayerProps>) {
const [center, setCenter] = useState<LatLngTuple | undefined>(undefined);
const map = useMap();
useMapCrosshairsCursor(map);

useMapEvents({
click: (ev: LeafletMouseEvent) => {
const newCenter: LatLngTuple = [ev.latlng.lat, ev.latlng.lng];
setCenter(newCenter);
onCrossHairClick();
},
});

const drawCircle = center && radius > 0;

return (
<>
<CrosshairsTooltipMarker center={center}>
Click to place center point
</CrosshairsTooltipMarker>
{drawCircle && (
<Circle
center={center}
radius={radius}
stroke
fill
className="point-search-circle"
/>
)}
</>
);
}
49 changes: 49 additions & 0 deletions frontend/src/app/features/map/CrossHairToolTipMarker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ReactNode, useRef } from 'react';
import {
LatLngExpression,
LeafletMouseEvent,
Marker as LeafletMarker,
} from 'leaflet';
import { Tooltip, useMap, useMapEvent } from 'react-leaflet';
import { IconMarker } from './IconMarker';
import { crosshairsIcon, emptyIcon } from './siteMarkers/icons';

interface Props {
children: ReactNode;
center?: LatLngExpression;
}

export function CrosshairsTooltipMarker({ children, center }: Readonly<Props>) {
const map = useMap();
const markerRef = useRef<LeafletMarker>(null);

useMapEvent('mousemove', (ev: LeafletMouseEvent) => {
if (markerRef.current && !center) {
markerRef.current.setLatLng(ev.latlng);
}
});

return (
<IconMarker
ref={markerRef}
position={center ?? map.getCenter()}
icon={center ? crosshairsIcon : emptyIcon}
interactive={false}
draggable={false}
cursor="crosshair"
>
{!center && (
<Tooltip
permanent
sticky
direction="right"
offset={[12, 0]}
className="search-layer-tooltip"
interactive={false}
>
{children}
</Tooltip>
)}
</IconMarker>
);
}
42 changes: 30 additions & 12 deletions frontend/src/app/features/map/MapSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
MAP_CONTROLS_RIGHT_LG,
MAP_CONTROLS_RIGHT_SM,
MAP_CONTROLS_RIGHT_XL,
MIN_CIRCLE_RADIUS,
} from '../../constants/Constant';

import { TextSearchButton } from './search/TextSearchButton';
Expand Down Expand Up @@ -62,11 +63,19 @@ const componentProps = {
};

interface MapSearchProps {
activeTool?: ActiveToolEnum | null;
setActiveTool?: React.Dispatch<React.SetStateAction<ActiveToolEnum | null>>;
radius: number;
setRadius: React.Dispatch<React.SetStateAction<number>>;
isLocationVisible: boolean;
setLocationVisible: React.Dispatch<React.SetStateAction<boolean>>;
}

export function MapSearch({
activeTool,
setActiveTool,
radius,
setRadius,
isLocationVisible,
setLocationVisible,
}: MapSearchProps) {
Expand All @@ -93,25 +102,29 @@ export function MapSearch({
setQuery({ search: searchValue }, 'replace');
};

const [activeTool, setActiveTool] = useState<ActiveToolEnum | null>(null);
const isPolygonTool = activeTool === ActiveToolEnum.polygonSearch;
const isRadiusTool = activeTool === ActiveToolEnum.radiusSearch;

const handlePolygonToolClick = () => {
setActiveTool((prevTool) =>
prevTool === ActiveToolEnum.polygonSearch
? null
: ActiveToolEnum.polygonSearch,
);
if (setActiveTool) {
setActiveTool((prevTool) =>
prevTool === ActiveToolEnum.polygonSearch
? null
: ActiveToolEnum.polygonSearch,
);
}
};

const handleRadiusToolClick = () => {
console.log('Radius tool clicked');
setActiveTool((prevTool) =>
prevTool === ActiveToolEnum.radiusSearch
? null
: ActiveToolEnum.radiusSearch,
);
if (setActiveTool) {
setActiveTool((prevTool) =>
prevTool === ActiveToolEnum.radiusSearch
? null
: ActiveToolEnum.radiusSearch,
);
setRadius(MIN_CIRCLE_RADIUS);
}
};

return (
Expand Down Expand Up @@ -155,6 +168,7 @@ export function MapSearch({
onClick={handlePolygonToolClick}
/>
<RadiusSearchButton
//mapRef={mapRef}
isActive={isRadiusTool}
onClick={handleRadiusToolClick}
/>
Expand All @@ -165,7 +179,11 @@ export function MapSearch({
{isPolygonTool ? (
<PolygonSearch />
) : (
<RadiusSearch setActiveTool={setActiveTool} />
<RadiusSearch
radius={radius}
setRadius={setRadius}
setActiveTool={setActiveTool}
/>
)}
</div>
</div>
Expand Down
28 changes: 24 additions & 4 deletions frontend/src/app/features/map/MapView.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { useContext, useRef, useState } from 'react';
import { useContext, useEffect, useRef, useState } from 'react';
import { LatLngBounds, LatLngTuple, Map } from 'leaflet';
import { MapContainer, TileLayer } from 'react-leaflet';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import clsx from 'clsx';

//import { env } from '@/env'
//import MapSearch from './MapSearch'
import { MyLocationMarker } from './MyLocationMarker'; // Import the MyLocationMarker component

import 'leaflet/dist/leaflet.css';
Expand All @@ -21,6 +19,8 @@ import {
MapSearchQueryProvider,
} from './mapSearchQueryParamsContext/MapSearchQueryParamsContext';
import { MapSearchDrawer } from './siteDrawer/MapSearchDrawer';
import { ActiveToolEnum, MIN_CIRCLE_RADIUS } from '../../constants/Constant';
import { RadiusSearchLayer } from './layers/RadiusSearchLayer';

// Set the position of the marker for center of BC
const CENTER_OF_BC: LatLngTuple = [53.7267, -127.6476];
Expand All @@ -44,6 +44,7 @@ function MapView() {
},
onCompleted: ({ mapSearch: { data } }) => {
flyToSiteBounds(data);
setSites(data);
},
});

Expand All @@ -64,6 +65,16 @@ function MapView() {

const mapRef = useRef<Map>(null);
const [isLocationVisible, setLocationVisible] = useState(false);
const [radius, setRadius] = useState(MIN_CIRCLE_RADIUS);
const [activeTool, setActiveTool] = useState<ActiveToolEnum | null>(null);
const [sites, setSites] = useState<Site[]>([]);
const clearSites = () => setSites([]);

useEffect(() => {
if (activeTool === null) {
setSites(data?.mapSearch.data || []);
}
}, [activeTool]);

return (
<div
Expand All @@ -87,11 +98,20 @@ function MapView() {
setLocationVisible={setLocationVisible}
/>
{<MyLocationMarker isLocationVisible={isLocationVisible} />}
<SiteMarkers sites={data?.mapSearch.data || []} />
<SiteMarkers sites={sites} />
<RadiusSearchLayer
activeTool={activeTool}
radius={radius}
onCrossHairClick={clearSites}
/>
</MapContainer>
<MapSearch
isLocationVisible={isLocationVisible}
setLocationVisible={setLocationVisible}
activeTool={activeTool}
setActiveTool={setActiveTool}
radius={radius}
setRadius={setRadius}
/>

<MapSearchDrawer
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/app/features/map/layers/RadiusSearchLayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ActiveToolEnum } from '../../../constants/Constant';
import { CircleLayer } from '../CircleLayer';

interface RadiusSearchLayerProps {
activeTool: ActiveToolEnum | null;
radius: number;
onCrossHairClick: () => void;
}
export function RadiusSearchLayer({
activeTool,
radius,
onCrossHairClick,
}: Readonly<RadiusSearchLayerProps>) {
if (activeTool === ActiveToolEnum.radiusSearch) {
return <CircleLayer radius={radius} onCrossHairClick={onCrossHairClick} />;
}
return null;
}
12 changes: 8 additions & 4 deletions frontend/src/app/features/map/search/RadiusSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,27 @@ import { formatDistance } from '../../../helpers/utility';
import { useState } from 'react';
import { Button } from '../../../components/button/Button';

interface Props {
interface RadiusSearchProps {
radius: number;
setRadius: React.Dispatch<React.SetStateAction<number>>;
isSmall?: boolean;
className?: string;
setActiveTool?: React.Dispatch<React.SetStateAction<ActiveToolEnum | null>>;
}

export function RadiusSearch({
radius = MIN_CIRCLE_RADIUS,
setRadius,
isSmall = false,
className,
setActiveTool,
}: Readonly<Props>) {
const [radius, setRadius] = useState(MIN_CIRCLE_RADIUS);

}: Readonly<RadiusSearchProps>) {
const [isVisible, setIsVisible] = useState(true);

const onCancel = () => {
setIsVisible(false);
setActiveTool && setActiveTool(null);
setRadius(MIN_CIRCLE_RADIUS);
};

const onRadiusChange = (_ev: any, value: number | number[]) => {
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/app/features/map/search/RadiusSearchButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ interface Props {
onClick: () => void;
}

export function RadiusSearchButton({ isActive, onClick }: Readonly<Props>) {
export function RadiusSearchButton({
isActive,
onClick,
}: Readonly<Props>) {
return (
<Button
variant="contained"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 19 additions & 1 deletion frontend/src/app/features/map/siteMarkers/icons.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Icon } from 'leaflet';
import L, { Icon, DivIcon } from 'leaflet';
import mapMarkerDefault from './assets/map_marker_default.png';
import mapMarkerHover from './assets/map_marker_hover.png';
import mapMarkerSelected from './assets/map_marker_selected.png';
import crosshairsSvg from './assets/crosshairsSvg.svg';

export const mapMarkerIconDefault = new Icon({
iconUrl: mapMarkerDefault,
Expand All @@ -20,3 +21,20 @@ export const mapMarkerIconSelected = new Icon({
iconSize: [55, 75],
iconAnchor: [27.5, 75],
});

// Used by Polygon and Point search tools
export const crosshairsIcon = new Icon({
iconUrl: crosshairsSvg,
iconSize: [32, 32],
iconAnchor: [16, 16],
// relative to iconAnchor
popupAnchor: [32, 0],
tooltipAnchor: [32, 0],
className: 'crosshairs-icon',
});

export const emptyIcon = new DivIcon({
html: '<span/>',
className: 'empty-icon',
iconSize: [0, 0],
});
19 changes: 19 additions & 0 deletions frontend/src/app/hooks/useMapCrossHairCursor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect } from 'react';
import L from 'leaflet';

export function useMapCrosshairsCursor(map: L.Map, enabled = true) {
// Show crosshairs cursor
useEffect(() => {
const container = map.getContainer();
if (container) {
if (enabled) {
L.DomUtil.addClass(container, 'crosshairs-cursor');
}
return () => {
L.DomUtil.removeClass(container, 'crosshairs-cursor');
};
}
}, [map, enabled]);

return null;
}
1 change: 1 addition & 0 deletions frontend/src/declaration.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
declare module '*.png';
declare module '*.svg';

0 comments on commit 5f37d98

Please sign in to comment.