app/javascript/react/components/Map/Markers/CrowdMapMarkers.tsx
import { useMap } from "@vis.gl/react-google-maps";
import React, { useEffect, useMemo, useRef, useState } from "react";
import {
clearCrowdMap,
fetchCrowdMapData,
selectCrowdMapRectangles,
selectFetchingCrowdMapData,
} from "../../../store/crowdMapSlice";
import { useAppDispatch, useAppSelector } from "../../../store/hooks";
import { selectMobileSessionsLoading } from "../../../store/loadingSelectors";
import { setMarkersLoading } from "../../../store/markersLoadingSlice";
import { selectMobileSessionsStreamIds } from "../../../store/mobileSessionsSelectors";
import {
clearRectangles,
fetchRectangleData,
selectRectangleData,
selectRectangleLoading,
} from "../../../store/rectangleSlice";
import { selectThresholds } from "../../../store/thresholdSlice";
import { Session } from "../../../types/sessionType";
import useMapEventListeners from "../../../utils/mapEventListeners";
import { useMapParams } from "../../../utils/mapParamsHandler";
import { getColorForValue } from "../../../utils/thresholdColors";
import { CustomMarker } from "./CustomMarker";
import MapOverlay from "./MapOverlay";
import {
RectangleInfo,
RectangleInfoLoading,
} from "./RectangleInfo/RectangleInfo";
type Props = {
pulsatingSessionId: number | null;
sessions: Session[];
};
const CrowdMapMarkers = ({ pulsatingSessionId, sessions }: Props) => {
const dispatch = useAppDispatch();
const crowdMapRectangles = useAppSelector(selectCrowdMapRectangles);
const fetchingCrowdMapData = useAppSelector(selectFetchingCrowdMapData);
const mobileSessionsLoading = useAppSelector(selectMobileSessionsLoading);
const mobileSessionsStreamIds = useAppSelector(selectMobileSessionsStreamIds);
const rectangleData = useAppSelector(selectRectangleData);
const rectangleLoading = useAppSelector(selectRectangleLoading);
const thresholds = useAppSelector(selectThresholds);
const map = useMap();
const {
boundEast,
boundNorth,
boundSouth,
boundWest,
gridSize,
measurementType,
sensorName,
tags,
timeFrom,
timeTo,
unitSymbol,
usernames,
} = useMapParams();
const preparedUnitSymbol = unitSymbol.replace(/"/g, "");
const encodedUnitSymbol = encodeURIComponent(preparedUnitSymbol);
const gridSizeX = (x: number) => {
const width =
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth;
const height =
window.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight;
return (Math.round(x) * width) / height;
};
const filters = useMemo(
() =>
JSON.stringify({
east: boundEast,
grid_size_x: gridSizeX(gridSize),
grid_size_y: gridSize,
measurement_type: measurementType,
north: boundNorth,
sensor_name: sensorName,
south: boundSouth,
stream_ids: mobileSessionsStreamIds,
tags: tags,
time_from: timeFrom,
time_to: timeTo,
unit_symbol: encodedUnitSymbol,
usernames: usernames,
west: boundWest,
}),
[
boundEast,
boundNorth,
boundSouth,
boundWest,
gridSize,
measurementType,
mobileSessionsStreamIds,
sensorName,
tags,
timeFrom,
timeTo,
unitSymbol,
usernames,
]
);
const rectanglesRef = useRef<google.maps.Rectangle[]>([]);
const [rectanglePoint, setRectanglePoint] = useState<{
lat: number;
lng: number;
} | null>(null);
const crowdMapRectanglesLength: number = crowdMapRectangles.length;
const displayedSession: Session | undefined = sessions.find(
(session) => session.id === pulsatingSessionId
);
const displayedSessionMarkerRef = useRef<CustomMarker | null>(null);
const cleanupMarker = () => {
if (displayedSessionMarkerRef.current) {
displayedSessionMarkerRef.current.setMap(null);
displayedSessionMarkerRef.current = null;
}
};
useEffect(() => {
dispatch(setMarkersLoading(true));
}, [crowdMapRectanglesLength, tags, usernames, dispatch]);
useEffect(() => {
if (!mobileSessionsLoading || fetchingCrowdMapData) {
setRectanglePoint(null); // Clear rectanglePoint when fetching new data
dispatch(clearCrowdMap());
dispatch(fetchCrowdMapData(filters));
}
}, [dispatch, fetchingCrowdMapData, filters, mobileSessionsLoading]);
useEffect(() => {
if (!mobileSessionsLoading && crowdMapRectanglesLength > 0) {
const newRectangles = crowdMapRectangles.map((rectangle) => {
const newRectangle = new google.maps.Rectangle({
bounds: new google.maps.LatLngBounds(
new google.maps.LatLng(rectangle.south, rectangle.west),
new google.maps.LatLng(rectangle.north, rectangle.east)
),
clickable: true,
fillColor: getColorForValue(thresholds, rectangle.value),
fillOpacity: 0.6,
map: map,
strokeWeight: 0,
});
google.maps.event.addListener(newRectangle, "click", () => {
const rectangleBounds = newRectangle.getBounds();
if (rectangleBounds) {
const rectangleBoundEast = rectangleBounds.getNorthEast().lng();
const rectangleBoundWest = rectangleBounds.getSouthWest().lng();
const rectangleBoundNorth = rectangleBounds.getNorthEast().lat();
const rectangleBoundSouth = rectangleBounds.getSouthWest().lat();
const rectangleFilters = {
west: rectangleBoundWest.toString(),
east: rectangleBoundEast.toString(),
south: rectangleBoundSouth.toString(),
north: rectangleBoundNorth.toString(),
time_from: timeFrom,
time_to: timeTo,
grid_size_x: gridSizeX(gridSize).toString(),
grid_size_y: gridSize.toString(),
tags: tags || "",
usernames: usernames || "",
sensor_name: sensorName,
measurement_type: measurementType,
unit_symbol: encodeURIComponent(unitSymbol),
stream_ids: mobileSessionsStreamIds.join(","),
};
const queryString = new URLSearchParams(
rectangleFilters
).toString();
dispatch(fetchRectangleData(queryString));
setRectanglePoint({
lat: rectangleBoundNorth,
lng: rectangleBoundEast,
});
}
});
return newRectangle;
});
rectanglesRef.current.push(...newRectangles);
}
// Cleanup function to remove rectangles on unmount
return () => {
rectanglesRef.current.forEach((rectangle) => rectangle.setMap(null));
rectanglesRef.current = [];
};
}, [
crowdMapRectangles,
dispatch,
gridSize,
map,
measurementType,
mobileSessionsLoading,
mobileSessionsStreamIds,
sensorName,
tags,
thresholds,
timeFrom,
timeTo,
unitSymbol,
usernames,
]);
useEffect(() => {
map &&
map.addListener("zoom_changed", () => {
dispatch(clearRectangles());
setRectanglePoint(null); // Clear rectanglePoint when zoom changes
});
}, [dispatch, map]);
useMapEventListeners(map, {
click: () => {
dispatch(clearRectangles());
setRectanglePoint(null); // Clear rectanglePoint when map is clicked
},
touchend: () => {
dispatch(clearRectangles());
setRectanglePoint(null);
},
dragstart: () => {
dispatch(clearRectangles());
setRectanglePoint(null);
},
});
useEffect(() => {
if (rectanglesRef.current.length >= crowdMapRectanglesLength) {
dispatch(setMarkersLoading(false));
}
}, [crowdMapRectanglesLength, dispatch, rectanglesRef.current.length]);
// Manage displayed session marker
useEffect(() => {
if (displayedSession && map) {
cleanupMarker();
const position = displayedSession.point;
const color = getColorForValue(
thresholds,
displayedSession.lastMeasurementValue
);
const marker = new CustomMarker(position, color, "", 12);
marker.setMap(map);
displayedSessionMarkerRef.current = marker;
return cleanupMarker;
} else {
cleanupMarker();
}
}, [displayedSession, map, thresholds]);
return (
<>
{rectanglePoint && (rectangleLoading || rectangleData) && (
<MapOverlay position={rectanglePoint}>
{rectangleData && !rectangleLoading ? (
<RectangleInfo
color={getColorForValue(thresholds, rectangleData.average)}
rectangleData={rectangleData}
/>
) : (
<RectangleInfoLoading />
)}
</MapOverlay>
)}
</>
);
};
export { CrowdMapMarkers };