diff --git a/src/frontend/src/Components/advisories/Advisories.js b/src/frontend/src/Components/advisories/Advisories.js index e7623d2f..fb9c9653 100644 --- a/src/frontend/src/Components/advisories/Advisories.js +++ b/src/frontend/src/Components/advisories/Advisories.js @@ -1,9 +1,7 @@ // React -import React, { useCallback, useEffect, useRef } from 'react'; +import React from 'react'; // Redux -import { useSelector, useDispatch } from 'react-redux' -import { memoize } from 'proxy-memoize' import { updateAdvisories } from '../../slices/cmsSlice'; // External Components @@ -13,38 +11,13 @@ import { } from "@fortawesome/free-solid-svg-icons"; // Components and functions -import { getAdvisories } from '../data/advisories.js'; import AdvisoriesList from './AdvisoriesList'; // Styling import './Advisories.scss'; -export default function Advisories() { - // Redux - const dispatch = useDispatch(); - const { advisories } = useSelector(useCallback(memoize(state => ({ - advisories: state.cms.advisories.list, - })))); - - // Refs - const isInitialMount = useRef(true); - - // Data loading - const loadAdvisories = async () => { - if (!advisories) { - dispatch(updateAdvisories({ - list: await getAdvisories(), - timeStamp: new Date().getTime() - })); - } - } - - useEffect(() => { - if (isInitialMount.current) { // Only run on initial load - loadAdvisories(); - isInitialMount.current = false; - } - }); +export default function Advisories(props) { + const { advisories } = props; return (advisories && !!advisories.length) ? (
diff --git a/src/frontend/src/pages/CamerasListPage.js b/src/frontend/src/pages/CamerasListPage.js index 1ccfa50b..340d17a4 100644 --- a/src/frontend/src/pages/CamerasListPage.js +++ b/src/frontend/src/pages/CamerasListPage.js @@ -1,16 +1,21 @@ // React import React, { useCallback, useEffect, useRef, useState } from 'react'; + +// Redux import { useSelector, useDispatch } from 'react-redux' import { memoize } from 'proxy-memoize' +import { updateAdvisories } from '../slices/cmsSlice'; +import { updateCameras } from '../slices/feedsSlice'; // Third party components import { AsyncTypeahead } from 'react-bootstrap-typeahead'; +import { booleanIntersects, point, polygon } from '@turf/turf'; import Button from 'react-bootstrap/Button'; import Container from 'react-bootstrap/Container'; // Components and functions +import { getAdvisories } from '../Components/data/advisories'; import { collator, getCameras, addCameraGroups } from '../Components/data/webcams'; -import { updateCameras } from '../slices/feedsSlice'; import Advisories from '../Components/advisories/Advisories'; import CameraList from '../Components/cameras/CameraList'; import Footer from '../Footer.js'; @@ -25,7 +30,8 @@ export default function CamerasListPage() { // Redux const dispatch = useDispatch(); - const { cameras, camTimeStamp, selectedRoute } = useSelector(useCallback(memoize(state => ({ + const { advisories, cameras, camTimeStamp, selectedRoute } = useSelector(useCallback(memoize(state => ({ + advisories: state.cms.advisories.list, cameras: state.feeds.cameras.list, camTimeStamp: state.feeds.cameras.routeTimeStamp, selectedRoute: state.routes.selectedRoute @@ -35,11 +41,12 @@ export default function CamerasListPage() { const isInitialMount = useRef(true); // UseState hooks + const [advisoriesInRoute, setAdvisoriesInRoute] = useState([]); const [displayedCameras, setDisplayedCameras] = useState(null); const [processedCameras, setProcessedCameras] = useState(null); const [searchText, setSearchText] = useState(''); - // UseEffect hooks and data functions + // Data functions const getCamerasData = async () => { const newRouteTimestamp = selectedRoute ? selectedRoute.searchTimestamp : null; @@ -69,8 +76,44 @@ export default function CamerasListPage() { }); setProcessedCameras(finalCameras); + getAdvisoriesData(finalCameras); + }; + + const getAdvisoriesData = async (camsData) => { + let advData = advisories; + + if (!advisories) { + advData = await getAdvisories(); + + dispatch(updateAdvisories({ + list: advData, + timeStamp: new Date().getTime() + })); + } + + // load all advisories if no route selected + const resAdvisories = !selectedRoute ? advData : []; + + // Route selected, load advisories that intersect with at least one cam on route + if (selectedRoute && advData && advData.length > 0 && camsData && camsData.length > 0) { + for (const adv of advData) { + const advPoly = polygon(adv.geometry.coordinates); + + for (const cam of camsData) { + const camPoint = point(cam.location.coordinates); + if (booleanIntersects(advPoly, camPoint)) { + // advisory intersects with a camera, add to list and break loop + resAdvisories.push(adv); + break; + } + } + } + } + + setAdvisoriesInRoute(resAdvisories); }; + // useEffect hooks useEffect(() => { getCamerasData(); @@ -114,6 +157,7 @@ export default function CamerasListPage() { } }, [displayedCameras]); + // Rendering return (
- +
diff --git a/src/frontend/src/pages/EventsListPage.js b/src/frontend/src/pages/EventsListPage.js index 033545bf..a8c516b2 100644 --- a/src/frontend/src/pages/EventsListPage.js +++ b/src/frontend/src/pages/EventsListPage.js @@ -1,11 +1,15 @@ // React import React, { useCallback, useContext, useEffect, useState, useRef } from 'react'; -import { memoize } from 'proxy-memoize'; -import { updateEvents } from '../slices/feedsSlice'; import { useNavigate } from 'react-router-dom'; + +// Redux import { useSelector, useDispatch } from 'react-redux'; +import { memoize } from 'proxy-memoize'; +import { updateAdvisories } from '../slices/cmsSlice'; +import { updateEvents } from '../slices/feedsSlice'; // External imports +import { booleanIntersects, point, lineString, polygon } from '@turf/turf'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faAngleDown, @@ -19,10 +23,10 @@ import DropdownButton from 'react-bootstrap/DropdownButton'; import InfiniteScroll from 'react-infinite-scroll-component'; // Internal imports +import { getAdvisories } from '../Components/data/advisories'; import { getEvents } from '../Components/data/events'; import { MapContext } from '../App.js'; import { defaultSortFn, routeSortFn, severitySortFn } from '../Components/events/functions'; - import Advisories from '../Components/advisories/Advisories'; import EventCard from '../Components/events/EventCard'; import EventsTable from '../Components/events/EventsTable'; @@ -67,25 +71,13 @@ export default function EventsListPage() { // Redux const dispatch = useDispatch(); - const { events, eventTimeStamp, selectedRoute } = useSelector(useCallback(memoize(state => ({ + const { advisories, events, eventTimeStamp, selectedRoute } = useSelector(useCallback(memoize(state => ({ + advisories: state.cms.advisories.list, events: state.feeds.events.list, eventTimeStamp: state.feeds.events.routeTimeStamp, selectedRoute: state.routes.selectedRoute })))); - const loadEvents = async (route) => { - const newRouteTimestamp = route ? route.searchTimestamp : null; - - // Fetch data if it doesn't already exist or route was updated - if (!events || (eventTimeStamp != newRouteTimestamp)) { - dispatch(updateEvents({ - list: await getEvents(route ? route.points : null), - routeTimeStamp: newRouteTimestamp, - timeStamp: new Date().getTime() - })); - } - } - // Context const { mapContext, setMapContext } = useContext(MapContext); @@ -98,14 +90,62 @@ export default function EventsListPage() { 'futureEvents': mapContext.visible_layers.futureEvents, 'roadConditions': false, }); - const [processedEvents, setProcessedEvents] = useState([]); // Nulls for mapping loader const [showLoader, setShowLoader] = useState(false); + const [advisoriesInRoute, setAdvisoriesInRoute] = useState([]); // Refs const isInitialMount = useRef(true); - // Data loading + // Data functions + const getAdvisoriesData = async (eventsData) => { + let advData = advisories; + + if (!advisories) { + advData = await getAdvisories(); + + dispatch(updateAdvisories({ + list: advData, + timeStamp: new Date().getTime() + })); + } + + // load all advisories if no route selected + const resAdvisories = !selectedRoute ? advData : []; + + // Route selected, load advisories that intersect with at least one event on route + if (selectedRoute && advData && advData.length > 0 && eventsData && eventsData.length > 0) { + for (const adv of advData) { + const advPoly = polygon(adv.geometry.coordinates); + + for (const event of eventsData) { + // Event geometry, point or line based on type + const eventGeom = event.location.type == 'Point' ? point(event.location.coordinates) : lineString(event.location.coordinates); + if (booleanIntersects(advPoly, eventGeom)) { + // advisory intersects with an event, add to list and break loop + resAdvisories.push(adv); + break; + } + } + } + } + + setAdvisoriesInRoute(resAdvisories); + }; + + const loadEvents = async (route) => { + const newRouteTimestamp = route ? route.searchTimestamp : null; + + // Fetch data if it doesn't already exist or route was updated + if (!events || (eventTimeStamp != newRouteTimestamp)) { + dispatch(updateEvents({ + list: await getEvents(route ? route.points : null), + routeTimeStamp: newRouteTimestamp, + timeStamp: new Date().getTime() + })); + } + } + const processEvents = () => { const hasTrue = (val) => !!val; const hasFilterOn = Object.values(eventCategoryFilter).some(hasTrue); @@ -123,6 +163,7 @@ export default function EventsListPage() { // Sort sortEvents(res, sortingKey); setProcessedEvents(res); + getAdvisoriesData(res); }; // useEffect hooks @@ -221,7 +262,7 @@ export default function EventsListPage() { - +
{ largeScreen &&