diff --git a/src/frontend/src/Components/Map.js b/src/frontend/src/Components/Map.js index 8606916b2..7309062d8 100644 --- a/src/frontend/src/Components/Map.js +++ b/src/frontend/src/Components/Map.js @@ -19,6 +19,7 @@ import { updateRestStops, } from '../slices/feedsSlice'; import { updateMapState } from '../slices/mapSlice'; +import { updateAdvisories } from '../slices/cmsSlice'; // External Components import Button from 'react-bootstrap/Button'; @@ -33,7 +34,6 @@ import { // Components and functions import CamPopup from './map/camPopup.js'; -import { getCamerasLayer } from './map/layers/camerasLayer.js'; import { getEventPopup, getFerryPopup, @@ -41,9 +41,12 @@ import { getRegionalPopup, getRestStopPopup, } from './map/mapPopup.js'; +import { getAdvisories } from './data/advisories.js'; import { getEvents } from './data/events.js'; import { getWeather, getRegional } from './data/weather.js'; import { getRestStops } from './data/restStops.js'; +import { getAdvisoriesLayer } from './map/layers/advisoriesLayer.js'; +import { getCamerasLayer } from './map/layers/camerasLayer.js'; import { getRestStopsLayer } from './map/layers/restStopsLayer.js'; import { loadEventsLayers } from './map/layers/eventsLayer.js'; import { loadWeatherLayers } from './map/layers/weatherLayer.js'; @@ -68,10 +71,11 @@ import CurrentCameraIcon from './CurrentCameraIcon'; import Filters from './Filters.js'; import RouteSearch from './map/RouteSearch.js'; -// OpenLayers +// OpenLayers & turf import { applyStyle } from 'ol-mapbox-style'; import { fromLonLat, toLonLat, transformExtent } from 'ol/proj'; import { ScaleLine } from 'ol/control.js'; +import { getBottomLeft, getTopRight } from 'ol/extent'; import Map from 'ol/Map'; import Overlay from 'ol/Overlay.js'; import Geolocation from 'ol/Geolocation.js'; @@ -79,6 +83,7 @@ import MVT from 'ol/format/MVT.js'; import VectorTileLayer from 'ol/layer/VectorTile.js'; import VectorTileSource from 'ol/source/VectorTile.js'; import View from 'ol/View'; +import { booleanIntersects, polygon } from '@turf/turf'; // Styling import { @@ -103,8 +108,9 @@ export default function MapWrapper({ camTimeStamp, // Cameras events, eventTimeStamp, // Events + advisories, // CMS ferries, - ferriesTimeStamp, // CMS + ferriesTimeStamp, // Ferries weather, weatherTimeStamp, // Current Weather regional, @@ -125,6 +131,8 @@ export default function MapWrapper({ events: state.feeds.events.list, eventTimeStamp: state.feeds.events.routeTimeStamp, // CMS + advisories: state.cms.advisories.list, + // Ferries ferries: state.feeds.ferries.list, ferriesTimeStamp: state.feeds.ferries.routeTimeStamp, // Current Weather @@ -207,6 +215,8 @@ export default function MapWrapper({ setClickedRestStop(feature); } + const [advisoriesInView, setAdvisoriesInView] = useState([]); + // Define the function to be executed after the delay function resetCameraPopupRef() { cameraPopupRef.current = null; @@ -631,6 +641,7 @@ export default function MapWrapper({ } }, [searchLocationFrom]); + // Route layer useEffect(() => { if (isInitialMountRoute.current) { // Do nothing on first load @@ -646,6 +657,7 @@ export default function MapWrapper({ loadData(false); }, [selectedRoute]); + // Camera layer useEffect(() => { // Remove layer if it already exists if (mapLayers.current['highwayCams']) { @@ -687,6 +699,7 @@ export default function MapWrapper({ } }; + // Event layers useEffect(() => { loadEventsLayers(events, mapContext, mapLayers, mapRef); }, [events]); @@ -706,6 +719,7 @@ export default function MapWrapper({ } }; + // Ferries and rest stops layers useEffect(() => { // Remove layer if it already exists if (mapLayers.current['inlandFerries']) { @@ -767,6 +781,7 @@ export default function MapWrapper({ } }; + // Weather layers useEffect(() => { if (mapLayers.current['weather']) { mapRef.current.removeLayer(mapLayers.current['weather']); @@ -797,6 +812,75 @@ export default function MapWrapper({ } }; + // Advisories helper functions + function wrapLon(value) { + const worlds = Math.floor((value + 180) / 360); + return value - worlds * 360; + } + + function onMoveEnd(evt) { + // calculate polygon based on map extent + const map = evt.map; + const extent = map.getView().calculateExtent(map.getSize()); + const bottomLeft = toLonLat(getBottomLeft(extent)); + const topRight = toLonLat(getTopRight(extent)); + + const mapPoly = polygon([[ + [wrapLon(bottomLeft[0]), topRight[1]], // Top left + [wrapLon(bottomLeft[0]), bottomLeft[1]], // Bottom left + [wrapLon(topRight[0]), bottomLeft[1]], // Bottom right + [wrapLon(topRight[0]), topRight[1]], // Top right + [wrapLon(bottomLeft[0]), topRight[1]], // Top left + ]]); + + // Update state with advisories that intersect with map extent + const resAdvisories = []; + if (advisories && advisories.length > 0) { + advisories.forEach(advisory => { + const advPoly = polygon(advisory.geometry.coordinates); + if (booleanIntersects(mapPoly, advPoly)) { + resAdvisories.push(advisory); + } + }); + } + setAdvisoriesInView(resAdvisories); + } + + // Advisories layer + const loadAdvisories = async () => { + // Fetch data if it doesn't already exist + if (!advisories) { + dispatch(updateAdvisories({ + list: await getAdvisories(), + timeStamp: new Date().getTime() + })); + } + }; + + useEffect(() => { + // Remove layer if it already exists + if (mapLayers.current['advisoriesLayer']) { + mapRef.current.removeLayer(mapLayers.current['advisoriesLayer']); + } + + // Add layer if array exists + if (advisories) { + // Generate and add layer + mapLayers.current['advisoriesLayer'] = getAdvisoriesLayer( + advisories, + mapRef.current.getView().getProjection().getCode(), + mapContext, + ); + + mapRef.current.addLayer(mapLayers.current['advisoriesLayer']); + mapLayers.current['advisoriesLayer'].setZIndex(55); + + if (mapRef.current) { + mapRef.current.on('moveend', onMoveEnd); + } + } + }, [advisories]); + useEffect(() => { if (mapLayers.current['regional']) { mapRef.current.removeLayer(mapLayers.current['regional']); @@ -844,6 +928,7 @@ export default function MapWrapper({ loadWeather(); loadRegional(); loadRestStops(); + loadAdvisories(); // Zoom/pan to route on route updates if (!isInitialMount) { @@ -857,6 +942,7 @@ export default function MapWrapper({ loadWeather(); loadRegional(); loadRestStops(); + loadAdvisories(); } }; @@ -1105,7 +1191,7 @@ export default function MapWrapper({ {!isPreview && (
- +
)} diff --git a/src/frontend/src/Components/advisories/AdvisoriesOnMap.js b/src/frontend/src/Components/advisories/AdvisoriesOnMap.js index 1b850f932..f219ed807 100644 --- a/src/frontend/src/Components/advisories/AdvisoriesOnMap.js +++ b/src/frontend/src/Components/advisories/AdvisoriesOnMap.js @@ -1,13 +1,7 @@ // 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 React, { useState } from 'react'; // Components and functions -import { getAdvisories } from '../data/advisories.js'; import AdvisoriesList from './AdvisoriesList'; // Third party packages @@ -21,33 +15,9 @@ import Button from 'react-bootstrap/Button'; // Styling import './AdvisoriesOnMap.scss'; -export default function AdvisoriesOnMap() { - // 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() - })); - } - } +export default function AdvisoriesOnMap(props) { + const { advisories } = props; - useEffect(() => { - if (isInitialMount.current) { // Only run on initial load - loadAdvisories(); - isInitialMount.current = false; - } - }); - // States const [open, setOpen] = useState(false); diff --git a/src/frontend/src/Components/advisories/AdvisoriesOnMap.scss b/src/frontend/src/Components/advisories/AdvisoriesOnMap.scss index 2f466dfd7..b6c7ac810 100644 --- a/src/frontend/src/Components/advisories/AdvisoriesOnMap.scss +++ b/src/frontend/src/Components/advisories/AdvisoriesOnMap.scss @@ -2,7 +2,7 @@ .advisories-on-map { position: relative; - top: 1rem; + top: 6rem; left: 1rem; width: fit-content; diff --git a/src/frontend/src/Components/data/featureStyleDefinitions.js b/src/frontend/src/Components/data/featureStyleDefinitions.js index 7606e51a6..780f058d0 100644 --- a/src/frontend/src/Components/data/featureStyleDefinitions.js +++ b/src/frontend/src/Components/data/featureStyleDefinitions.js @@ -1,4 +1,4 @@ -import { Icon, Stroke, Style } from 'ol/style.js'; +import { Fill, Icon, Stroke, Style } from 'ol/style.js'; // Static assets // Cameras @@ -61,6 +61,19 @@ import genericDelaysActiveIcon from '../../images/mapIcons/incident-minor-active import genericDelaysHoverIcon from '../../images/mapIcons/incident-minor-hover.png'; import genericDelaysStaticIcon from '../../images/mapIcons/incident-minor-static.png'; +// Map advisory styles +export const advisoryStyles = { + polygon: new Style({ + stroke: new Stroke({ + color: 'rgb(255, 90, 0)', + width: 2, + }), + fill: new Fill({ + color: 'rgba(255, 217, 105, 0.4)', + }), + }) +}; + // Camera icon styles export const cameraStyles = { static: new Style({ diff --git a/src/frontend/src/Components/map/layers/advisoriesLayer.js b/src/frontend/src/Components/map/layers/advisoriesLayer.js new file mode 100644 index 000000000..2b93b5d49 --- /dev/null +++ b/src/frontend/src/Components/map/layers/advisoriesLayer.js @@ -0,0 +1,50 @@ +// Components and functions +import { transformFeature } from '../helper.js'; + +// OpenLayers +import { Polygon } from 'ol/geom'; +import * as ol from 'ol'; +import GeoJSON from 'ol/format/GeoJSON.js'; +import VectorLayer from 'ol/layer/Vector'; +import VectorSource from 'ol/source/Vector'; + +// Styling +import { advisoryStyles } from '../../data/featureStyleDefinitions.js'; + +export function getAdvisoriesLayer( + advisories, + projectionCode, + mapContext, +) { + return new VectorLayer({ + classname: 'advisories', + visible: true, + source: new VectorSource({ + format: new GeoJSON(), + loader: function (extent, resolution, projection) { + const vectorSource = this; + vectorSource.clear(); + + advisories.forEach(advisory => { + // Build a new OpenLayers feature + const olGeometry = new Polygon(advisory.geometry.coordinates); + const olFeature = new ol.Feature({ geometry: olGeometry }); + + // Transform the projection + const olFeatureForMap = transformFeature( + olFeature, + 'EPSG:4326', + projectionCode, + ); + + // feature ID to advisory ID for retrieval + olFeatureForMap.setId(advisory.id); + + vectorSource.addFeature(olFeatureForMap); + }); + }, + }), + + style: () => advisoryStyles.polygon, + }); +} diff --git a/src/frontend/src/pages/AdvisoryDetailsPage.js b/src/frontend/src/pages/AdvisoryDetailsPage.js index 50e752945..52ab6816f 100644 --- a/src/frontend/src/pages/AdvisoryDetailsPage.js +++ b/src/frontend/src/pages/AdvisoryDetailsPage.js @@ -54,12 +54,11 @@ function getMap(advisoryData) { }), 'Polygon': new Style({ stroke: new Stroke({ - color: 'blue', - lineDash: [4], - width: 3, + color: 'rgb(255, 90, 0)', + width: 2, }), fill: new Fill({ - color: 'rgba(0, 0, 255, 0.1)', + color: 'rgba(255, 217, 105, 0.4)', }), }) };