diff --git a/src/frontend/.eslintrc.js b/src/frontend/.eslintrc.js index 1c39b1646..0f8aa9d63 100644 --- a/src/frontend/.eslintrc.js +++ b/src/frontend/.eslintrc.js @@ -43,6 +43,5 @@ module.exports = { }], "react/prop-types": "off", 'camelcase': 'off', - 'no-unused-vars': 'off', }, }; diff --git a/src/frontend/src/App.js b/src/frontend/src/App.js index c69b58fb3..0215abf68 100644 --- a/src/frontend/src/App.js +++ b/src/frontend/src/App.js @@ -17,7 +17,7 @@ import AdvisoryDetailsPage from './pages/AdvisoryDetailsPage'; import BulletinsListPage from './pages/BulletinsListPage'; import BulletinDetailsPage from './pages/BulletinDetailsPage'; import FeedbackPage from './pages/FeedbackPage'; -import ScrollToTop from './Components/ScrollToTop'; +import ScrollToTop from './Components/shared/ScrollToTop'; // https://github.com/dai-shi/proxy-memoize?tab=readme-ov-file#usage-with-immer import { setAutoFreeze } from 'immer'; diff --git a/src/frontend/src/Components/Filters.js b/src/frontend/src/Components/Filters.js deleted file mode 100644 index 70fd61f20..000000000 --- a/src/frontend/src/Components/Filters.js +++ /dev/null @@ -1,405 +0,0 @@ -// React -import React, { useState, useContext } from 'react'; - -// Third party packages -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { - faFilter, - faXmark, - faMinusCircle, - faCalendarDays, - faVideo, - faSnowflake, - faFerry, - faSunCloud, - faRestroom, -} from '@fortawesome/pro-solid-svg-icons'; -import Button from 'react-bootstrap/Button'; -import Tooltip from 'react-bootstrap/Tooltip'; -import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; -import {useMediaQuery} from '@uidotdev/usehooks'; -import trackEvent from './TrackEvent.js'; - - -// Components and functions -import { MapContext } from '../App.js'; - -// Styling -import './Filters.scss'; - -export default function Filters(props) { - // Misc - const largeScreen = useMediaQuery('only screen and (min-width : 768px)'); - - // Context - const { mapContext } = useContext(MapContext); - - // Props - const { toggleHandler, disableFeatures, enableRoadConditions, textOverride } = props; - - // States - // Show layer menu by default on main page, desktop only - const [open, setOpen] = useState(largeScreen && !textOverride); - - const tooltipClosures = ( - -

Travel is not possible in one or both directions on this road. Find an alternate route or a detour where possible.

-
- ); - - const tooltipMajor = ( - -

Expect delays of at least 30 minutes or more on this road. This could be due to a traffic incident, road work, or construction.

-
- ); - - const tooltipMinor = ( - -

Expect delays up to 30 minutes on this road. This could be due to a traffic incident, road work, or construction.

-
- ); - - const tooltipFutureevents = ( - -

Future road work or construction is planned for this road.

-
- ); - - const tooltipHighwaycameras = ( - -

Look at recent pictures from cameras near the highway.

-
- ); - - const tooltipRoadconditions = ( - -

States of the road that may impact drivability.

-
- ); - - const tooltipInlandferries = ( - -

Travel requires the use of an inland ferry.

-
- ); - const tooltipWeather = ( - -

Weather updates for roads.

-
- ); - const tooltipRestStops = ( - -

Travel requires the use of a rest stop.

-
- ); - - // States for toggles - const [closures, setClosures] = useState(mapContext.visible_layers.closures); - const [majorEvents, setMajorEvents] = useState(mapContext.visible_layers.majorEvents); - const [minorEvents, setMinorEvents] = useState(mapContext.visible_layers.minorEvents); - const [futureEvents, setFutureEvents] = useState(mapContext.visible_layers.futureEvents); - const [roadConditions, setRoadConditions] = useState(mapContext.visible_layers.roadConditions); - const [highwayCams, setHighwayCams] = useState(mapContext.visible_layers.highwayCams); - const [inlandFerries, setInlandFerries] = useState(mapContext.visible_layers.inlandFerries); - const [weather, setWeather] = useState(mapContext.visible_layers.weather); - const [restStops, setRestStops] = useState(mapContext.visible_layers.restStops); - - return ( -
- - - { open && -
-

Filters

- - -
-
-

Delays

-
-
-
- { - trackEvent('click', 'map', 'Toggle closures layer') - toggleHandler('closures', e.target.checked); - toggleHandler('closuresLines', e.target.checked, true); - setClosures(!closures) - }} - defaultChecked={mapContext.visible_layers.closures} - /> - - - - ? - -
- -
- { - trackEvent('click', 'map', 'Toggle major events layer') - toggleHandler('majorEvents', e.target.checked); - toggleHandler('majorEventsLines', e.target.checked, true); - setMajorEvents(!majorEvents) - }} - defaultChecked={mapContext.visible_layers.majorEvents} - /> - - - ? - -
- -
- { - trackEvent('click', 'map', 'Toggle minor events layer') - toggleHandler('minorEvents', e.target.checked); - toggleHandler('minorEventsLines', e.target.checked, true); - setMinorEvents(!minorEvents); - }} - defaultChecked={mapContext.visible_layers.minorEvents} - /> - - - ? - -
- -
- { - trackEvent('click', 'map', 'Toggle future events layer') - toggleHandler('futureEvents', e.target.checked); - toggleHandler('futureEventsLines', e.target.checked, true); - setFutureEvents(!futureEvents); - }} - defaultChecked={mapContext.visible_layers.futureEvents} - /> - - - ? - -
-
-
-
-
-

Conditions and features

-
-
-
- { - trackEvent('click', 'map', 'Toggle highway cameras layer') - toggleHandler('highwayCams', e.target.checked); setHighwayCams(!highwayCams)}} - defaultChecked={mapContext.visible_layers.highwayCams} - disabled={disableFeatures} - /> - - - ? - -
- -
- { - trackEvent('click', 'map', 'Toggle road conditions layer') - toggleHandler('roadConditions', e.target.checked); - toggleHandler('roadConditionsLines', e.target.checked); - setRoadConditions(!roadConditions); - }} - defaultChecked={mapContext.visible_layers.roadConditions} - disabled={(disableFeatures && !enableRoadConditions)} - /> - - - ? - -
- -
- { - trackEvent('click', 'map', 'Toggle inland ferries layer') - toggleHandler('inlandFerries', e.target.checked); setInlandFerries(!inlandFerries)}} - defaultChecked={mapContext.visible_layers.inlandFerries} - disabled={disableFeatures} - /> - - - ? - -
- -
- { - trackEvent('click', 'map', 'Toggle weather layer') - toggleHandler('weather', e.target.checked); - toggleHandler('regional', e.target.checked); - setWeather(!weather)} - } - defaultChecked={mapContext.visible_layers.weather} - disabled={disableFeatures} - /> - - - ? - -
-
- { - trackEvent('click', 'map', 'Toggle rest stops layer') - toggleHandler('restStops', e.target.checked); setRestStops(!restStops)}} - defaultChecked={mapContext.visible_layers.restStops} - disabled={disableFeatures} - /> - - - ? - -
- - {/* -
- - - - - - - ? - -
- */} - - {/* -
- - - - - - - ? - -
- */} -
-
-
- -
-
- } -
- ); -} diff --git a/src/frontend/src/Components/Map.js b/src/frontend/src/Components/Map.js deleted file mode 100644 index d9ce7a48c..000000000 --- a/src/frontend/src/Components/Map.js +++ /dev/null @@ -1,1462 +0,0 @@ -// React -import React, { - useContext, - useRef, - useEffect, - useState, - useCallback, -} from 'react'; - -// Redux -import { memoize } from 'proxy-memoize'; -import { useSelector, useDispatch } from 'react-redux'; -import { - updateCameras, - updateEvents, - updateFerries, - updateWeather, - updateRegional, - updateRestStops, -} from '../slices/feedsSlice'; -import { updateMapState } from '../slices/mapSlice'; -import { updateAdvisories } from '../slices/cmsSlice'; - -// External Components -import Button from 'react-bootstrap/Button'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { - faPlus, - faMinus, - faUpRightAndDownLeftFromCenter, - faLocationCrosshairs, - faXmark, -} from '@fortawesome/pro-solid-svg-icons'; -import {useMediaQuery} from '@uidotdev/usehooks'; - -// Components and functions -import CamPopup from './map/camPopup.js'; -import { - getEventPopup, - getFerryPopup, - getWeatherPopup, - 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, isRestStopClosed } 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'; -import { loadRegionalLayers } from './map/layers/regionalLayer.js'; -import { - compareRoutePoints, - filterByRoute, - fitMap, - blueLocationMarkup, - redLocationMarkup, - setLocationPin, - setEventStyle, - setZoomPan, - zoomIn, - zoomOut, -} from './map/helper.js'; -import { getFerries } from './data/ferries.js'; -import { getFerriesLayer } from './map/layers/ferriesLayer.js'; -import { getCameras, addCameraGroups } from './data/webcams.js'; -import { getRouteLayer } from './map/routeLayer.js'; -import { MapContext } from '../App.js'; -import { NetworkError, ServerError } from './data/helper'; -import NetworkErrorPopup from './map/errors/NetworkError'; -import ServerErrorPopup from './map/errors/ServerError'; -import AdvisoriesOnMap from './advisories/AdvisoriesOnMap'; -import CurrentCameraIcon from './CurrentCameraIcon'; -import Filters from './Filters.js'; -import RouteSearch from './map/RouteSearch.js'; -import ExitSurvey from './advisories/ExitSurvey.js'; -import trackEvent from './TrackEvent.js'; - -// Map & geospatial imports -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 * as turf from '@turf/turf'; -import Map from 'ol/Map'; -import Overlay from 'ol/Overlay.js'; -import Geolocation from 'ol/Geolocation.js'; -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 overrides from './map/overrides.js'; - -// Styling -import { - cameraStyles, - ferryStyles, - roadWeatherStyles, - regionalStyles, - restStopStyles, - restStopClosedStyles, - restStopTruckStyles, - restStopTruckClosedStyles, -} from './data/featureStyleDefinitions.js'; -import './Map.scss'; - - -export default function MapWrapper(props) { - let { camera, isCamDetail, mapViewRoute, loadCamDetails } = props; - - // Redux - const dispatch = useDispatch(); - const { - cameras, - filteredCameras, - camFilterPoints, // Cameras - events, - filteredEvents, - eventFilterPoints, // Events - advisories, // CMS - ferries, - filteredFerries, - ferryFilterPoints, // Ferries - currentWeather, - filteredCurrentWeathers, - currentWeatherFilterPoints, // Current Weather - regionalWeather, - filteredRegionalWeathers, - regionalWeatherFilterPoints, - regionalTimeStamp, // Regional Weather - restStops, - filteredRestStops, - restStopFilterPoints, // Rest Stops - searchLocationFrom, - searchLocationTo, - selectedRoute, // Routing - zoom, - pan, // Map - } = useSelector( - useCallback( - memoize(state => ({ - // Cameras - cameras: state.feeds.cameras.list, - filteredCameras: state.feeds.cameras.filteredList, - camFilterPoints: state.feeds.cameras.filterPoints, - // Events - events: state.feeds.events.list, - filteredEvents: state.feeds.events.filteredList, - eventFilterPoints: state.feeds.events.filterPoints, - // CMS - advisories: state.cms.advisories.list, - // Ferries - ferries: state.feeds.ferries.list, - filteredFerries: state.feeds.ferries.filteredList, - ferryFilterPoints: state.feeds.ferries.filterPoints, - // Current Weather - currentWeather: state.feeds.weather.list, - filteredCurrentWeathers: state.feeds.weather.filteredList, - currentWeatherFilterPoints: state.feeds.weather.filterPoints, - // Regional Weather - regionalWeather: state.feeds.regional.list, - filteredRegionalWeathers: state.feeds.regional.filteredList, - regionalWeatherFilterPoints: state.feeds.regional.filterPoints, - // Rest Stops - restStops: state.feeds.restStops.list, - filteredRestStops: state.feeds.restStops.filteredList, - restStopFilterPoints: state.feeds.restStops.filterPoints, - // Routing - searchLocationFrom: state.routes.searchLocationFrom, - searchLocationTo: state.routes.searchLocationTo, - selectedRoute: state.routes.selectedRoute, - // Map - zoom: state.map.zoom, - pan: state.map.pan, - })), - ), - ); - - // Context - const { mapContext, setMapContext } = useContext(MapContext); - - // Refs - const isInitialMountLocation = useRef('not set'); - const isInitialMountRoute = useRef(true); - const mapElement = useRef(); - const mapRef = useRef(); - const popup = useRef(); - const panel = useRef(); - const mapLayers = useRef({}); - const mapView = useRef(); - const container = useRef(); - const geolocation = useRef(null); - const hoveredFeature = useRef(); - const locationPinRef = useRef(null); - const cameraPopupRef = useRef(null); - - // Workaround for OL handlers not being able to read states - const [clickedCamera, setClickedCamera] = useState(); - const clickedCameraRef = useRef(); - const updateClickedCamera = feature => { - clickedCameraRef.current = feature; - setClickedCamera(feature); - }; - - const [clickedEvent, setClickedEvent] = useState(); - const clickedEventRef = useRef(); - const updateClickedEvent = feature => { - clickedEventRef.current = feature; - setClickedEvent(feature); - }; - - const [clickedFerry, setClickedFerry] = useState(); - const clickedFerryRef = useRef(); - const updateClickedFerry = feature => { - clickedFerryRef.current = feature; - setClickedFerry(feature); - }; - - const [clickedWeather, setClickedWeather] = useState(); - const clickedWeatherRef = useRef(); - const updateClickedWeather = feature => { - clickedWeatherRef.current = feature; - setClickedWeather(feature); - }; - - const [clickedRegional, setClickedRegional] = useState(); - const clickedRegionalRef = useRef(); - const updateClickedRegional = feature => { - clickedRegionalRef.current = feature; - setClickedRegional(feature); - }; - - const [clickedRestStop, setClickedRestStop] = useState(); - const clickedRestStopRef = useRef(); - const updateClickedRestStop = feature => { - clickedRestStopRef.current = feature; - setClickedRestStop(feature); - }; - - const [advisoriesInView, setAdvisoriesInView] = useState([]); - const [showNetworkError, setShowNetworkError] = useState(false); - const [showServerError, setShowServerError] = useState(false); - - // Define the function to be executed after the delay - function resetCameraPopupRef() { - cameraPopupRef.current = null; - } - - // initialization hook for the OpenLayers map logic - useEffect(() => { - if (mapRef.current) return; // stops map from intializing more than once - - container.current = document.getElementById('popup'); - - popup.current = new Overlay({ - element: container.current, - autoPan: { - animation: { - duration: 250, - }, - margin: 90, - }, - }); - - // base tile map layer - const vectorLayer = new VectorTileLayer({ - declutter: true, - source: new VectorTileSource({ - format: new MVT(), - url: window.BASE_MAP, - }), - }); - - // initialize starting optional mapLayers - mapLayers.current = { - tid: Date.now(), - }; - - // Set map extent (W, S, E, N) - const extent = [-143.230138, 46.180153, -109.977437, 65.591323]; - const transformedExtent = transformExtent(extent, 'EPSG:4326', 'EPSG:3857'); - - mapView.current = new View({ - projection: 'EPSG:3857', - constrainResolution: true, - center: camera ? handleCenter() : fromLonLat(pan), - zoom: handleZoom(), - maxZoom: 15, - extent: transformedExtent, - }); - - // Apply the basemap style from the arcgis resource - fetch(window.MAP_STYLE, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }).then(function (response) { - response.json().then(function (glStyle) { - // DBC22-2153 - glStyle.metadata['ol:webfonts'] = '/fonts/{font-family}/{fontweight}{-fontstyle}.css'; - console.log(glStyle); - for (const layer of glStyle.layers) { - Object.assign(layer, overrides[layer.id] || {}); - } - applyStyle(vectorLayer, glStyle, 'esri'); - }); - }); - - // create map - mapRef.current = new Map({ - target: mapElement.current, - layers: [vectorLayer], - overlays: [popup.current], - view: mapView.current, - pixelRatio: 1.875, - moveTolerance: 7, - controls: [new ScaleLine({ units: 'metric' })], - }); - window.mapRef = mapRef; - - geolocation.current = new Geolocation({ - projection: mapView.current.getProjection(), - }); - - mapRef.current.once('loadstart', async () => { - if (camera && !isCamDetail) { - if (camera.event_type) { - updateClickedEvent(camera); - } else { - updateClickedCamera(camera); - } - } - }); - - mapRef.current.on('moveend', function () { - if(smallScreen){ - resetHoveredStates(); - } - dispatch( - updateMapState({ - pan: toLonLat(mapView.current.getCenter()), - zoom: mapView.current.getZoom(), - }), - ); - }); - - // Click states - const resetClickedStates = (targetFeature) => { - // camera is set to data structure rather than map feature - if (clickedCameraRef.current && !clickedCameraRef.current.setStyle) { - clickedCameraRef.current = mapLayers.current['highwayCams'] - .getSource() - .getFeatureById(clickedCameraRef.current.id); - } - - if ( - clickedCameraRef.current && - targetFeature != clickedCameraRef.current - ) { - clickedCameraRef.current.setStyle(cameraStyles['static']); - updateClickedCamera(null); - } - - // event is set to data structure rather than map feature - if (clickedEventRef.current && !clickedEventRef.current.ol_uid) { - const features = - mapLayers.current[ - clickedEventRef.current.display_category - ].getSource(); - clickedEventRef.current = features.getFeatureById( - clickedEventRef.current.id, - ); - } - - if (clickedEventRef.current && targetFeature != clickedEventRef.current) { - setEventStyle(clickedEventRef.current, 'static'); - setEventStyle(clickedEventRef.current.get('altFeature') || [], 'static') - clickedEventRef.current.set('clicked', false); - updateClickedEvent(null); - } - - if (clickedFerryRef.current && targetFeature != clickedFerryRef.current) { - clickedFerryRef.current.setStyle(ferryStyles['static']); - updateClickedFerry(null); - } - - if ( - clickedWeatherRef.current && - targetFeature !== clickedWeatherRef.current - ) { - clickedWeatherRef.current.setStyle(roadWeatherStyles['static']); - updateClickedWeather(null); - } - - if ( - clickedRegionalRef.current && - targetFeature !== clickedRegionalRef.current - ) { - clickedRegionalRef.current.setStyle(regionalStyles['static']); - updateClickedRegional(null); - } - - if ( - clickedRestStopRef.current && - targetFeature != clickedRestStopRef.current - ) { - if (clickedRestStopRef.current !== undefined) { - const isClosed = isRestStopClosed( - clickedRestStopRef.current.values_.properties, - ); - const isLargeVehiclesAccommodated = - clickedRestStopRef.current.values_.properties - .ACCOM_COMMERCIAL_TRUCKS === 'Yes' - ? true - : false; - if (isClosed) { - if (isLargeVehiclesAccommodated) { - clickedRestStopRef.current.setStyle( - restStopTruckClosedStyles['static'], - ); - } else { - clickedRestStopRef.current.setStyle( - restStopClosedStyles['static'], - ); - } - } else { - if (isLargeVehiclesAccommodated) { - clickedRestStopRef.current.setStyle( - restStopTruckStyles['static'], - ); - } else { - clickedRestStopRef.current.setStyle(restStopStyles['static']); - } - } - } - updateClickedRestStop(null); - } - }; - - const camClickHandler = feature => { - if (!isCamDetail) { - resetClickedStates(feature); - - // set new clicked camera feature - feature.setStyle(cameraStyles['active']); - feature.setProperties({ clicked: true }, true); - - updateClickedCamera(feature); - - cameraPopupRef.current = popup; - - setTimeout(resetCameraPopupRef, 500); - - } else { - setZoomPan(mapView, null, feature.getGeometry().getCoordinates()); - loadCamDetails(feature.getProperties()); - } - }; - - const eventClickHandler = feature => { - // reset previous clicked feature - resetClickedStates(feature); - - // set new clicked event feature - setEventStyle(feature, 'active'); - setEventStyle(feature.get('altFeature') || [], 'active'); - feature.setProperties({ clicked: true }, true); - - updateClickedEvent(feature); - }; - - const ferryClickHandler = feature => { - // reset previous clicked feature - resetClickedStates(feature); - - // set new clicked ferry feature - feature.setStyle(ferryStyles['active']); - feature.setProperties({ clicked: true }, true); - updateClickedFerry(feature); - }; - - const weatherClickHandler = feature => { - // reset previous clicked feature - resetClickedStates(feature); - - // set new clicked ferry feature - feature.setStyle(roadWeatherStyles['active']); - feature.setProperties({ clicked: true }, true); - updateClickedWeather(feature); - }; - - const regionalClickHandler = feature => { - // reset previous clicked feature - resetClickedStates(feature); - - // set new clicked ferry feature - feature.setStyle(regionalStyles['active']); - feature.setProperties({ clicked: true }, true); - updateClickedRegional(feature); - }; - - const restStopClickHandler = feature => { - // reset previous clicked feature - resetClickedStates(feature); - - // set new clicked rest stop feature - const isClosed = isRestStopClosed(feature.values_.properties); - const isLargeVehiclesAccommodated = - feature.values_.properties.ACCOM_COMMERCIAL_TRUCKS === 'Yes' - ? true - : false; - if (isClosed) { - if (isLargeVehiclesAccommodated) { - feature.setStyle(restStopTruckClosedStyles['active']); - } else { - feature.setStyle(restStopClosedStyles['active']); - } - } else { - if (isLargeVehiclesAccommodated) { - feature.setStyle(restStopTruckStyles['active']); - } else { - feature.setStyle(restStopStyles['active']); - } - } - feature.setProperties({ clicked: true }, true); - updateClickedRestStop(feature); - }; - - mapRef.current.on('click', async e => { - const features = mapRef.current.getFeaturesAtPixel(e.pixel, { - hitTolerance: 20, - }); - - if (features.length) { - const clickedFeature = features[0]; - switch (clickedFeature.getProperties()['type']) { - case 'camera': - trackEvent( - 'click', - 'map', - 'camera', - clickedFeature.getProperties().name, - ); - camClickHandler(clickedFeature); - return; - case 'event': - eventClickHandler(clickedFeature); - return; - case 'ferry': - trackEvent( - 'click', - 'map', - 'ferry', - clickedFeature.getProperties().name, - ); - ferryClickHandler(clickedFeature); - return; - case 'weather': - trackEvent( - 'click', - 'map', - 'weather', - clickedFeature.getProperties().weather_station_name, - ); - weatherClickHandler(clickedFeature); - return; - case 'regional': - trackEvent( - 'click', - 'map', - 'regional weather', - clickedFeature.getProperties().name, - ); - regionalClickHandler(clickedFeature); - return; - case 'rest': - trackEvent( - 'click', - 'map', - 'rest stop', - clickedFeature.getProperties().properties.REST_AREA_NAME - ); - restStopClickHandler(clickedFeature); - return; - } - } - - // Close popups if clicked on blank space - closePopup(); - }); - - // Hover states - const resetHoveredStates = (targetFeature) => { - if (hoveredFeature.current && targetFeature != hoveredFeature.current) { - if (!hoveredFeature.current.getProperties().clicked) { - switch (hoveredFeature.current.getProperties()['type']) { - case 'camera': - hoveredFeature.current.setStyle(cameraStyles['static']); - break; - case 'event': - setEventStyle(hoveredFeature.current, 'static'); - setEventStyle(hoveredFeature.current.get('altFeature') || [], 'static'); - break; - case 'ferry': - hoveredFeature.current.setStyle(ferryStyles['static']); - break; - case 'weather': - hoveredFeature.current.setStyle(roadWeatherStyles['static']); - break; - case 'regional': - hoveredFeature.current.setStyle(regionalStyles['static']); - break; - case 'rest': - { - const isClosed = isRestStopClosed( - hoveredFeature.current.values_.properties, - ); - const isLargeVehiclesAccommodated = - hoveredFeature.current.values_.properties - .ACCOM_COMMERCIAL_TRUCKS === 'Yes' - ? true - : false; - if (isClosed) { - if (isLargeVehiclesAccommodated) { - hoveredFeature.current.setStyle( - restStopTruckClosedStyles['static'], - ); - } else { - hoveredFeature.current.setStyle( - restStopClosedStyles['static'], - ); - } - } else { - if (isLargeVehiclesAccommodated) { - hoveredFeature.current.setStyle( - restStopTruckStyles['static'], - ); - } else { - hoveredFeature.current.setStyle(restStopStyles['static']); - } - } - } - break; - } - } - - hoveredFeature.current = null; - } - }; - - mapRef.current.on('pointermove', async e => { - const features = mapRef.current.getFeaturesAtPixel(e.pixel, { - hitTolerance: 20, - }); - - if (features.length) { - const targetFeature = features[0]; - resetHoveredStates(targetFeature); - hoveredFeature.current = targetFeature; - switch (targetFeature.getProperties()['type']) { - case 'camera': - if (!targetFeature.getProperties().clicked) { - targetFeature.setStyle(cameraStyles['hover']); - } - return; - case 'event': - if (!targetFeature.getProperties().clicked) { - setEventStyle(targetFeature, 'hover'); - setEventStyle(targetFeature.get('altFeature') || [], 'hover'); - } - return; - case 'ferry': - if (!targetFeature.getProperties().clicked) { - targetFeature.setStyle(ferryStyles['hover']); - } - return; - case 'weather': - if (!targetFeature.getProperties().clicked) { - targetFeature.setStyle(roadWeatherStyles['hover']); - } - return; - case 'rest': - if (!targetFeature.getProperties().clicked) { - const isClosed = isRestStopClosed( - targetFeature.values_.properties, - ); - const isLargeVehiclesAccommodated = - targetFeature.values_.properties.ACCOM_COMMERCIAL_TRUCKS === - 'Yes' - ? true - : false; - if (isClosed) { - if (isLargeVehiclesAccommodated) { - targetFeature.setStyle(restStopTruckClosedStyles['hover']); - } else { - targetFeature.setStyle(restStopClosedStyles['hover']); - } - } else { - if (isLargeVehiclesAccommodated) { - targetFeature.setStyle(restStopTruckStyles['hover']); - } else { - targetFeature.setStyle(restStopStyles['hover']); - } - } - } - return; - case 'regional': - if (!targetFeature.getProperties().clicked) { - targetFeature.setStyle(regionalStyles['hover']); - } - return; - } - } - - // Reset on blank space - resetHoveredStates(null); - }); - - loadData(true); - }); - - // Error handling - const displayError = (error) => { - if (error instanceof ServerError) { - setShowServerError(true); - - } else if (error instanceof NetworkError) { - setShowNetworkError(true); - } - } - - // Location search - useEffect(() => { - if (searchLocationFrom && searchLocationFrom.length) { - if (locationPinRef.current) { - mapRef.current.removeOverlay(locationPinRef.current); - } - - setLocationPin( - searchLocationFrom[0].geometry.coordinates, - blueLocationMarkup, - mapRef, - locationPinRef, - ); - - if (isInitialMountLocation.current === 'not set') { - // first run of this effector - // store the initial searchLocationFrom.[0].label so that subsequent - // runs can be evaluated to detect change in the search from - isInitialMountLocation.current = searchLocationFrom[0].label; - - // only zoomPan on from location change when to location is NOT set - } else if ( - isInitialMountLocation.current !== searchLocationFrom[0].label && - searchLocationTo.length == 0 - ) { - isInitialMountLocation.current = false; - setZoomPan( - mapView, - 9, - fromLonLat(searchLocationFrom[0].geometry.coordinates), - ); - } - } else { - // initial location was set, so no need to prevent pan/zoom - isInitialMountLocation.current = false; - } - }, [searchLocationFrom]); - - // Route layer - useEffect(() => { - if (isInitialMountRoute.current) { - // Do nothing on first load - isInitialMountRoute.current = false; - return; - } - - // Remove existing layer and reload data on route change - if (mapLayers.current['routeLayer']) { - mapRef.current.removeLayer(mapLayers.current['routeLayer']); - } - - loadData(false); - }, [selectedRoute]); - - const loadCameras = async route => { - const routePoints = route ? route.points : null; - - // Load if filtered cams don't exist or route doesn't match - if (!filteredCameras || !compareRoutePoints(routePoints, camFilterPoints)) { - // Fetch data if it doesn't already exist - const camData = cameras ? cameras : await getCameras().catch((error) => displayError(error)); - - // Filter data by route - const filteredCamData = route ? filterByRoute(camData, route, null, true) : camData; - - dispatch( - updateCameras({ - list: camData, - filteredList: filteredCamData, - filterPoints: route ? route.points : null - }) - ); - } - }; - - useEffect(() => { - // Remove layer if it already exists - if (mapLayers.current['highwayCams']) { - mapRef.current.removeLayer(mapLayers.current['highwayCams']); - } - - // Add layer if array exists - if (filteredCameras) { - // Deep clone and add group reference to each cam - const clonedCameras = JSON.parse(JSON.stringify(filteredCameras)); - const finalCameras = addCameraGroups(clonedCameras); - - // Generate and add layer - mapLayers.current['highwayCams'] = getCamerasLayer( - finalCameras, - mapRef.current.getView().getProjection().getCode(), - mapContext - ); - - mapRef.current.addLayer(mapLayers.current['highwayCams']); - mapLayers.current['highwayCams'].setZIndex(78); - } - }, [filteredCameras]); - - // Event layers - const loadEvents = async route => { - const routePoints = route ? route.points : null; - - // Load if filtered events don't exist or route doesn't match - if (!filteredEvents || !compareRoutePoints(routePoints, eventFilterPoints)) { - // Fetch data if it doesn't already exist - const eventData = events ? events : await getEvents().catch((error) => displayError(error)); - - // Filter data by route - const filteredEventData = route ? filterByRoute(eventData, route) : eventData; - - dispatch( - updateEvents({ - list: eventData, - filteredList: filteredEventData, - filterPoints: route ? route.points : null - }) - ); - } - }; - - useEffect(() => { - loadEventsLayers(filteredEvents, mapContext, mapLayers, mapRef); - }, [filteredEvents]); - - // Ferries layer - const loadFerries = async route => { - const routePoints = route ? route.points : null; - - // Load if filtered cams don't exist or route doesn't match - if (!filteredFerries || !compareRoutePoints(routePoints, ferryFilterPoints)) { - // Fetch data if it doesn't already exist - const ferryData = ferries ? ferries : await getFerries().catch((error) => displayError(error)); - - // Filter data by route - const filteredFerryData = route ? filterByRoute(ferryData, route) : ferryData; - - dispatch( - updateFerries({ - list: ferryData, - filteredList: filteredFerryData, - filterPoints: route ? route.points : null - }) - ); - } - }; - - useEffect(() => { - // Remove layer if it already exists - if (mapLayers.current['inlandFerries']) { - mapRef.current.removeLayer(mapLayers.current['inlandFerries']); - } - - // Add layer if array exists - if (filteredFerries) { - // Generate and add layer - mapLayers.current['inlandFerries'] = getFerriesLayer( - filteredFerries, - mapRef.current.getView().getProjection().getCode(), - mapContext, - ); - - mapRef.current.addLayer(mapLayers.current['inlandFerries']); - mapLayers.current['inlandFerries'].setZIndex(68); - } - }, [filteredFerries]); - - // Rest stops layer - const loadRestStops = async route => { - const routePoints = route ? route.points : null; - - // Load if filtered cams don't exist or route doesn't match - if (!filteredRestStops || !compareRoutePoints(routePoints, restStopFilterPoints)) { - // Fetch data if it doesn't already exist - const restStopsData = restStops ? restStops : await getRestStops().catch((error) => displayError(error)); - - // Filter data by route - const filteredRestStopsData = route ? filterByRoute(restStopsData, route) : restStopsData; - - dispatch( - updateRestStops({ - list: restStopsData, - filteredList: filteredRestStopsData, - filterPoints: route ? route.points : null - }) - ); - } - }; - - useEffect(() => { - // Remove layer if it already exists - if (mapLayers.current['restStops']) { - mapRef.current.removeLayer(mapLayers.current['restStops']); - } - - // Add layer if array exists - if (filteredRestStops) { - // Generate and add layer - mapLayers.current['restStops'] = getRestStopsLayer( - filteredRestStops, - mapRef.current.getView().getProjection().getCode(), - mapContext, - ); - - mapRef.current.addLayer(mapLayers.current['restStops']); - mapLayers.current['restStops'].setZIndex(68); - } - }, [filteredRestStops]); - - // Current weather layer - const loadWeather = async route => { - const routePoints = route ? route.points : null; - - // Load if filtered cams don't exist or route doesn't match - if (!filteredCurrentWeathers || !compareRoutePoints(routePoints, currentWeatherFilterPoints)) { - // Fetch data if it doesn't already exist - const currentWeathersData = currentWeather ? currentWeather : await getWeather().catch((error) => displayError(error)); - - // Filter data by route - const filteredCurrentWeathersData = route ? filterByRoute(currentWeathersData, route, 15000) : currentWeathersData; - - dispatch( - updateWeather({ - list: currentWeathersData, - filteredList: filteredCurrentWeathersData, - filterPoints: route ? route.points : null - }) - ); - } - }; - - useEffect(() => { - if (mapLayers.current['weather']) { - mapRef.current.removeLayer(mapLayers.current['weather']); - } - - if (filteredCurrentWeathers) { - mapLayers.current['weather'] = loadWeatherLayers( - filteredCurrentWeathers, - mapContext, - mapRef.current.getView().getProjection().getCode(), - ); - mapRef.current.addLayer(mapLayers.current['weather']); - mapLayers.current['weather'].setZIndex(66); - } - }, [filteredCurrentWeathers]); - - // 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 = turf.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 = turf.polygon(advisory.geometry.coordinates); - if (turf.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().catch((error) => displayError(error)), - 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(5); - - if (mapRef.current) { - mapRef.current.on('moveend', onMoveEnd); - } - } - }, [advisories]); - - // Regional weather layer - const loadRegional = async route => { - const routePoints = route ? route.points : null; - - // Load if filtered cams don't exist or route doesn't match - if (!filteredRegionalWeathers || !compareRoutePoints(routePoints, regionalWeatherFilterPoints)) { - // Fetch data if it doesn't already exist - const regionalWeathersData = regionalWeather ? regionalWeather : await getRegional().catch((error) => displayError(error)); - - // Filter with 20km extra tolerance - const filteredRegionalWeathersData = filterByRoute(regionalWeathersData, route, 15000); - - dispatch( - updateRegional({ - list: regionalWeathersData, - filteredList: filteredRegionalWeathersData, - filterPoints: route ? route.points : null - }) - ); - } - }; - - useEffect(() => { - if (mapLayers.current['regional']) { - mapRef.current.removeLayer(mapLayers.current['regional']); - } - - if (filteredRegionalWeathers) { - mapLayers.current['regional'] = loadRegionalLayers( - filteredRegionalWeathers, - mapContext, - mapRef.current.getView().getProjection().getCode(), - ); - mapRef.current.addLayer(mapLayers.current['regional']); - mapLayers.current['regional'].setZIndex(67); - } - }, [filteredRegionalWeathers]); - - // Function to load all data - const loadData = isInitialMount => { - if (selectedRoute && selectedRoute.routeFound) { - const routeLayer = getRouteLayer( - selectedRoute, - mapRef.current.getView().getProjection().getCode(), - ); - mapLayers.current['routeLayer'] = routeLayer; - mapRef.current.addLayer(routeLayer); - - // Clear and update data - loadCameras(selectedRoute); - loadEvents(selectedRoute); - loadFerries(selectedRoute); - loadWeather(selectedRoute); - loadRegional(selectedRoute); - loadRestStops(selectedRoute); - loadAdvisories(); - - // Zoom/pan to route on route updates - if (!isInitialMount) { - fitMap(selectedRoute.route, mapView); - } - } else { - // Clear and update data - loadCameras(); - loadEvents(); - loadFerries(); - loadWeather(); - loadRegional(); - loadRestStops(); - loadAdvisories(); - } - }; - - function closePopup() { - // camera is set to data structure rather than map feature - if (clickedCameraRef.current && !clickedCameraRef.current.setStyle) { - clickedCameraRef.current = mapLayers.current['highwayCams'] - .getSource() - .getFeatureById(clickedCameraRef.current.id); - } - - // check for active camera icons - if (clickedCameraRef.current) { - clickedCameraRef.current.setStyle(cameraStyles['static']); - clickedCameraRef.current.set('clicked', false); - updateClickedCamera(null); - } - - // check for active event icons - - // event is set to data structure rather than map feature - if (clickedEventRef.current && !clickedEventRef.current.ol_uid) { - const features = - mapLayers.current[clickedEventRef.current.display_category].getSource(); - clickedEventRef.current = features.getFeatureById( - clickedEventRef.current.id, - ); - } - - if (clickedEventRef.current) { - setEventStyle(clickedEventRef.current, 'static'); - setEventStyle(clickedEventRef.current.get('altFeature') || [], 'static'); - clickedEventRef.current.set('clicked', false); - updateClickedEvent(null); - } - - // check for active ferry icons - if (clickedFerryRef.current) { - clickedFerryRef.current.setStyle(ferryStyles['static']); - clickedFerryRef.current.set('clicked', false); - updateClickedFerry(null); - } - - // check for active weather icons - if (clickedWeatherRef.current) { - clickedWeatherRef.current.setStyle(roadWeatherStyles['static']); - clickedWeatherRef.current.set('clicked', false); - updateClickedWeather(null); - } - - // check for active weather icons - if (clickedRegionalRef.current) { - clickedRegionalRef.current.setStyle(regionalStyles['static']); - clickedRegionalRef.current.set('clicked', false); - updateClickedRegional(null); - } - - // check for active rest stop icons - if (clickedRestStopRef.current) { - const isClosed = isRestStopClosed(clickedRestStopRef.current.properties); - const isLargeVehiclesAccommodated = clickedRestStopRef.current.properties - ? clickedRestStopRef.current.properties.ACCOM_COMMERCIAL_TRUCKS === - 'Yes' - : false; - if (isClosed) { - if (isLargeVehiclesAccommodated) { - clickedRestStopRef.current.setStyle( - restStopTruckClosedStyles['static'], - ); - } else { - clickedRestStopRef.current.setStyle(restStopClosedStyles['static']); - } - } else { - if (isLargeVehiclesAccommodated) { - clickedRestStopRef.current.setStyle(restStopTruckStyles['static']); - } else { - clickedRestStopRef.current.setStyle(restStopStyles['static']); - } - } - clickedRestStopRef.current.set('clicked', false); - updateClickedRestStop(null); - } - - // Reset cam popup handler lock timer - cameraPopupRef.current = null; - } - - function toggleMyLocation() { - if ('geolocation' in navigator) { - navigator.geolocation.getCurrentPosition( - position => { - const { latitude, longitude } = position.coords; - if ( - position.coords.longitude <= -113.7 && - position.coords.longitude >= -139.3 && - position.coords.latitude <= 60.1 && - position.coords.latitude >= 48.2 - ) { - setZoomPan(mapView, 9, fromLonLat([longitude, latitude])); - setLocationPin([longitude, latitude], redLocationMarkup, mapRef); - } else { - // set my location to the center of BC for users outside of BC - setZoomPan(mapView, 9, fromLonLat([-126.5, 54.2])); - setLocationPin([-126.5, 54.2], redLocationMarkup, mapRef); - } - }, - error => { - if (error.code === error.PERMISSION_DENIED) { - // The user has blocked location access - console.error('Location access denied by user.', error); - } else { - // Zoom out and center to BC if location not available - setZoomPan(mapView, 9, fromLonLat([-126.5, 54.2])); - } - }, - ); - } - } - - function togglePanel() { - panel.current.classList.toggle('open'); - panel.current.classList.remove('maximized'); - if (!panel.current.classList.contains('open')) { - closePopup(); - } - } - - function maximizePanel() { - if (panel.current.classList.contains('open')) { - if (!panel.current.classList.contains('maximized')) { - panel.current.classList.add('maximized'); - } else { - panel.current.classList.remove('maximized'); - } - } - } - - function handleCenter() { - if (typeof camera === 'string') { - camera = JSON.parse(camera); - } - return Array.isArray(camera.location.coordinates[0]) - ? fromLonLat( - camera.location.coordinates[ - Math.floor(camera.location.coordinates.length / 2) - ], - ) - : fromLonLat(camera.location.coordinates); - } - - function handleZoom() { - if (typeof camera === 'string') { - camera = JSON.parse(camera); - } - if (isCamDetail || camera) { - return 12; - } else { - return zoom; - } - } - - function toggleLayer(layer, checked) { - mapLayers.current[layer].setVisible(checked); - - // Set context and local storage - mapContext.visible_layers[layer] = checked; - setMapContext(mapContext); - localStorage.setItem('mapContext', JSON.stringify(mapContext)); - } - - // Force camera and inland ferries filters to be checked on preview mode - if (isCamDetail) { - mapContext.visible_layers['highwayCams'] = true; - mapContext.visible_layers['inlandFerries'] = true; - } - - const openPanel = !!( - clickedCamera || - clickedEvent || - clickedFerry || - clickedWeather || - clickedRegional || - clickedRestStop - ); - - const smallScreen = useMediaQuery('only screen and (max-width: 767px)'); - - return ( -
-
{ - if (keyEvent.keyCode == 13) { - maximizePanel(); - } - }}> - - -
- {clickedCamera && ( - - )} - - {clickedEvent && getEventPopup(clickedEvent)} - - {clickedFerry && getFerryPopup(clickedFerry)} - - {clickedWeather && getWeatherPopup(clickedWeather)} - - {clickedRegional && getRegionalPopup(clickedRegional)} - - {clickedRestStop && getRestStopPopup(clickedRestStop)} -
-
- -
- - {!isCamDetail && ( -
- {smallScreen && ( - - )} - - -
- )} - - {(!isCamDetail && smallScreen) && ( - - - - - )} - - {(!isCamDetail && !smallScreen) && ( - - - - - - )} - -
- -
- -
- -
- - - - {isCamDetail && ( - - )} - - {isCamDetail && ( - - )} - - {isCamDetail && ( - - )} - - {showNetworkError && - - } - - {!showNetworkError && showServerError && - - } -
- ); -} diff --git a/src/frontend/src/Components/Pin.js b/src/frontend/src/Components/Pin.js deleted file mode 100644 index ae1950493..000000000 --- a/src/frontend/src/Components/Pin.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; - -import {useDrag} from 'react-dnd'; - -import './Pin.scss'; - -export default function Pin({role}) { - const fill = role === 'start' ? '#003399' : '#009933'; - - - const [{opacity}, dragRef] = useDrag( - () => ({ - type: 'pin', - item: () => ({role}), - collect: (monitor) => ({ - opacity: monitor.isDragging() ? 0.5 : 1, - }), - }), - ); - - return ( -
- -
- ); -} diff --git a/src/frontend/src/Components/Pin.scss b/src/frontend/src/Components/Pin.scss deleted file mode 100644 index 6b8923883..000000000 --- a/src/frontend/src/Components/Pin.scss +++ /dev/null @@ -1,30 +0,0 @@ -.pin { - font-size: 2rem; - background: none; - width: 64px; - border: 3px dotted rgb(204, 204, 204); - height: 64px; - border-radius: 50%; - text-align: center; - cursor: grab; - padding-top: 11px; - - svg { - margin: auto; - } - - @media screen and (max-width: 729px) { - background: none; - width: 48px; - border: 3px dotted rgb(204, 204, 204); - height: 48px; - border-radius: 50%; - text-align: center; - cursor: grab; - padding-top: 2px; - - svg { - width: 22px; - } - } -} diff --git a/src/frontend/src/Components/__tests__/test_data/event_feed_list_of_five.json b/src/frontend/src/Components/__tests__/test_data/event_feed_list_of_five.json deleted file mode 100644 index ed9bf7ff4..000000000 --- a/src/frontend/src/Components/__tests__/test_data/event_feed_list_of_five.json +++ /dev/null @@ -1,2261 +0,0 @@ -{ - "events": [ - { - "jurisdiction_url": "https://api.open511.gov.bc.ca/jurisdiction", - "url": "https://api.open511.gov.bc.ca/events/drivebc.ca/DBC-28386", - "id": "drivebc.ca/DBC-28386", - "headline": "INCIDENT", - "status": "ACTIVE", - "created": "2021-04-26T08:19:02-07:00", - "updated": "2023-04-13T10:30:12-07:00", - "description": "Quesnel-Hixon Road, in both directions. Landslide at Cottonwood bridge. Road closed. Next update time Wed Jul 12 at 2:00 PM PDT. Last updated Thu Apr 13 at 10:30 AM PDT. (DBC-28386)", - "+ivr_message": "Quesnel-Hixon Road, in both directions. Landslide at Cottonwood bridge. Road closed. Next update time Wednesday, July 12 at 2:00 PM. Last updated Thursday, April 13 at 10:30 AM.", - "+linear_reference_km": -1, - "schedule": { - "intervals": [ - "2021-04-26T15:19/" - ] - }, - "event_type": "INCIDENT", - "event_subtypes": [ - "HAZARD" - ], - "severity": "MAJOR", - "geography": { - "type": "Point", - "coordinates": [ - -122.479074, - 53.155476 - ] - }, - "roads": [ - { - "name": "Other Roads", - "from": "at Cottonwood bridge", - "direction": "BOTH" - } - ], - "areas": [ - { - "url": "http://www.geonames.org/8630134", - "name": "Cariboo District", - "id": "drivebc.ca/7" - } - ] - }, - { - "jurisdiction_url": "https://api.open511.gov.bc.ca/jurisdiction", - "url": "https://api.open511.gov.bc.ca/events/drivebc.ca/DBC-46014", - "id": "drivebc.ca/DBC-46014", - "headline": "CONSTRUCTION", - "status": "ACTIVE", - "created": "2022-10-21T08:01:01-07:00", - "updated": "2023-06-25T14:42:45-07:00", - "description": "Highway 14, in both directions. Construction work between Ludlow Rd and Kangaroo Rd for 6.0 km (Sooke to Metchosin). Night-time Highway line marking and Daytime Barrier placement, watch for changing traffic patterns and follow the direction of traffic control. Expect Delays. Last updated Sun Jun 25 at 2:42 PM PDT. (DBC-46014)", - "+ivr_message": "Highway 14, in both directions. Construction work between Ludlow Rd and Kangaroo Rd for 6.0 km (Sooke to Metchosin). Night-time Highway line marking and Daytime Barrier placement, watch for changing traffic patterns and follow the direction of traffic control. Expect Delays. Last updated Sunday, June 25 at 2:42 PM.", - "+linear_reference_km": 78.35, - "schedule": { - "intervals": [ - "2022-10-21T15:01/" - ] - }, - "event_type": "CONSTRUCTION", - "event_subtypes": [ - "ROAD_MAINTENANCE" - ], - "severity": "MINOR", - "geography": { - "type": "LineString", - "coordinates": [ - [ - -123.658384, - 48.39398 - ], - [ - -123.657644, - 48.394198 - ], - [ - -123.657044, - 48.394374 - ], - [ - -123.656459, - 48.394547 - ], - [ - -123.656141, - 48.394641 - ], - [ - -123.655938, - 48.394684 - ], - [ - -123.65577, - 48.394721 - ], - [ - -123.655441, - 48.394754 - ], - [ - -123.655162, - 48.394748 - ], - [ - -123.654904, - 48.394703 - ], - [ - -123.653611, - 48.39435 - ], - [ - -123.653345, - 48.394315 - ], - [ - -123.652159, - 48.394159 - ], - [ - -123.651787, - 48.394081 - ], - [ - -123.651285, - 48.393819 - ], - [ - -123.651085, - 48.393623 - ], - [ - -123.650925, - 48.393371 - ], - [ - -123.650883, - 48.392867 - ], - [ - -123.651008, - 48.392311 - ], - [ - -123.651249, - 48.391772 - ], - [ - -123.651295, - 48.391474 - ], - [ - -123.651214, - 48.390354 - ], - [ - -123.651117, - 48.390064 - ], - [ - -123.650962, - 48.389812 - ], - [ - -123.650802, - 48.389639 - ], - [ - -123.64892, - 48.38855 - ], - [ - -123.648607, - 48.388327 - ], - [ - -123.646862, - 48.387087 - ], - [ - -123.646517, - 48.386897 - ], - [ - -123.646054, - 48.386765 - ], - [ - -123.645705, - 48.38673 - ], - [ - -123.645386, - 48.38674 - ], - [ - -123.645078, - 48.386793 - ], - [ - -123.644763, - 48.38689 - ], - [ - -123.644511, - 48.387027 - ], - [ - -123.644281, - 48.387193 - ], - [ - -123.644103, - 48.387423 - ], - [ - -123.643989, - 48.387639 - ], - [ - -123.643822, - 48.388199 - ], - [ - -123.643695, - 48.38845 - ], - [ - -123.643613, - 48.388564 - ], - [ - -123.643547, - 48.388655 - ], - [ - -123.643346, - 48.38883 - ], - [ - -123.642857, - 48.389105 - ], - [ - -123.642185, - 48.389482 - ], - [ - -123.642132, - 48.389512 - ], - [ - -123.641887, - 48.389732 - ], - [ - -123.641713, - 48.38994 - ], - [ - -123.641374, - 48.390537 - ], - [ - -123.641199, - 48.390742 - ], - [ - -123.640964, - 48.390935 - ], - [ - -123.640691, - 48.391077 - ], - [ - -123.640365, - 48.39119 - ], - [ - -123.639882, - 48.391276 - ], - [ - -123.638548, - 48.391383 - ], - [ - -123.638053, - 48.391493 - ], - [ - -123.637521, - 48.391754 - ], - [ - -123.637318, - 48.391949 - ], - [ - -123.637216, - 48.392058 - ], - [ - -123.637166, - 48.39213 - ], - [ - -123.637012, - 48.392353 - ], - [ - -123.636687, - 48.39314 - ], - [ - -123.636631, - 48.393252 - ], - [ - -123.6366, - 48.393314 - ], - [ - -123.636483, - 48.393458 - ], - [ - -123.636356, - 48.393598 - ], - [ - -123.636217, - 48.393754 - ], - [ - -123.636044, - 48.393947 - ], - [ - -123.635618, - 48.394255 - ], - [ - -123.635396, - 48.394381 - ], - [ - -123.635043, - 48.394558 - ], - [ - -123.634687, - 48.394793 - ], - [ - -123.634661, - 48.394684 - ], - [ - -123.634146, - 48.39479 - ], - [ - -123.633893, - 48.394828 - ], - [ - -123.63332, - 48.394853 - ], - [ - -123.632864, - 48.39484 - ], - [ - -123.632522, - 48.394792 - ], - [ - -123.632061, - 48.394722 - ], - [ - -123.631014, - 48.394488 - ], - [ - -123.630062, - 48.394385 - ], - [ - -123.629554, - 48.394428 - ], - [ - -123.629234, - 48.394488 - ], - [ - -123.629002, - 48.39456 - ], - [ - -123.628793, - 48.39462 - ], - [ - -123.628667, - 48.394656 - ], - [ - -123.628356, - 48.394793 - ], - [ - -123.628127, - 48.394932 - ], - [ - -123.627636, - 48.395279 - ], - [ - -123.627494, - 48.395368 - ], - [ - -123.627301, - 48.395492 - ], - [ - -123.626893, - 48.395741 - ], - [ - -123.626451, - 48.395937 - ], - [ - -123.625894, - 48.396088 - ], - [ - -123.625519, - 48.396157 - ], - [ - -123.625112, - 48.396202 - ], - [ - -123.624767, - 48.396229 - ], - [ - -123.624101, - 48.396206 - ], - [ - -123.623398, - 48.396185 - ], - [ - -123.622948, - 48.396207 - ], - [ - -123.622643, - 48.396267 - ], - [ - -123.622362, - 48.396338 - ], - [ - -123.622012, - 48.396452 - ], - [ - -123.621695, - 48.39658 - ], - [ - -123.621123, - 48.396876 - ], - [ - -123.620656, - 48.397125 - ], - [ - -123.620195, - 48.397293 - ], - [ - -123.619873, - 48.397381 - ], - [ - -123.619728, - 48.397418 - ], - [ - -123.619679, - 48.397431 - ], - [ - -123.61796, - 48.397755 - ], - [ - -123.617648, - 48.397807 - ], - [ - -123.61752, - 48.397839 - ], - [ - -123.617262, - 48.39788 - ], - [ - -123.616971, - 48.397934 - ], - [ - -123.616765, - 48.397983 - ], - [ - -123.616541, - 48.398009 - ], - [ - -123.61616, - 48.398051 - ], - [ - -123.61402, - 48.397994 - ], - [ - -123.613678, - 48.397968 - ], - [ - -123.613387, - 48.397899 - ], - [ - -123.613241, - 48.397851 - ], - [ - -123.613034, - 48.397784 - ], - [ - -123.611976, - 48.397438 - ], - [ - -123.611667, - 48.397386 - ], - [ - -123.610707, - 48.397305 - ], - [ - -123.61031, - 48.397227 - ], - [ - -123.609871, - 48.397039 - ], - [ - -123.609334, - 48.39667 - ], - [ - -123.608667, - 48.395995 - ], - [ - -123.608589, - 48.395886 - ], - [ - -123.608182, - 48.395317 - ], - [ - -123.607896, - 48.395058 - ], - [ - -123.607417, - 48.394803 - ], - [ - -123.606906, - 48.394661 - ], - [ - -123.606599, - 48.394575 - ], - [ - -123.60648, - 48.394541 - ], - [ - -123.606439, - 48.394519 - ], - [ - -123.606198, - 48.394386 - ], - [ - -123.605828, - 48.394091 - ], - [ - -123.604636, - 48.393282 - ], - [ - -123.604274, - 48.393213 - ], - [ - -123.603171, - 48.393109 - ], - [ - -123.601481, - 48.392996 - ], - [ - -123.599991, - 48.392857 - ], - [ - -123.599616, - 48.392821 - ], - [ - -123.598862, - 48.392847 - ], - [ - -123.598786, - 48.392849 - ] - ] - }, - "roads": [ - { - "name": "Highway 14", - "from": "Ludlow Rd", - "to": "Kangaroo Rd", - "direction": "BOTH" - } - ], - "areas": [ - { - "url": "http://www.geonames.org/8630140", - "name": "Vancouver Island District", - "id": "drivebc.ca/2" - } - ] - }, - { - "jurisdiction_url": "https://api.open511.gov.bc.ca/jurisdiction", - "url": "https://api.open511.gov.bc.ca/events/drivebc.ca/DBC-53145", - "id": "drivebc.ca/DBC-53145", - "headline": "CONSTRUCTION", - "status": "ACTIVE", - "created": "2023-06-08T10:43:05-07:00", - "updated": "2023-06-25T14:42:51-07:00", - "description": "Highway 14, in both directions. Tree pruning between Sombrio Beach Trail and Petrel Dr for 13.0 km (2 to 15 km west of Jordan River). Until Thu Jul 27. From 9:00 AM to 3:00 PM PDT daily. Single lane alternating traffic. Watch for traffic control. Expect delays. Last updated Sun Jun 25 at 2:42 PM PDT. (DBC-53145)", - "+ivr_message": "Highway 14, in both directions. Tree pruning between Sombrio Beach Trail and Petrel Dr for 13.0 km (2 to 15 km west of Jordan River). Until Thursday, July 27. From 9:00 AM to 3:00 PM daily. Single lane alternating traffic. Watch for traffic control. Expect delays. Last updated Sunday, June 25 at 2:42 PM.", - "+linear_reference_km": 37.49, - "schedule": { - "recurring_schedules": [ - { - "days": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7 - ], - "start_date": "2023-06-05", - "daily_start_time": "09:00", - "end_date": "2023-07-28", - "daily_end_time": "15:00" - } - ] - }, - "event_type": "CONSTRUCTION", - "event_subtypes": [ - "ROAD_MAINTENANCE" - ], - "severity": "MINOR", - "geography": { - "type": "LineString", - "coordinates": [ - [ - -124.237149, - 48.475724 - ], - [ - -124.236641, - 48.475618 - ], - [ - -124.235573, - 48.475217 - ], - [ - -124.235082, - 48.475071 - ], - [ - -124.234378, - 48.47494 - ], - [ - -124.23332, - 48.474882 - ], - [ - -124.231065, - 48.47454 - ], - [ - -124.229646, - 48.474223 - ], - [ - -124.229585, - 48.474209 - ], - [ - -124.229066, - 48.474016 - ], - [ - -124.22854, - 48.473744 - ], - [ - -124.227762, - 48.473157 - ], - [ - -124.2273, - 48.472876 - ], - [ - -124.225425, - 48.472027 - ], - [ - -124.224898, - 48.471871 - ], - [ - -124.223318, - 48.471583 - ], - [ - -124.221818, - 48.471192 - ], - [ - -124.221305, - 48.471094 - ], - [ - -124.220266, - 48.471013 - ], - [ - -124.219566, - 48.470839 - ], - [ - -124.21733, - 48.470107 - ], - [ - -124.217015, - 48.469933 - ], - [ - -124.216845, - 48.469783 - ], - [ - -124.216724, - 48.469599 - ], - [ - -124.216242, - 48.467989 - ], - [ - -124.216118, - 48.467702 - ], - [ - -124.215934, - 48.467445 - ], - [ - -124.215687, - 48.467196 - ], - [ - -124.215399, - 48.466972 - ], - [ - -124.214992, - 48.466783 - ], - [ - -124.214432, - 48.466581 - ], - [ - -124.212818, - 48.466133 - ], - [ - -124.2113, - 48.465618 - ], - [ - -124.209975, - 48.465035 - ], - [ - -124.209652, - 48.46495 - ], - [ - -124.209258, - 48.464894 - ], - [ - -124.207948, - 48.46482 - ], - [ - -124.205899, - 48.464909 - ], - [ - -124.205416, - 48.464838 - ], - [ - -124.204666, - 48.464618 - ], - [ - -124.204391, - 48.464564 - ], - [ - -124.204028, - 48.464531 - ], - [ - -124.202401, - 48.464607 - ], - [ - -124.202078, - 48.464584 - ], - [ - -124.201021, - 48.464509 - ], - [ - -124.199795, - 48.464504 - ], - [ - -124.199012, - 48.46438 - ], - [ - -124.197219, - 48.463874 - ], - [ - -124.196518, - 48.463763 - ], - [ - -124.195717, - 48.463729 - ], - [ - -124.192614, - 48.463498 - ], - [ - -124.191453, - 48.46349 - ], - [ - -124.188884, - 48.463182 - ], - [ - -124.187798, - 48.463168 - ], - [ - -124.187097, - 48.463055 - ], - [ - -124.18609, - 48.462986 - ], - [ - -124.185154, - 48.462792 - ], - [ - -124.18327, - 48.462401 - ], - [ - -124.182404, - 48.462222 - ], - [ - -124.179955, - 48.461406 - ], - [ - -124.179354, - 48.461229 - ], - [ - -124.178683, - 48.461069 - ], - [ - -124.177996, - 48.460965 - ], - [ - -124.177526, - 48.460965 - ], - [ - -124.176414, - 48.461159 - ], - [ - -124.175517, - 48.461147 - ], - [ - -124.174825, - 48.46107 - ], - [ - -124.174082, - 48.460921 - ], - [ - -124.171861, - 48.460224 - ], - [ - -124.171541, - 48.460061 - ], - [ - -124.171158, - 48.459857 - ], - [ - -124.170767, - 48.459648 - ], - [ - -124.169984, - 48.45924 - ], - [ - -124.169565, - 48.458976 - ], - [ - -124.169101, - 48.458527 - ], - [ - -124.168925, - 48.458357 - ], - [ - -124.168517, - 48.45808 - ], - [ - -124.168125, - 48.457888 - ], - [ - -124.167756, - 48.457762 - ], - [ - -124.167164, - 48.457607 - ], - [ - -124.166429, - 48.457438 - ], - [ - -124.166141, - 48.457371 - ], - [ - -124.159768, - 48.455921 - ], - [ - -124.158562, - 48.455403 - ], - [ - -124.157247, - 48.454945 - ], - [ - -124.156375, - 48.454457 - ], - [ - -124.156066, - 48.454341 - ], - [ - -124.155598, - 48.454251 - ], - [ - -124.154486, - 48.454205 - ], - [ - -124.153784, - 48.454044 - ], - [ - -124.153261, - 48.453852 - ], - [ - -124.152835, - 48.453634 - ], - [ - -124.152527, - 48.453397 - ], - [ - -124.1518, - 48.452601 - ], - [ - -124.151537, - 48.452375 - ], - [ - -124.151245, - 48.452177 - ], - [ - -124.150917, - 48.452011 - ], - [ - -124.150475, - 48.451865 - ], - [ - -124.149815, - 48.451728 - ], - [ - -124.148857, - 48.451592 - ], - [ - -124.148476, - 48.451454 - ], - [ - -124.147599, - 48.450928 - ], - [ - -124.147215, - 48.45075 - ], - [ - -124.146821, - 48.45063 - ], - [ - -124.146466, - 48.450572 - ], - [ - -124.14477, - 48.450468 - ], - [ - -124.1444, - 48.450425 - ], - [ - -124.144098, - 48.450319 - ], - [ - -124.14351, - 48.449914 - ], - [ - -124.143137, - 48.449774 - ], - [ - -124.142651, - 48.449651 - ], - [ - -124.140427, - 48.449245 - ], - [ - -124.139495, - 48.448911 - ], - [ - -124.139132, - 48.44885 - ], - [ - -124.138315, - 48.448793 - ], - [ - -124.137664, - 48.4487 - ], - [ - -124.13705, - 48.448539 - ], - [ - -124.136628, - 48.448364 - ], - [ - -124.135649, - 48.447789 - ], - [ - -124.135399, - 48.44769 - ], - [ - -124.135093, - 48.447626 - ], - [ - -124.133507, - 48.447464 - ], - [ - -124.132145, - 48.447172 - ], - [ - -124.131, - 48.446828 - ], - [ - -124.130487, - 48.446699 - ], - [ - -124.129994, - 48.446622 - ], - [ - -124.128926, - 48.446595 - ], - [ - -124.128199, - 48.446576 - ], - [ - -124.128109, - 48.446574 - ], - [ - -124.127811, - 48.446516 - ], - [ - -124.127353, - 48.446427 - ], - [ - -124.126267, - 48.445993 - ], - [ - -124.12583, - 48.445852 - ], - [ - -124.125375, - 48.445757 - ], - [ - -124.124953, - 48.44572 - ], - [ - -124.123046, - 48.445742 - ], - [ - -124.122518, - 48.445668 - ], - [ - -124.122055, - 48.445547 - ], - [ - -124.120734, - 48.445059 - ], - [ - -124.12017, - 48.444925 - ], - [ - -124.119453, - 48.44481 - ], - [ - -124.118344, - 48.44471 - ], - [ - -124.117888, - 48.444626 - ], - [ - -124.116743, - 48.444303 - ], - [ - -124.115579, - 48.443933 - ], - [ - -124.11528, - 48.443874 - ], - [ - -124.114991, - 48.443864 - ], - [ - -124.114865, - 48.443876 - ], - [ - -124.114598, - 48.443902 - ], - [ - -124.11424, - 48.443896 - ], - [ - -124.113849, - 48.443826 - ], - [ - -124.112126, - 48.4432 - ], - [ - -124.111676, - 48.442996 - ], - [ - -124.111106, - 48.442644 - ], - [ - -124.110563, - 48.442314 - ], - [ - -124.109895, - 48.441991 - ], - [ - -124.109548, - 48.441793 - ], - [ - -124.108925, - 48.441279 - ], - [ - -124.108304, - 48.44084 - ], - [ - -124.107885, - 48.440625 - ], - [ - -124.106881, - 48.440241 - ], - [ - -124.105848, - 48.439773 - ], - [ - -124.105041, - 48.439334 - ], - [ - -124.104661, - 48.43917 - ], - [ - -124.104098, - 48.439004 - ], - [ - -124.103331, - 48.438851 - ], - [ - -124.102656, - 48.438773 - ], - [ - -124.101724, - 48.438736 - ], - [ - -124.100935, - 48.43872 - ], - [ - -124.099205, - 48.438617 - ], - [ - -124.098139, - 48.438636 - ], - [ - -124.096954, - 48.438748 - ], - [ - -124.095236, - 48.439023 - ], - [ - -124.094303, - 48.439138 - ], - [ - -124.093573, - 48.439167 - ], - [ - -124.091418, - 48.438943 - ], - [ - -124.090815, - 48.438925 - ], - [ - -124.089731, - 48.439033 - ], - [ - -124.089087, - 48.439014 - ], - [ - -124.088682, - 48.439006 - ], - [ - -124.088317, - 48.438998 - ], - [ - -124.085919, - 48.43895 - ], - [ - -124.08307, - 48.439096 - ], - [ - -124.079387, - 48.438983 - ], - [ - -124.079319, - 48.438975 - ], - [ - -124.078921, - 48.43893 - ] - ] - }, - "roads": [ - { - "name": "Highway 14", - "from": "Sombrio Beach Trail", - "to": "Petrel Dr", - "direction": "BOTH" - } - ], - "areas": [ - { - "url": "http://www.geonames.org/8630140", - "name": "Vancouver Island District", - "id": "drivebc.ca/2" - } - ] - }, - { - "jurisdiction_url": "https://api.open511.gov.bc.ca/jurisdiction", - "url": "https://api.open511.gov.bc.ca/events/drivebc.ca/DBC-52791", - "id": "drivebc.ca/DBC-52791", - "headline": "CONSTRUCTION", - "status": "ACTIVE", - "created": "2023-05-30T12:38:15-07:00", - "updated": "2023-06-25T14:42:57-07:00", - "description": "Highway 14, in both directions. Tree pruning between Sombrio Beach Trail and Petrel Dr for 11.3 km (2 to 13 km west of Jordan River). Until Thu Jul 27 at 3:00 PM PDT. Lane Closure. Watch for traffic control. Last updated Sun Jun 25 at 2:42 PM PDT. (DBC-52791)", - "+ivr_message": "Highway 14, in both directions. Tree pruning between Sombrio Beach Trail and Petrel Dr for 11.3 km (2 to 13 km west of Jordan River). Until Thursday, July 27 at 3:00 PM. Lane Closure. Watch for traffic control. Last updated Sunday, June 25 at 2:42 PM.", - "+linear_reference_km": 37.33, - "schedule": { - "intervals": [ - "2023-05-24T16:00/2023-07-27T22:00" - ] - }, - "event_type": "CONSTRUCTION", - "event_subtypes": [ - "ROAD_MAINTENANCE" - ], - "severity": "MINOR", - "geography": { - "type": "LineString", - "coordinates": [ - [ - -124.217557, - 48.470181 - ], - [ - -124.21733, - 48.470107 - ], - [ - -124.217015, - 48.469933 - ], - [ - -124.216845, - 48.469783 - ], - [ - -124.216724, - 48.469599 - ], - [ - -124.216242, - 48.467989 - ], - [ - -124.216118, - 48.467702 - ], - [ - -124.215934, - 48.467445 - ], - [ - -124.215687, - 48.467196 - ], - [ - -124.215399, - 48.466972 - ], - [ - -124.214992, - 48.466783 - ], - [ - -124.214432, - 48.466581 - ], - [ - -124.212818, - 48.466133 - ], - [ - -124.2113, - 48.465618 - ], - [ - -124.209975, - 48.465035 - ], - [ - -124.209652, - 48.46495 - ], - [ - -124.209258, - 48.464894 - ], - [ - -124.207948, - 48.46482 - ], - [ - -124.205899, - 48.464909 - ], - [ - -124.205416, - 48.464838 - ], - [ - -124.204666, - 48.464618 - ], - [ - -124.204391, - 48.464564 - ], - [ - -124.204028, - 48.464531 - ], - [ - -124.202401, - 48.464607 - ], - [ - -124.202078, - 48.464584 - ], - [ - -124.201021, - 48.464509 - ], - [ - -124.199795, - 48.464504 - ], - [ - -124.199012, - 48.46438 - ], - [ - -124.197219, - 48.463874 - ], - [ - -124.196518, - 48.463763 - ], - [ - -124.195717, - 48.463729 - ], - [ - -124.192614, - 48.463498 - ], - [ - -124.191453, - 48.46349 - ], - [ - -124.188884, - 48.463182 - ], - [ - -124.187798, - 48.463168 - ], - [ - -124.187097, - 48.463055 - ], - [ - -124.18609, - 48.462986 - ], - [ - -124.185154, - 48.462792 - ], - [ - -124.18327, - 48.462401 - ], - [ - -124.182404, - 48.462222 - ], - [ - -124.179955, - 48.461406 - ], - [ - -124.179354, - 48.461229 - ], - [ - -124.178683, - 48.461069 - ], - [ - -124.177996, - 48.460965 - ], - [ - -124.177526, - 48.460965 - ], - [ - -124.176414, - 48.461159 - ], - [ - -124.175517, - 48.461147 - ], - [ - -124.174825, - 48.46107 - ], - [ - -124.174082, - 48.460921 - ], - [ - -124.171861, - 48.460224 - ], - [ - -124.171541, - 48.460061 - ], - [ - -124.171158, - 48.459857 - ], - [ - -124.170767, - 48.459648 - ], - [ - -124.169984, - 48.45924 - ], - [ - -124.169565, - 48.458976 - ], - [ - -124.169101, - 48.458527 - ], - [ - -124.168925, - 48.458357 - ], - [ - -124.168517, - 48.45808 - ], - [ - -124.168125, - 48.457888 - ], - [ - -124.167756, - 48.457762 - ], - [ - -124.167164, - 48.457607 - ], - [ - -124.166429, - 48.457438 - ], - [ - -124.166141, - 48.457371 - ], - [ - -124.159768, - 48.455921 - ], - [ - -124.158562, - 48.455403 - ], - [ - -124.157247, - 48.454945 - ], - [ - -124.156375, - 48.454457 - ], - [ - -124.156066, - 48.454341 - ], - [ - -124.155598, - 48.454251 - ], - [ - -124.154486, - 48.454205 - ], - [ - -124.153784, - 48.454044 - ], - [ - -124.153261, - 48.453852 - ], - [ - -124.152835, - 48.453634 - ], - [ - -124.152527, - 48.453397 - ], - [ - -124.1518, - 48.452601 - ], - [ - -124.151537, - 48.452375 - ], - [ - -124.151245, - 48.452177 - ], - [ - -124.150917, - 48.452011 - ], - [ - -124.150475, - 48.451865 - ], - [ - -124.149815, - 48.451728 - ], - [ - -124.148857, - 48.451592 - ], - [ - -124.148476, - 48.451454 - ], - [ - -124.147599, - 48.450928 - ], - [ - -124.147215, - 48.45075 - ], - [ - -124.146821, - 48.45063 - ], - [ - -124.146466, - 48.450572 - ], - [ - -124.14477, - 48.450468 - ], - [ - -124.1444, - 48.450425 - ], - [ - -124.144098, - 48.450319 - ], - [ - -124.14351, - 48.449914 - ], - [ - -124.143137, - 48.449774 - ], - [ - -124.142651, - 48.449651 - ], - [ - -124.140427, - 48.449245 - ], - [ - -124.139495, - 48.448911 - ], - [ - -124.139132, - 48.44885 - ], - [ - -124.138315, - 48.448793 - ], - [ - -124.137664, - 48.4487 - ], - [ - -124.13705, - 48.448539 - ], - [ - -124.136628, - 48.448364 - ], - [ - -124.135649, - 48.447789 - ], - [ - -124.135399, - 48.44769 - ], - [ - -124.135093, - 48.447626 - ], - [ - -124.133507, - 48.447464 - ], - [ - -124.132145, - 48.447172 - ], - [ - -124.131, - 48.446828 - ], - [ - -124.130487, - 48.446699 - ], - [ - -124.129994, - 48.446622 - ], - [ - -124.128926, - 48.446595 - ], - [ - -124.128199, - 48.446576 - ], - [ - -124.128109, - 48.446574 - ], - [ - -124.127811, - 48.446516 - ], - [ - -124.127353, - 48.446427 - ], - [ - -124.126267, - 48.445993 - ], - [ - -124.12583, - 48.445852 - ], - [ - -124.125375, - 48.445757 - ], - [ - -124.124953, - 48.44572 - ], - [ - -124.123046, - 48.445742 - ], - [ - -124.122518, - 48.445668 - ], - [ - -124.122055, - 48.445547 - ], - [ - -124.120734, - 48.445059 - ], - [ - -124.12017, - 48.444925 - ], - [ - -124.119453, - 48.44481 - ], - [ - -124.118344, - 48.44471 - ], - [ - -124.117888, - 48.444626 - ], - [ - -124.116743, - 48.444303 - ], - [ - -124.115579, - 48.443933 - ], - [ - -124.11528, - 48.443874 - ], - [ - -124.114991, - 48.443864 - ], - [ - -124.114865, - 48.443876 - ], - [ - -124.114598, - 48.443902 - ], - [ - -124.11424, - 48.443896 - ], - [ - -124.113849, - 48.443826 - ], - [ - -124.112126, - 48.4432 - ], - [ - -124.111676, - 48.442996 - ], - [ - -124.111106, - 48.442644 - ], - [ - -124.110563, - 48.442314 - ], - [ - -124.109895, - 48.441991 - ], - [ - -124.109548, - 48.441793 - ], - [ - -124.108925, - 48.441279 - ], - [ - -124.108304, - 48.44084 - ], - [ - -124.107885, - 48.440625 - ], - [ - -124.106881, - 48.440241 - ], - [ - -124.105848, - 48.439773 - ], - [ - -124.105041, - 48.439334 - ], - [ - -124.104661, - 48.43917 - ], - [ - -124.104098, - 48.439004 - ], - [ - -124.103331, - 48.438851 - ], - [ - -124.102656, - 48.438773 - ], - [ - -124.101724, - 48.438736 - ], - [ - -124.100935, - 48.43872 - ], - [ - -124.099205, - 48.438617 - ], - [ - -124.098139, - 48.438636 - ], - [ - -124.096954, - 48.438748 - ], - [ - -124.095236, - 48.439023 - ], - [ - -124.094303, - 48.439138 - ], - [ - -124.093573, - 48.439167 - ], - [ - -124.091418, - 48.438943 - ], - [ - -124.090815, - 48.438925 - ], - [ - -124.089731, - 48.439033 - ], - [ - -124.089087, - 48.439014 - ], - [ - -124.088682, - 48.439006 - ], - [ - -124.088317, - 48.438998 - ], - [ - -124.085919, - 48.43895 - ], - [ - -124.08307, - 48.439096 - ], - [ - -124.080086, - 48.439004 - ] - ] - }, - "roads": [ - { - "name": "Highway 14", - "from": "Sombrio Beach Trail", - "to": "Petrel Dr", - "direction": "BOTH" - } - ], - "areas": [ - { - "url": "http://www.geonames.org/8630140", - "name": "Vancouver Island District", - "id": "drivebc.ca/2" - } - ] - }, - { - "jurisdiction_url": "https://api.open511.gov.bc.ca/jurisdiction", - "url": "https://api.open511.gov.bc.ca/events/drivebc.ca/DBC-52446", - "id": "drivebc.ca/DBC-52446", - "headline": "CONSTRUCTION", - "status": "ACTIVE", - "created": "2023-05-19T14:29:20-07:00", - "updated": "2023-06-29T10:14:55-07:00", - "description": "Highway 3. Road maintenance work between Bromley Pl and Frontage Rd for 0.6 km (Princeton). Until Sat Jul 22 at 7:00 AM PDT. Single lane alternating traffic. Next update time Fri Jul 21 at 1:00 PM PDT. Last updated Thu Jun 29 at 10:14 AM PDT. (DBC-52446)", - "+ivr_message": "Highway 3. Road maintenance work between Bromley Pl and Frontage Rd for 0.6 km (Princeton). Until Saturday, July 22 at 7:00 AM. Single lane alternating traffic. Next update time Friday, July 21 at 1:00 PM. Last updated Thursday, June 29 at 10:14 AM.", - "+linear_reference_km": 131.14, - "schedule": { - "intervals": [ - "2023-05-23T14:00/2023-07-22T14:00" - ] - }, - "event_type": "CONSTRUCTION", - "event_subtypes": [ - "ROAD_MAINTENANCE" - ], - "severity": "MAJOR", - "geography": { - "type": "LineString", - "coordinates": [ - [ - -120.528796, - 49.446318 - ], - [ - -120.528342, - 49.447589 - ], - [ - -120.527977, - 49.448609 - ], - [ - -120.527803, - 49.449096 - ], - [ - -120.527031, - 49.450689 - ], - [ - -120.526853, - 49.451003 - ], - [ - -120.526427, - 49.451752 - ] - ] - }, - "roads": [ - { - "name": "Highway 3", - "from": "Bromley Pl", - "to": "Frontage Rd", - "direction": "NONE" - } - ], - "areas": [ - { - "url": "http://www.geonames.org/8630138", - "name": "Okanagan-Shuswap District", - "id": "drivebc.ca/5" - } - ] - } - ], - "pagination": { - "offset": "0" - }, - "meta": { - "url": "/events", - "up_url": "", - "version": "v1" - } -} diff --git a/src/frontend/src/Components/__tests__/test_data/webcam_results_five_set.json b/src/frontend/src/Components/__tests__/test_data/webcam_results_five_set.json deleted file mode 100644 index 0c4b7c503..000000000 --- a/src/frontend/src/Components/__tests__/test_data/webcam_results_five_set.json +++ /dev/null @@ -1,152 +0,0 @@ -[ - { - "id": 6, - "links": { - "imageDisplay": "https://images.drivebc.ca/bchighwaycam/pub/cameras/6.jpg", - "imageThumbnail": "https://images.drivebc.ca/bchighwaycam/pub/cameras/tn/6.jpg", - "currentImage": "https://images.drivebc.ca/webcam/imageUpdate.php?cam=6", - "replayTheDay": "https://images.drivebc.ca/ReplayTheDay/player.html?cam=6" - }, - "name": "Smithers - N", - "caption": "Hwy 16 in Smithers at Main Street, looking north.", - "region": 0, - "region_name": "Northern", - "highway": "16", - "highway_description": "", - "highway_group": 2, - "highway_cam_order": 25, - "location": { "type": "Point", "coordinates": [-127.1667312, 54.782104] }, - "orientation": "N", - "elevation": 497, - "is_on": true, - "should_appear": true, - "is_new": false, - "is_on_demand": false, - "marked_stale": false, - "marked_delayed": false, - "last_update_attempt": "2023-06-13T17:03:25-07:00", - "last_update_modified": "2023-06-13T17:03:25-07:00", - "update_period_mean": 901, - "update_period_stddev": 19 - }, - { - "id": 7, - "links": { - "imageDisplay": "https://images.drivebc.ca/bchighwaycam/pub/cameras/7.jpg", - "imageThumbnail": "https://images.drivebc.ca/bchighwaycam/pub/cameras/tn/7.jpg", - "currentImage": "https://images.drivebc.ca/webcam/imageUpdate.php?cam=7", - "replayTheDay": "https://images.drivebc.ca/ReplayTheDay/player.html?cam=7" - }, - "name": "Cole Road - E", - "caption": "Hwy 1 at Cole Road Rest Area, looking east.", - "region": 2, - "region_name": "Lower Mainland", - "highway": "1", - "highway_description": "Fraser Valley", - "highway_group": 2, - "highway_cam_order": 32, - "location": { "type": "Point", "coordinates": [-122.177, 49.0575] }, - "orientation": "E", - "elevation": 12, - "is_on": true, - "should_appear": true, - "is_new": false, - "is_on_demand": false, - "marked_stale": false, - "marked_delayed": false, - "last_update_attempt": "2023-06-13T17:00:59-07:00", - "last_update_modified": "2023-06-13T17:00:59-07:00", - "update_period_mean": 914, - "update_period_stddev": 27 - }, - { - "id": 8, - "links": { - "imageDisplay": "https://images.drivebc.ca/bchighwaycam/pub/cameras/8.jpg", - "imageThumbnail": "https://images.drivebc.ca/bchighwaycam/pub/cameras/tn/8.jpg", - "currentImage": "https://images.drivebc.ca/webcam/imageUpdate.php?cam=8", - "replayTheDay": "https://images.drivebc.ca/ReplayTheDay/player.html?cam=8" - }, - "name": "Malahat Drive - N", - "caption": "Hwy 1 at South Shawnigan Lake Road, looking north.", - "region": 3, - "region_name": "Vancouver Island", - "highway": "1", - "highway_description": "Vancouver Island", - "highway_group": 0, - "highway_cam_order": 29, - "location": { "type": "Point", "coordinates": [-123.569743, 48.561231] }, - "orientation": "N", - "elevation": 327, - "is_on": true, - "should_appear": true, - "is_new": false, - "is_on_demand": false, - "marked_stale": false, - "marked_delayed": false, - "last_update_attempt": "2023-06-13T16:57:25-07:00", - "last_update_modified": "2023-06-13T16:57:25-07:00", - "update_period_mean": 689, - "update_period_stddev": 60 - }, - { - "id": 9, - "links": { - "imageDisplay": "https://images.drivebc.ca/bchighwaycam/pub/cameras/9.jpg", - "imageThumbnail": "https://images.drivebc.ca/bchighwaycam/pub/cameras/tn/9.jpg", - "currentImage": "https://images.drivebc.ca/webcam/imageUpdate.php?cam=9", - "replayTheDay": "https://images.drivebc.ca/ReplayTheDay/player.html?cam=9" - }, - "name": "Nanaimo Parkway", - "caption": "Hwy 19 at College Drive, looking north.", - "region": 3, - "region_name": "Vancouver Island", - "highway": "19", - "highway_description": "", - "highway_group": 5, - "highway_cam_order": 42, - "location": { "type": "Point", "coordinates": [-123.97196, 49.153005] }, - "orientation": "N", - "elevation": 130, - "is_on": true, - "should_appear": true, - "is_new": false, - "is_on_demand": false, - "marked_stale": false, - "marked_delayed": false, - "last_update_attempt": "2023-06-13T17:06:04-07:00", - "last_update_modified": "2023-06-13T17:06:04-07:00", - "update_period_mean": 120, - "update_period_stddev": 0 - }, - { - "id": 10, - "links": { - "imageDisplay": "https://images.drivebc.ca/bchighwaycam/pub/cameras/10.jpg", - "imageThumbnail": "https://images.drivebc.ca/bchighwaycam/pub/cameras/tn/10.jpg", - "currentImage": "https://images.drivebc.ca/webcam/imageUpdate.php?cam=10", - "replayTheDay": "https://images.drivebc.ca/ReplayTheDay/player.html?cam=10" - }, - "name": "South Taylor Hill - N", - "caption": "Hwy 97 at South Taylor Hill, 20 km south of Fort St John, looking north.", - "region": 0, - "region_name": "Northern", - "highway": "97", - "highway_description": "Northern Region", - "highway_group": 9, - "highway_cam_order": 16, - "location": { "type": "Point", "coordinates": [-120.642756, 56.09491] }, - "orientation": "N", - "elevation": 714, - "is_on": true, - "should_appear": true, - "is_new": false, - "is_on_demand": false, - "marked_stale": false, - "marked_delayed": false, - "last_update_attempt": "2023-06-13T16:58:04-07:00", - "last_update_modified": "2023-06-13T16:58:04-07:00", - "update_period_mean": 708, - "update_period_stddev": 37 - } -] diff --git a/src/frontend/src/Components/advisories/Advisories.js b/src/frontend/src/Components/advisories/Advisories.js index e251193c5..b3b6030de 100644 --- a/src/frontend/src/Components/advisories/Advisories.js +++ b/src/frontend/src/Components/advisories/Advisories.js @@ -1,9 +1,6 @@ // React import React from 'react'; -// Redux -import { updateAdvisories } from '../../slices/cmsSlice'; - // External Components import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { diff --git a/src/frontend/src/Components/advisories/AdvisoriesList.js b/src/frontend/src/Components/advisories/AdvisoriesList.js index a96f4da50..7349980b7 100644 --- a/src/frontend/src/Components/advisories/AdvisoriesList.js +++ b/src/frontend/src/Components/advisories/AdvisoriesList.js @@ -6,8 +6,8 @@ import { useNavigate } from 'react-router-dom'; // Components and functions import { stripRichText } from '../data/helper'; -import FriendlyTime from '../FriendlyTime'; -import trackEvent from '../TrackEvent.js'; +import FriendlyTime from '../shared/FriendlyTime'; +import trackEvent from '../shared/TrackEvent.js'; // Styling import './AdvisoriesList.scss'; diff --git a/src/frontend/src/Components/advisories/AdvisoriesOnMap.js b/src/frontend/src/Components/advisories/AdvisoriesOnMap.js index cb8e90bd4..a0b118170 100644 --- a/src/frontend/src/Components/advisories/AdvisoriesOnMap.js +++ b/src/frontend/src/Components/advisories/AdvisoriesOnMap.js @@ -3,7 +3,7 @@ import React, { useState } from 'react'; // Components and functions import AdvisoriesList from './AdvisoriesList'; -import trackEvent from '../TrackEvent'; +import trackEvent from '../shared/TrackEvent'; // Third party packages import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import { diff --git a/src/frontend/src/Components/bulletins/BulletinsList.js b/src/frontend/src/Components/bulletins/BulletinsList.js index a71b3331f..82c9d4aa2 100644 --- a/src/frontend/src/Components/bulletins/BulletinsList.js +++ b/src/frontend/src/Components/bulletins/BulletinsList.js @@ -4,8 +4,8 @@ import {useNavigate} from 'react-router-dom'; // Components and functions import { stripRichText } from '../data/helper'; -import FriendlyTime from '../FriendlyTime'; -import trackEvent from '../TrackEvent'; +import FriendlyTime from '../shared/FriendlyTime'; +import trackEvent from '../shared/TrackEvent'; // Styling import './BulletinsList.scss'; diff --git a/src/frontend/src/Components/cameras/CameraCard.js b/src/frontend/src/Components/cameras/CameraCard.js index 5c3707322..f7accd803 100644 --- a/src/frontend/src/Components/cameras/CameraCard.js +++ b/src/frontend/src/Components/cameras/CameraCard.js @@ -10,7 +10,7 @@ import { faMapMarkerAlt, faVideoSlash } from '@fortawesome/pro-solid-svg-icons'; import Card from 'react-bootstrap/Card'; import Button from 'react-bootstrap/Button'; -import FriendlyTime from '../FriendlyTime'; +import FriendlyTime from '../shared/FriendlyTime'; import { getCameraOrientation } from './helper.js'; @@ -59,7 +59,10 @@ export default function CameraCard(props) { } function handleViewOnMap() { - navigate('/', {state: camera}); + const refCamData = { ...camera }; + refCamData.type = 'camera'; + + navigate('/', { state: refCamData }); window.snowplow('trackSelfDescribingEvent', { schema: 'iglu:ca.bc.gov.drivebc/action/jsonschema/1-0-0', data: { @@ -90,7 +93,7 @@ export default function CameraCard(props) { colocated cameras icon {camera.camGroup.map((cam) => - + +
+ {openPanel && renderPanel(clickedFeature, isCamDetail)} +
+
+ +
+ {!isCamDetail && ( +
+ {smallScreen && ( + + )} + + +
+ )} + + {(!isCamDetail && smallScreen) && ( + + + + + + )} + + {(!isCamDetail && !smallScreen) && ( + + + + + + )} + +
+ +
+ +
+ +
+ + {isCamDetail && ( + + )} + + {isCamDetail && ( + + )} + + {isCamDetail && ( + + )} + + {showNetworkError && + + } + + {!showNetworkError && showServerError && + + } +
+ ); +} diff --git a/src/frontend/src/Components/Map.scss b/src/frontend/src/Components/map/Map.scss similarity index 99% rename from src/frontend/src/Components/Map.scss rename to src/frontend/src/Components/map/Map.scss index 2d5eee319..c0c17eb2f 100644 --- a/src/frontend/src/Components/Map.scss +++ b/src/frontend/src/Components/map/Map.scss @@ -1,4 +1,4 @@ -@import "../styles/variables.scss"; +@import "../../styles/variables.scss"; @import '~ol/ol.css'; // Z-index diff --git a/src/frontend/src/Components/map/MapWrapper.js b/src/frontend/src/Components/map/MapWrapper.js new file mode 100644 index 000000000..c064a05cb --- /dev/null +++ b/src/frontend/src/Components/map/MapWrapper.js @@ -0,0 +1,93 @@ +// React +import React, { useEffect, useState, useCallback } from 'react'; + +// Redux +import { memoize } from 'proxy-memoize'; +import { useSelector, useDispatch } from 'react-redux'; + +// Components and functions +import { NetworkError, ServerError } from '../data/helper'; +import * as dataLoaders from './dataLoaders' +import DriveBCMap from './Map'; + +export default function MapWrapper(props) { + // Redux + const dispatch = useDispatch(); + const { + feeds: { + cameras: { list: cameras, filteredList: filteredCameras, filterPoints: camFilterPoints }, + events: { list: events, filteredList: filteredEvents, filterPoints: eventFilterPoints }, + ferries: { list: ferries, filteredList: filteredFerries, filterPoints: ferryFilterPoints }, + weather: { list: currentWeather, filteredList: filteredCurrentWeathers, filterPoints: currentWeatherFilterPoints }, + regional: { list: regionalWeather, filteredList: filteredRegionalWeathers, filterPoints: regionalWeatherFilterPoints }, + restStops: { list: restStops, filteredList: filteredRestStops, filterPoints: restStopFilterPoints }, + }, + advisories: { list: advisories }, + routes: { selectedRoute }, + + } = useSelector( + useCallback( + memoize(state => ({ + feeds: { + cameras: state.feeds.cameras, + events: state.feeds.events, + ferries: state.feeds.ferries, + weather: state.feeds.weather, + regional: state.feeds.regional, + restStops: state.feeds.restStops, + }, + advisories: state.cms.advisories, + routes: state.routes, + })), + ), + ); + + // States + const [showNetworkError, setShowNetworkError] = useState(false); + const [showServerError, setShowServerError] = useState(false); + + // Error handling + const displayError = (error) => { + if (error instanceof ServerError) { + setShowServerError(true); + + } else if (error instanceof NetworkError) { + setShowNetworkError(true); + } + } + + useEffect(() => { + loadData(); + }, [selectedRoute]); + + // Function to load all data + const loadData = () => { + if (selectedRoute && selectedRoute.routeFound) { + // Clear and update data + dataLoaders.loadCameras(selectedRoute, cameras, filteredCameras, camFilterPoints, dispatch, displayError); + dataLoaders.loadEvents(selectedRoute, events, filteredEvents, eventFilterPoints, dispatch, displayError); + dataLoaders.loadFerries(selectedRoute, ferries, filteredFerries, ferryFilterPoints, dispatch, displayError); + dataLoaders.loadCurrentWeather(selectedRoute, currentWeather, filteredCurrentWeathers, currentWeatherFilterPoints, dispatch, displayError); + dataLoaders.loadRegionalWeather(selectedRoute, regionalWeather, filteredRegionalWeathers, regionalWeatherFilterPoints, dispatch, displayError); + dataLoaders.loadRestStops(selectedRoute, restStops, filteredRestStops, restStopFilterPoints, dispatch, displayError); + dataLoaders.loadAdvisories(advisories, dispatch, displayError); + + } else { + // Clear and update data + dataLoaders.loadCameras(null, cameras, filteredCameras, camFilterPoints, dispatch, displayError); + dataLoaders.loadEvents(null, events, filteredEvents, eventFilterPoints, dispatch, displayError); + dataLoaders.loadFerries(null, ferries, filteredFerries, ferryFilterPoints, dispatch, displayError); + dataLoaders.loadCurrentWeather(null, currentWeather, filteredCurrentWeathers, currentWeatherFilterPoints, dispatch, displayError); + dataLoaders.loadRegionalWeather(null, regionalWeather, filteredRegionalWeathers, regionalWeatherFilterPoints, dispatch, displayError); + dataLoaders.loadRestStops(null, restStops, filteredRestStops, restStopFilterPoints, dispatch, displayError); + dataLoaders.loadAdvisories(advisories, dispatch, displayError); + } + }; + + return ( + + ); +} diff --git a/src/frontend/src/Components/OpenSeason.js b/src/frontend/src/Components/map/OpenSeason.js similarity index 100% rename from src/frontend/src/Components/OpenSeason.js rename to src/frontend/src/Components/map/OpenSeason.js diff --git a/src/frontend/src/Components/RestStopTypeIcon.js b/src/frontend/src/Components/map/RestStopTypeIcon.js similarity index 73% rename from src/frontend/src/Components/RestStopTypeIcon.js rename to src/frontend/src/Components/map/RestStopTypeIcon.js index 496c4e089..0dfdced53 100644 --- a/src/frontend/src/Components/RestStopTypeIcon.js +++ b/src/frontend/src/Components/map/RestStopTypeIcon.js @@ -1,10 +1,10 @@ // React import React from 'react'; -import restStopIconActive from '../images/mapIcons/restarea-open-active.png'; -import restStopIconActiveClosed from '../images/mapIcons/restarea-closed-active.png'; -import restStopIconActiveTruck from '../images/mapIcons/restarea-truck-open-active.png'; -import restStopIconActiveTruckClosed from '../images/mapIcons/restarea-truck-closed-active.png'; -import { isRestStopClosed } from './data/restStops'; +import restStopIconActive from '../../images/mapIcons/restarea-open-active.png'; +import restStopIconActiveClosed from '../../images/mapIcons/restarea-closed-active.png'; +import restStopIconActiveTruck from '../../images/mapIcons/restarea-truck-open-active.png'; +import restStopIconActiveTruckClosed from '../../images/mapIcons/restarea-truck-closed-active.png'; +import { isRestStopClosed } from '../data/restStops'; export default function RestStopTypeIcon(props) { const { reststop } = props; @@ -16,7 +16,7 @@ export default function RestStopTypeIcon(props) { return } else { return - } + } } else { const isClosed = isRestStopClosed(reststop.properties); @@ -38,5 +38,5 @@ export default function RestStopTypeIcon(props) { } } - } -} \ No newline at end of file + } +} diff --git a/src/frontend/src/Components/WeatherIcon.js b/src/frontend/src/Components/map/WeatherIcon.js similarity index 100% rename from src/frontend/src/Components/WeatherIcon.js rename to src/frontend/src/Components/map/WeatherIcon.js diff --git a/src/frontend/src/Components/map/dataLoaders/advisories.js b/src/frontend/src/Components/map/dataLoaders/advisories.js new file mode 100644 index 000000000..b8a52f80e --- /dev/null +++ b/src/frontend/src/Components/map/dataLoaders/advisories.js @@ -0,0 +1,14 @@ +import { getAdvisories } from '../../data/advisories'; +import * as slices from '../../../slices'; + +export const loadAdvisories = async (advisories, dispatch, displayError) => { + // Fetch data if it doesn't already exist + if (!advisories) { + dispatch( + slices.updateAdvisories({ + list: await getAdvisories().catch((error) => displayError(error)), + timeStamp: new Date().getTime(), + }), + ); + } +}; diff --git a/src/frontend/src/Components/map/dataLoaders/cameras.js b/src/frontend/src/Components/map/dataLoaders/cameras.js new file mode 100644 index 000000000..4b4912a0a --- /dev/null +++ b/src/frontend/src/Components/map/dataLoaders/cameras.js @@ -0,0 +1,25 @@ +import { getCameras } from '../../data/webcams'; +import * as helpers from '../helpers'; +import * as slices from '../../../slices'; + +export const loadCameras = async (route, cameras, filteredCameras, camFilterPoints, dispatch, displayError) => { + const routePoints = route ? route.points : null; + + // Load if filtered cams don't exist or route doesn't match + if (!filteredCameras || !helpers.compareRoutePoints(routePoints, camFilterPoints)) { + // Fetch data if it doesn't already exist + const camData = cameras ? cameras : await getCameras().catch((error) => displayError(error)); + + // Filter data by route + const filteredCamData = route ? helpers.filterByRoute(camData, route, null, true) : camData; + + dispatch( + slices.updateCameras({ + list: camData, + filteredList: filteredCamData, + filterPoints: route ? route.points : null, + timeStamp: new Date().getTime() + }) + ); + } +}; diff --git a/src/frontend/src/Components/map/dataLoaders/currentWeathers.js b/src/frontend/src/Components/map/dataLoaders/currentWeathers.js new file mode 100644 index 000000000..fe4b2f288 --- /dev/null +++ b/src/frontend/src/Components/map/dataLoaders/currentWeathers.js @@ -0,0 +1,25 @@ +import { getWeather } from '../../data/weather'; +import * as helpers from '../helpers'; +import * as slices from '../../../slices'; + +export const loadCurrentWeather = async (route, currentWeather, filteredCurrentWeathers, currentWeatherFilterPoints, dispatch, displayError) => { + const routePoints = route ? route.points : null; + + // Load if filtered cams don't exist or route doesn't match + if (!filteredCurrentWeathers || !helpers.compareRoutePoints(routePoints, currentWeatherFilterPoints)) { + // Fetch data if it doesn't already exist + const currentWeathersData = currentWeather ? currentWeather : await getWeather().catch((error) => displayError(error)); + + // Filter data by route + const filteredCurrentWeathersData = route ? helpers.filterByRoute(currentWeathersData, route, 15000) : currentWeathersData; + + dispatch( + slices.updateWeather({ + list: currentWeathersData, + filteredList: filteredCurrentWeathersData, + filterPoints: route ? route.points : null, + timeStamp: new Date().getTime() + }) + ); + } +}; diff --git a/src/frontend/src/Components/map/dataLoaders/events.js b/src/frontend/src/Components/map/dataLoaders/events.js new file mode 100644 index 000000000..4c6589b85 --- /dev/null +++ b/src/frontend/src/Components/map/dataLoaders/events.js @@ -0,0 +1,26 @@ +import { getEvents } from '../../data/events'; +import * as helpers from '../helpers'; +import * as slices from '../../../slices'; + +// Event layers +export const loadEvents = async (route, events, filteredEvents, eventFilterPoints, dispatch, displayError) => { + const routePoints = route ? route.points : null; + + // Load if filtered events don't exist or route doesn't match + if (!filteredEvents || !helpers.compareRoutePoints(routePoints, eventFilterPoints)) { + // Fetch data if it doesn't already exist + const eventData = events ? events : await getEvents().catch((error) => displayError(error)); + + // Filter data by route + const filteredEventData = route ? helpers.filterByRoute(eventData, route) : eventData; + + dispatch( + slices.updateEvents({ + list: eventData, + filteredList: filteredEventData, + filterPoints: route ? route.points : null, + timeStamp: new Date().getTime() + }) + ); + } +}; diff --git a/src/frontend/src/Components/map/dataLoaders/ferries.js b/src/frontend/src/Components/map/dataLoaders/ferries.js new file mode 100644 index 000000000..0288a2c97 --- /dev/null +++ b/src/frontend/src/Components/map/dataLoaders/ferries.js @@ -0,0 +1,25 @@ +import { getFerries } from '../../data/ferries'; +import * as helpers from '../helpers'; +import * as slices from '../../../slices'; + +export const loadFerries = async (route, ferries, filteredFerries, ferryFilterPoints, dispatch, displayError) => { + const routePoints = route ? route.points : null; + + // Load if filtered cams don't exist or route doesn't match + if (!filteredFerries || !helpers.compareRoutePoints(routePoints, ferryFilterPoints)) { + // Fetch data if it doesn't already exist + const ferryData = ferries ? ferries : await getFerries().catch((error) => displayError(error)); + + // Filter data by route + const filteredFerryData = route ? helpers.filterByRoute(ferryData, route) : ferryData; + + dispatch( + slices.updateFerries({ + list: ferryData, + filteredList: filteredFerryData, + filterPoints: route ? route.points : null, + timeStamp: new Date().getTime() + }) + ); + } +}; diff --git a/src/frontend/src/Components/map/dataLoaders/index.js b/src/frontend/src/Components/map/dataLoaders/index.js new file mode 100644 index 000000000..3cf4224f3 --- /dev/null +++ b/src/frontend/src/Components/map/dataLoaders/index.js @@ -0,0 +1,17 @@ +import { loadAdvisories } from './advisories'; +import { loadCameras } from './cameras'; +import { loadCurrentWeather } from './currentWeathers'; +import { loadEvents } from './events'; +import { loadFerries } from './ferries'; +import { loadRegionalWeather } from './regionalWeathers'; +import { loadRestStops } from './restStops'; + +export { + loadAdvisories, + loadCameras, + loadEvents, + loadFerries, + loadCurrentWeather, + loadRegionalWeather, + loadRestStops +} diff --git a/src/frontend/src/Components/map/dataLoaders/regionalWeathers.js b/src/frontend/src/Components/map/dataLoaders/regionalWeathers.js new file mode 100644 index 000000000..75956ebeb --- /dev/null +++ b/src/frontend/src/Components/map/dataLoaders/regionalWeathers.js @@ -0,0 +1,25 @@ +import { getRegional } from '../../data/weather'; +import * as helpers from '../helpers'; +import * as slices from '../../../slices'; + +export const loadRegionalWeather = async (route, regionalWeather, filteredRegionalWeathers, regionalWeatherFilterPoints, dispatch, displayError) => { + const routePoints = route ? route.points : null; + + // Load if filtered cams don't exist or route doesn't match + if (!filteredRegionalWeathers || !helpers.compareRoutePoints(routePoints, regionalWeatherFilterPoints)) { + // Fetch data if it doesn't already exist + const regionalWeathersData = regionalWeather ? regionalWeather : await getRegional().catch((error) => displayError(error)); + + // Filter with 20km extra tolerance + const filteredRegionalWeathersData = helpers.filterByRoute(regionalWeathersData, route, 15000); + + dispatch( + slices.updateRegional({ + list: regionalWeathersData, + filteredList: filteredRegionalWeathersData, + filterPoints: route ? route.points : null, + timeStamp: new Date().getTime() + }) + ); + } +}; diff --git a/src/frontend/src/Components/map/dataLoaders/restStops.js b/src/frontend/src/Components/map/dataLoaders/restStops.js new file mode 100644 index 000000000..1457686e1 --- /dev/null +++ b/src/frontend/src/Components/map/dataLoaders/restStops.js @@ -0,0 +1,25 @@ +import { getRestStops } from '../../data/restStops'; +import * as helpers from '../helpers'; +import * as slices from '../../../slices'; + +export const loadRestStops = async (route, restStops, filteredRestStops, restStopFilterPoints, dispatch, displayError) => { + const routePoints = route ? route.points : null; + + // Load if filtered cams don't exist or route doesn't match + if (!filteredRestStops || !helpers.compareRoutePoints(routePoints, restStopFilterPoints)) { + // Fetch data if it doesn't already exist + const restStopsData = restStops ? restStops : await getRestStops().catch((error) => displayError(error)); + + // Filter data by route + const filteredRestStopsData = route ? helpers.filterByRoute(restStopsData, route) : restStopsData; + + dispatch( + slices.updateRestStops({ + list: restStopsData, + filteredList: filteredRestStopsData, + filterPoints: route ? route.points : null, + timeStamp: new Date().getTime() + }) + ); + } +}; diff --git a/src/frontend/src/Components/map/errors/ServerError.js b/src/frontend/src/Components/map/errors/ServerError.js index ba665d12e..48e3e54dc 100644 --- a/src/frontend/src/Components/map/errors/ServerError.js +++ b/src/frontend/src/Components/map/errors/ServerError.js @@ -1,5 +1,5 @@ // React -import React from 'react'; +import React, { useState } from 'react'; // External imports import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -13,11 +13,11 @@ import staleLogo from '../../../images/status-stale.svg'; // Styling import './ServerError.scss'; -export default function ServerErrorPopup(props) { - const { setShowServerError } = props; +export default function ServerErrorPopup() { + const [visible, setVisible] = useState(true); // Rendering - return ( + return visible ? (
Server error
@@ -27,10 +27,10 @@ export default function ServerErrorPopup(props) {
{ event.stopPropagation(); - setShowServerError(false); + setVisible(false); }}>
- ); + ) : null; } diff --git a/src/frontend/src/Components/map/handlers/click.js b/src/frontend/src/Components/map/handlers/click.js new file mode 100644 index 000000000..239ef0a8b --- /dev/null +++ b/src/frontend/src/Components/map/handlers/click.js @@ -0,0 +1,259 @@ +import { + setEventStyle, + setZoomPan, +} from '../helpers'; +import trackEvent from '../../shared/TrackEvent.js'; + +import { isRestStopClosed } from '../../data/restStops.js'; + +// Styling +import { + cameraStyles, + ferryStyles, + roadWeatherStyles, + regionalStyles, + restStopStyles, + restStopClosedStyles, + restStopTruckStyles, + restStopTruckClosedStyles, +} from '../../data/featureStyleDefinitions.js'; + +// Click states +export const resetClickedStates = (targetFeature, clickedFeatureRef, updateClickedFeature, isCamDetail) => { + // No features were clicked before, do nothing + if (!clickedFeatureRef.current) { + return; + } + + // Reset feature if target feature does not equal to it or its altFeature + if (!targetFeature || (targetFeature != clickedFeatureRef.current && targetFeature != clickedFeatureRef.current.get('altFeature'))) { + switch(clickedFeatureRef.current.get('type')) { + case 'camera': + clickedFeatureRef.current.setStyle(cameraStyles['static']); + clickedFeatureRef.current.set('clicked', false); + updateClickedFeature(null); + break; + case 'event': { + setEventStyle(clickedFeatureRef.current, 'static'); + setEventStyle(clickedFeatureRef.current.get('altFeature') || [], 'static') + clickedFeatureRef.current.set('clicked', false); + + // Set alt feature to not clicked + const altFeatureList = clickedFeatureRef.current.get('altFeature'); + if (altFeatureList) { + const altFeature = altFeatureList instanceof Array ? altFeatureList[0] : altFeatureList; + altFeature.set('clicked', false); + } + + updateClickedFeature(null); + break; + } + case 'ferry': + clickedFeatureRef.current.setStyle(ferryStyles['static']); + clickedFeatureRef.current.set('clicked', false); + updateClickedFeature(null); + break; + case 'currentWeather': + clickedFeatureRef.current.setStyle(roadWeatherStyles['static']); + clickedFeatureRef.current.set('clicked', false); + updateClickedFeature(null); + break; + case 'regionalWeather': + clickedFeatureRef.current.setStyle(regionalStyles['static']); + clickedFeatureRef.current.set('clicked', false); + updateClickedFeature(null); + break; + case 'restStop': { + const isClosed = isRestStopClosed( + clickedFeatureRef.current.values_.properties, + ); + const isLargeVehiclesAccommodated = + clickedFeatureRef.current.values_.properties + .ACCOM_COMMERCIAL_TRUCKS === 'Yes' + ? true + : false; + if (isClosed) { + if (isLargeVehiclesAccommodated) { + clickedFeatureRef.current.setStyle( + restStopTruckClosedStyles['static'], + ); + } else { + clickedFeatureRef.current.setStyle( + restStopClosedStyles['static'], + ); + } + } else { + if (isLargeVehiclesAccommodated) { + clickedFeatureRef.current.setStyle( + restStopTruckStyles['static'], + ); + } else { + clickedFeatureRef.current.setStyle(restStopStyles['static']); + } + } + clickedFeatureRef.current.set('clicked', false); + updateClickedFeature(null); + break; + } + } + } +}; + +const camClickHandler = (feature, clickedFeatureRef, updateClickedFeature, mapView, isCamDetail, loadCamDetails) => { + resetClickedStates(feature, clickedFeatureRef, updateClickedFeature, isCamDetail); + + // set new clicked camera feature + feature.setStyle(cameraStyles['active']); + feature.setProperties({ clicked: true }, true); + + updateClickedFeature(feature); + + if (isCamDetail) { + setZoomPan(mapView, null, feature.getGeometry().getCoordinates()); + loadCamDetails(feature.getProperties()); + } +}; + +const eventClickHandler = (feature, clickedFeatureRef, updateClickedFeature, isCamDetail) => { + // reset previous clicked feature + resetClickedStates(feature, clickedFeatureRef, updateClickedFeature, isCamDetail); + + // set new clicked event feature + setEventStyle(feature, 'active'); + setEventStyle(feature.get('altFeature') || [], 'active'); + feature.set('clicked', true); + + // Set alt feature to clicked + const altFeatureList = feature.get('altFeature'); + if (altFeatureList) { + const altFeature = altFeatureList instanceof Array ? altFeatureList[0] : altFeatureList; + altFeature.set('clicked', true); + } + + updateClickedFeature(feature); +}; + +const ferryClickHandler = (feature, clickedFeatureRef, updateClickedFeature, isCamDetail) => { + // reset previous clicked feature + resetClickedStates(feature, clickedFeatureRef, updateClickedFeature, isCamDetail); + + // set new clicked ferry feature + feature.setStyle(ferryStyles['active']); + feature.setProperties({ clicked: true }, true); + updateClickedFeature(feature); +}; + +const weatherClickHandler = (feature, clickedFeatureRef, updateClickedFeature, isCamDetail) => { + // reset previous clicked feature + resetClickedStates(feature, clickedFeatureRef, updateClickedFeature, isCamDetail); + + // set new clicked ferry feature + feature.setStyle(roadWeatherStyles['active']); + feature.setProperties({ clicked: true }, true); + updateClickedFeature(feature); +}; + +const regionalClickHandler = (feature, clickedFeatureRef, updateClickedFeature, isCamDetail) => { + // reset previous clicked feature + resetClickedStates(feature, clickedFeatureRef, updateClickedFeature, isCamDetail); + + // set new clicked ferry feature + feature.setStyle(regionalStyles['active']); + feature.setProperties({ clicked: true }, true); + updateClickedFeature(feature); +}; + +const restStopClickHandler = (feature, clickedFeatureRef, updateClickedFeature, isCamDetail) => { + // reset previous clicked feature + resetClickedStates(feature, clickedFeatureRef, updateClickedFeature, isCamDetail); + + // set new clicked rest stop feature + const isClosed = isRestStopClosed(feature.values_.properties); + const isLargeVehiclesAccommodated = + feature.values_.properties.ACCOM_COMMERCIAL_TRUCKS === 'Yes' + ? true + : false; + if (isClosed) { + if (isLargeVehiclesAccommodated) { + feature.setStyle(restStopTruckClosedStyles['active']); + } else { + feature.setStyle(restStopClosedStyles['active']); + } + } else { + if (isLargeVehiclesAccommodated) { + feature.setStyle(restStopTruckStyles['active']); + } else { + feature.setStyle(restStopStyles['active']); + } + } + feature.setProperties({ clicked: true }, true); + updateClickedFeature(feature); +}; + +export const pointerClickHandler = ( + features, clickedFeatureRef, updateClickedFeature, + mapView, isCamDetail, loadCamDetails +) => { + if (features.length) { + const clickedFeature = features[0]; + switch (clickedFeature.getProperties()['type']) { + case 'camera': + trackEvent( + 'click', + 'map', + 'camera', + clickedFeature.getProperties().name, + ); + camClickHandler(clickedFeature, clickedFeatureRef, updateClickedFeature, mapView, isCamDetail, loadCamDetails); + return; + case 'event': + trackEvent( + 'click', + 'map', + 'event', + clickedFeature.getProperties().name, + ); + eventClickHandler(clickedFeature, clickedFeatureRef, updateClickedFeature, isCamDetail); + return; + case 'ferry': + trackEvent( + 'click', + 'map', + 'ferry', + clickedFeature.getProperties().name, + ); + ferryClickHandler(clickedFeature, clickedFeatureRef, updateClickedFeature, isCamDetail); + return; + case 'currentWeather': + trackEvent( + 'click', + 'map', + 'weather', + clickedFeature.getProperties().weather_station_name, + ); + weatherClickHandler(clickedFeature, clickedFeatureRef, updateClickedFeature, isCamDetail); + return; + case 'regionalWeather': + trackEvent( + 'click', + 'map', + 'regional weather', + clickedFeature.getProperties().name, + ); + regionalClickHandler(clickedFeature, clickedFeatureRef, updateClickedFeature, isCamDetail); + return; + case 'restStop': + trackEvent( + 'click', + 'map', + 'rest stop', + clickedFeature.getProperties().properties.REST_AREA_NAME + ); + restStopClickHandler(clickedFeature, clickedFeatureRef, updateClickedFeature, isCamDetail); + return; + } + } + + // Close popups if clicked on blank space + resetClickedStates(null, clickedFeatureRef, updateClickedFeature, isCamDetail); +} diff --git a/src/frontend/src/Components/map/handlers/hover.js b/src/frontend/src/Components/map/handlers/hover.js new file mode 100644 index 000000000..ac0a7c92e --- /dev/null +++ b/src/frontend/src/Components/map/handlers/hover.js @@ -0,0 +1,161 @@ +import { + cameraStyles, + ferryStyles, + roadWeatherStyles, + regionalStyles, + restStopStyles, + restStopClosedStyles, + restStopTruckStyles, + restStopTruckClosedStyles, +} from '../../data/featureStyleDefinitions.js'; +import { + setEventStyle +} from '../helpers'; +import { isRestStopClosed } from '../../data/restStops.js'; + +export const resetHoveredStates = (targetFeature, hoveredFeatureRef) => { + let hoveredFeature = hoveredFeatureRef.current; + + // Reset feature if target isn't clicked + if (hoveredFeature && targetFeature != hoveredFeature) { + if (!hoveredFeature.getProperties().clicked) { + switch (hoveredFeature.getProperties()['type']) { + case 'camera': + hoveredFeature.setStyle(cameraStyles['static']); + break; + case 'event': { + // Reset feature if alt feature also isn't clicked + const altFeatureList = hoveredFeature.get('altFeature'); + if (altFeatureList) { + const altFeature = altFeatureList instanceof Array ? altFeatureList[0] : altFeatureList; + if (!altFeature.getProperties().clicked) { + setEventStyle(hoveredFeature, 'static'); + setEventStyle(hoveredFeature.get('altFeature') || [], 'static'); + } + } + break; + } + case 'ferry': + hoveredFeature.setStyle(ferryStyles['static']); + break; + case 'currentWeather': + hoveredFeature.setStyle(roadWeatherStyles['static']); + break; + case 'regionalWeather': + hoveredFeature.setStyle(regionalStyles['static']); + break; + case 'restStop': + { + const isClosed = isRestStopClosed( + hoveredFeature.values_.properties, + ); + const isLargeVehiclesAccommodated = + hoveredFeature.values_.properties + .ACCOM_COMMERCIAL_TRUCKS === 'Yes' + ? true + : false; + if (isClosed) { + if (isLargeVehiclesAccommodated) { + hoveredFeature.setStyle( + restStopTruckClosedStyles['static'], + ); + } else { + hoveredFeature.setStyle( + restStopClosedStyles['static'], + ); + } + } else { + if (isLargeVehiclesAccommodated) { + hoveredFeature.setStyle( + restStopTruckStyles['static'], + ); + } else { + hoveredFeature.setStyle(restStopStyles['static']); + } + } + } + break; + } + } + + hoveredFeature = null; + } +}; + +export const pointerMoveHandler = (e, mapRef, hoveredFeature) => { + const features = mapRef.current.getFeaturesAtPixel(e.pixel, { + hitTolerance: 20, + }); + + if (features.length) { + const targetFeature = features[0]; + resetHoveredStates(targetFeature, hoveredFeature); + hoveredFeature.current = targetFeature; + + // Set hover style if feature isn't clicked + switch (targetFeature.getProperties()['type']) { + case 'camera': + if (!targetFeature.getProperties().clicked) { + targetFeature.setStyle(cameraStyles['hover']); + } + return; + case 'event': + if (!targetFeature.getProperties().clicked) { + setEventStyle(targetFeature, 'hover'); + + // Set alt feature style if it isn't clicked + const altFeatureList = targetFeature.get('altFeature'); + if (altFeatureList) { + const altFeature = altFeatureList instanceof Array ? altFeatureList[0] : altFeatureList; + if (!altFeature.getProperties().clicked) { + setEventStyle(altFeature, 'hover'); + } + } + } + return; + case 'ferry': + if (!targetFeature.getProperties().clicked) { + targetFeature.setStyle(ferryStyles['hover']); + } + return; + case 'currentWeather': + if (!targetFeature.getProperties().clicked) { + targetFeature.setStyle(roadWeatherStyles['hover']); + } + return; + case 'regionalWeather': + if (!targetFeature.getProperties().clicked) { + targetFeature.setStyle(regionalStyles['hover']); + } + return; + case 'restStop': + if (!targetFeature.getProperties().clicked) { + const isClosed = isRestStopClosed( + targetFeature.values_.properties, + ); + const isLargeVehiclesAccommodated = + targetFeature.values_.properties.ACCOM_COMMERCIAL_TRUCKS === + 'Yes' + ? true + : false; + if (isClosed) { + if (isLargeVehiclesAccommodated) { + targetFeature.setStyle(restStopTruckClosedStyles['hover']); + } else { + targetFeature.setStyle(restStopClosedStyles['hover']); + } + } else { + if (isLargeVehiclesAccommodated) { + targetFeature.setStyle(restStopTruckStyles['hover']); + } else { + targetFeature.setStyle(restStopStyles['hover']); + } + } + } + return; + } + } + + // Reset on blank space + resetHoveredStates(null, hoveredFeature); +}; diff --git a/src/frontend/src/Components/map/helper.js b/src/frontend/src/Components/map/helper.js deleted file mode 100644 index 85789a399..000000000 --- a/src/frontend/src/Components/map/helper.js +++ /dev/null @@ -1,280 +0,0 @@ -/* eslint-disable guard-for-in */ -// Map & geospatial imports -import { fromLonLat, transformExtent } from 'ol/proj'; -import * as turf from '@turf/turf'; -import Flatbush from 'flatbush'; -import Overlay from 'ol/Overlay.js'; - -// Styling -import { closureStyles, eventStyles } from '../data/featureStyleDefinitions.js'; - -// Static assets -export const setEventStyle = (events, state) => { - if (!Array.isArray(events)) { events = [events]; } - - events.forEach((event) => { - const display_category = event.get('display_category'); - const is_closure = event.get('closed'); - const geometry = event.getGeometry().getType(); - - if (geometry !== 'Point') { // Line/polygon segments - const category = is_closure ? 'closure' : display_category; - event.setStyle(eventStyles['segments'][category][state]); - } else { // Points - if (is_closure) { - return event.setStyle(eventStyles['closures'][state]); - } - const severity = event.get('severity').toLowerCase(); - - switch (display_category) { - case 'futureEvents': - return event.setStyle(eventStyles[ - severity === 'major' ? 'major_future_events' : 'future_events' - ][state]); - - case 'roadConditions': - return event.setStyle(eventStyles['road_conditions'][state]); - - default: { - const type = event.get('event_type').toLowerCase(); - if (type === 'construction') { - event.setStyle(eventStyles[ - severity === 'major' ? 'major_constructions' : 'constructions' - ][state]); - } else { // Other major/minor delays - event.setStyle(eventStyles[ - severity === 'major' ? 'major_generic_delays' : 'generic_delays' - ][state]); - } - } - } - } - }) -}; - - -// Map transformation -export const transformFeature = (feature, sourceCRS, targetCRS) => { - const clone = feature.clone(); - clone.getGeometry().transform(sourceCRS, targetCRS); - return clone; -}; - -// Zoom and pan -export const fitMap = (route, mapView) => { - const routeBbox = turf.bbox(turf.lineString(route)); - const routeExtent = transformExtent(routeBbox, 'EPSG:4326', 'EPSG:3857'); - - if (mapView.current) { - mapView.current.fit(routeExtent, { duration: 1000 }); - } -} - -export const setZoomPan = (mapView, zoom, panCoords) => { - if (!mapView.current) { - return; - } - - const args = { - duration: 1000 - }; - - if (zoom) { - args.zoom = zoom; - } - - if (panCoords) { - args.center = panCoords; - } - - mapView.current.animate(args); -}; - -export const zoomIn = (mapView) => { - if (!mapView.current) { - return; - } - - setZoomPan(mapView, mapView.current.getZoom() + 1); -} - -export const zoomOut = (mapView) => { - if (!mapView.current) { - return; - } - - setZoomPan(mapView, mapView.current.getZoom() - 1); -} - -// Location pins -export const blueLocationMarkup = ` - - - - - - - - - - - - - - - - -`; - -export const redLocationMarkup = ` - - - - - - - - - - - - - - - - -`; - -export const setLocationPin = (coordinates, svgMarkup, mapRef, pinRef) => { - const svgImage = new Image(); - svgImage.src = - 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgMarkup); - svgImage.alt = 'my location pin'; - - // Create an overlay for the marker - const pinOverlay = new Overlay({ - position: fromLonLat(coordinates), - positioning: 'center-center', - element: svgImage, - stopEvent: false, // Allow interactions with the overlay content - }); - - if (pinRef) { - pinRef.current = pinOverlay; - } - - mapRef.current.addOverlay(pinOverlay); - mapRef.current.on('moveend', function (event) { - const newZoom = mapRef.current.getView().getZoom(); - // Calculate new marker size based on the zoom level - const newSize = 44 * (newZoom / 10); - svgImage.style.width = newSize + 'px'; - svgImage.style.height = newSize + 'px'; - }); -} - -// Route filtering and ordering -export const populateRouteProjection = (data, route) => { - // Deep copy to avoid direct state mutation - const res = JSON.parse(JSON.stringify(data)); - - // Reference route start point/ls - const routeLs = turf.lineString(route.route); - const startPoint = turf.point(route.route[0]); - - // Calculate and store distance alone reference line - for (let i=0; i < res.length; i++) { - const camPt = turf.point(res[i].location.coordinates); - const closestPoint = turf.nearestPointOnLine(routeLs, camPt, { units: 'meters' }); - - const distanceAlongLine = turf.lineDistance(turf.lineSlice(startPoint, closestPoint, routeLs), { units: 'meters' }); - res[i].route_projection = distanceAlongLine; - } - - return res; -} - -export const filterByRoute = (data, route, extraToleranceMeters, populateProjection) => { - if (!route) { - return data; - } - - const lineCoords = route.route; - const routeLineString = turf.lineString(lineCoords); - const bufferedRouteLineString = turf.buffer(routeLineString, 150, {units: 'meters'}); - const routeBBox = turf.bbox(routeLineString); - - const spatialIndex = new Flatbush(data.length); - - data.forEach((entry) => { - // Add points to the index with slight tolerance - if (entry.location.type == "Point") { - const coords = entry.location.coordinates; - const pointRadius = extraToleranceMeters ? 0.0001 * (extraToleranceMeters / 10) : 0.0001; // ~11m default tolerance - spatialIndex.add(coords[0] - pointRadius, coords[1] - pointRadius, coords[0] + pointRadius, coords[1] + pointRadius); - - // Add linestrings to the index - } else { - const coords = entry.location.coordinates; - const ls = turf.lineString(coords); - const bbox = turf.bbox(routeLineString); - spatialIndex.add(turf.bbox[0], turf.bbox[1], turf.bbox[2], turf.bbox[3]); - } - }); - - // Finish building the index - spatialIndex.finish(); - - // Query the index for features intersecting with the linestring - const dataInBBox = []; - spatialIndex.search(routeBBox[0], routeBBox[1], routeBBox[2], routeBBox[3], (idx) => { - dataInBBox.push(data[idx]); - }); - - // Narrow down the results to only include intersections along the linestring - const intersectingData = dataInBBox.filter(entry => { - if (entry.location.type == "Point") { - const coords = entry.location.coordinates; - let dataPoint = turf.point(coords); - if (extraToleranceMeters) { - dataPoint = turf.buffer(dataPoint, extraToleranceMeters, {units: 'meters'}); - } - - return turf.booleanIntersects(dataPoint, bufferedRouteLineString); - - } else { - const coords = entry.location.coordinates; - const dataLs = turf.lineString(coords); - - return turf.booleanIntersects(dataLs, routeLineString); - } - }); - - // Populate route projection for camera ordering - if (populateProjection) { - return populateRouteProjection(intersectingData, route); - } - - return intersectingData; -} - -export const compareRoutePoints = (routePoints, savedPoints) => { - // Both are arrays of points, compare each point - if (!!routePoints && !!savedPoints) { - for (let i=0; i < routePoints.length; i++) { - const rPoint = turf.point(routePoints[i]); - const sPoint = turf.point(savedPoints[i]); - - // Return false if one of the points aren't equal - if (!turf.booleanEqual(rPoint, sPoint)) { - return false; - } - } - - // Return true if all points are equal - return true; - } - - // Direct comparison if not both of them are arrays of points - return routePoints == savedPoints; -} diff --git a/src/frontend/src/Components/map/helpers/advisories.js b/src/frontend/src/Components/map/helpers/advisories.js new file mode 100644 index 000000000..773279af7 --- /dev/null +++ b/src/frontend/src/Components/map/helpers/advisories.js @@ -0,0 +1,36 @@ +import { getBottomLeft, getTopRight } from 'ol/extent'; +import { toLonLat } from 'ol/proj'; +import * as turf from '@turf/turf'; + +const wrapLon = (value) => { + const worlds = Math.floor((value + 180) / 360); + return value - worlds * 360; +} + +export const onMoveEnd = (e, advisories, setAdvisoriesInView) => { + // calculate polygon based on map extent + const map = e.map; + const extent = map.getView().calculateExtent(map.getSize()); + const bottomLeft = toLonLat(getBottomLeft(extent)); + const topRight = toLonLat(getTopRight(extent)); + + const mapPoly = turf.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 = turf.polygon(advisory.geometry.coordinates); + if (turf.booleanIntersects(mapPoly, advPoly)) { + resAdvisories.push(advisory); + } + }); + } + setAdvisoriesInView(resAdvisories); +} diff --git a/src/frontend/src/Components/map/helpers/events.js b/src/frontend/src/Components/map/helpers/events.js new file mode 100644 index 000000000..059489e17 --- /dev/null +++ b/src/frontend/src/Components/map/helpers/events.js @@ -0,0 +1,51 @@ +// Styling +import { eventStyles } from '../../data/featureStyleDefinitions.js'; + +// Static assets +export const setEventStyle = (events, state) => { + if (!Array.isArray(events)) { events = [events]; } + + events.forEach((event) => { + const display_category = event.get('display_category'); + const is_closure = event.get('closed'); + const geometry = event.getGeometry().getType(); + + if (geometry !== 'Point') { // Line segments + const category = is_closure ? 'closure' : display_category; + + if (event.get('layerType') === 'webgl') { + event.setProperties(eventStyles['segments'][category][state]); + } else { + event.setStyle(eventStyles['segments'][category][state]); + } + } else { // Points + if (is_closure) { + return event.setStyle(eventStyles['closures'][state]); + } + const severity = event.get('severity').toLowerCase(); + + switch (display_category) { + case 'futureEvents': + return event.setStyle(eventStyles[ + severity === 'major' ? 'major_future_events' : 'future_events' + ][state]); + + case 'roadConditions': + return event.setStyle(eventStyles['road_conditions'][state]); + + default: { + const type = event.get('event_type').toLowerCase(); + if (type === 'construction') { + event.setStyle(eventStyles[ + severity === 'major' ? 'major_constructions' : 'constructions' + ][state]); + } else { // Other major/minor delays + event.setStyle(eventStyles[ + severity === 'major' ? 'major_generic_delays' : 'generic_delays' + ][state]); + } + } + } + } + }) +}; diff --git a/src/frontend/src/Components/map/helpers/index.js b/src/frontend/src/Components/map/helpers/index.js new file mode 100644 index 000000000..35dcde71b --- /dev/null +++ b/src/frontend/src/Components/map/helpers/index.js @@ -0,0 +1,19 @@ +// Import all helper functions for map component +import { onMoveEnd } from './advisories'; +import { setEventStyle } from './events'; +import { blueLocationMarkup, redLocationMarkup, setLocationPin } from './location'; +import { calculateCenter, fitMap, setZoomPan, toggleMyLocation, transformFeature, zoomIn, zoomOut } from './map'; +import { compareRoutePoints, filterByRoute, populateRouteProjection } from './spatial'; + +export { + // advisories, + onMoveEnd, + // events + setEventStyle, + // location + blueLocationMarkup, redLocationMarkup, setLocationPin, + // map + calculateCenter, fitMap, setZoomPan, toggleMyLocation, transformFeature, zoomIn, zoomOut, + // spatial + compareRoutePoints, filterByRoute, populateRouteProjection +}; diff --git a/src/frontend/src/Components/map/helpers/location.js b/src/frontend/src/Components/map/helpers/location.js new file mode 100644 index 000000000..34b7b92d3 --- /dev/null +++ b/src/frontend/src/Components/map/helpers/location.js @@ -0,0 +1,69 @@ +import { fromLonLat } from 'ol/proj'; +import Overlay from 'ol/Overlay.js'; + +// Location pins +export const blueLocationMarkup = ` + + + + + + + + + + + + + + + + +`; + +export const redLocationMarkup = ` + + + + + + + + + + + + + + + + +`; + +export const setLocationPin = (coordinates, svgMarkup, mapRef, pinRef) => { + const svgImage = new Image(); + svgImage.src = + 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgMarkup); + svgImage.alt = 'my location pin'; + + // Create an overlay for the marker + const pinOverlay = new Overlay({ + position: fromLonLat(coordinates), + positioning: 'center-center', + element: svgImage, + stopEvent: false, // Allow interactions with the overlay content + }); + + if (pinRef) { + pinRef.current = pinOverlay; + } + + mapRef.current.addOverlay(pinOverlay); + mapRef.current.on('moveend', function (event) { + const newZoom = mapRef.current.getView().getZoom(); + // Calculate new marker size based on the zoom level + const newSize = 44 * (newZoom / 10); + svgImage.style.width = newSize + 'px'; + svgImage.style.height = newSize + 'px'; + }); +} diff --git a/src/frontend/src/Components/map/helpers/map.js b/src/frontend/src/Components/map/helpers/map.js new file mode 100644 index 000000000..80f6cae95 --- /dev/null +++ b/src/frontend/src/Components/map/helpers/map.js @@ -0,0 +1,101 @@ +// Internal imports +import { redLocationMarkup, setLocationPin } from './'; + +// Map & geospatial imports +import { fromLonLat, transformExtent } from 'ol/proj'; +import * as turf from '@turf/turf'; + +// Map transformation +export const transformFeature = (feature, sourceCRS, targetCRS) => { + const clone = feature.clone(); + clone.getGeometry().transform(sourceCRS, targetCRS); + return clone; +}; + +// Zoom and pan +export const fitMap = (route, mapView) => { + const routeBbox = turf.bbox(turf.lineString(route)); + const routeExtent = transformExtent(routeBbox, 'EPSG:4326', 'EPSG:3857'); + + if (mapView.current) { + mapView.current.fit(routeExtent, { duration: 1000 }); + } +} + +export const setZoomPan = (mapView, zoom, panCoords) => { + if (!mapView.current) { + return; + } + + const args = { + duration: 1000 + }; + + if (zoom) { + args.zoom = zoom; + } + + if (panCoords) { + args.center = panCoords; + } + + mapView.current.animate(args); +}; + +export const zoomIn = (mapView) => { + if (!mapView.current) { + return; + } + + setZoomPan(mapView, mapView.current.getZoom() + 1); +} + +export const zoomOut = (mapView) => { + if (!mapView.current) { + return; + } + + setZoomPan(mapView, mapView.current.getZoom() - 1); +} + +export const toggleMyLocation = (mapRef, mapView) => { + if ('geolocation' in navigator) { + navigator.geolocation.getCurrentPosition( + position => { + const { latitude, longitude } = position.coords; + if ( + position.coords.longitude <= -113.7 && + position.coords.longitude >= -139.3 && + position.coords.latitude <= 60.1 && + position.coords.latitude >= 48.2 + ) { + setZoomPan(mapView, 9, fromLonLat([longitude, latitude])); + setLocationPin([longitude, latitude], redLocationMarkup, mapRef); + } else { + // set my location to the center of BC for users outside of BC + setZoomPan(mapView, 9, fromLonLat([-126.5, 54.2])); + setLocationPin([-126.5, 54.2], redLocationMarkup, mapRef); + } + }, + error => { + if (error.code === error.PERMISSION_DENIED) { + // The user has blocked location access + console.error('Location access denied by user.', error); + } else { + // Zoom out and center to BC if location not available + setZoomPan(mapView, 9, fromLonLat([-126.5, 54.2])); + } + }, + ); + } +} + +export const calculateCenter = (referenceData) => { + return Array.isArray(referenceData.location.coordinates[0]) + ? fromLonLat( + referenceData.location.coordinates[ + Math.floor(referenceData.location.coordinates.length / 2) + ], + ) + : fromLonLat(referenceData.location.coordinates); +} diff --git a/src/frontend/src/Components/map/helpers/reference.js b/src/frontend/src/Components/map/helpers/reference.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/frontend/src/Components/map/helpers/spatial.js b/src/frontend/src/Components/map/helpers/spatial.js new file mode 100644 index 000000000..73bb318c5 --- /dev/null +++ b/src/frontend/src/Components/map/helpers/spatial.js @@ -0,0 +1,109 @@ +// Map & geospatial imports +import * as turf from '@turf/turf'; +import Flatbush from 'flatbush'; + +// Route filtering and ordering +export const populateRouteProjection = (data, route) => { + // Deep copy to avoid direct state mutation + const res = JSON.parse(JSON.stringify(data)); + + // Reference route start point/ls + const routeLs = turf.lineString(route.route); + const startPoint = turf.point(route.route[0]); + + // Calculate and store distance alone reference line + for (let i=0; i < res.length; i++) { + const camPt = turf.point(res[i].location.coordinates); + const closestPoint = turf.nearestPointOnLine(routeLs, camPt, { units: 'meters' }); + + const distanceAlongLine = turf.lineDistance(turf.lineSlice(startPoint, closestPoint, routeLs), { units: 'meters' }); + res[i].route_projection = distanceAlongLine; + } + + return res; +} + +export const filterByRoute = (data, route, extraToleranceMeters, populateProjection) => { + if (!route) { + return data; + } + + const lineCoords = route.route; + const routeLineString = turf.lineString(lineCoords); + const bufferedRouteLineString = turf.buffer(routeLineString, 150, {units: 'meters'}); + const routeBBox = turf.bbox(routeLineString); + + const spatialIndex = new Flatbush(data.length); + + data.forEach((entry) => { + // Add points to the index with slight tolerance + if (entry.location.type == "Point") { + const coords = entry.location.coordinates; + const pointRadius = extraToleranceMeters ? 0.0001 * (extraToleranceMeters / 10) : 0.0001; // ~11m default tolerance + spatialIndex.add(coords[0] - pointRadius, coords[1] - pointRadius, coords[0] + pointRadius, coords[1] + pointRadius); + + // Add linestrings to the index + } else { + const coords = entry.location.coordinates; + const entryLs = turf.lineString(coords); + const entryBbox = turf.bbox(entryLs); + spatialIndex.add(entryBbox[0], entryBbox[1], entryBbox[2], entryBbox[3]); + } + }); + + // Finish building the index + spatialIndex.finish(); + + // Query the index for features intersecting with the linestring + const dataInBBox = []; + spatialIndex.search(routeBBox[0], routeBBox[1], routeBBox[2], routeBBox[3], (idx) => { + dataInBBox.push(data[idx]); + }); + + // Narrow down the results to only include intersections along the linestring + const intersectingData = dataInBBox.filter(entry => { + if (entry.location.type == "Point") { + const coords = entry.location.coordinates; + let dataPoint = turf.point(coords); + if (extraToleranceMeters) { + dataPoint = turf.buffer(dataPoint, extraToleranceMeters, {units: 'meters'}); + } + + return turf.booleanIntersects(dataPoint, bufferedRouteLineString); + + } else { + const coords = entry.location.coordinates; + const dataLs = turf.lineString(coords); + + return turf.booleanIntersects(dataLs, routeLineString); + } + }); + + // Populate route projection for camera ordering + if (populateProjection) { + return populateRouteProjection(intersectingData, route); + } + + return intersectingData; +} + +export const compareRoutePoints = (routePoints, savedPoints) => { + // Both are arrays of points, compare each point + if (!!routePoints && !!savedPoints) { + for (let i=0; i < routePoints.length; i++) { + const rPoint = turf.point(routePoints[i]); + const sPoint = turf.point(savedPoints[i]); + + // Return false if one of the points aren't equal + if (!turf.booleanEqual(rPoint, sPoint)) { + return false; + } + } + + // Return true if all points are equal + return true; + } + + // Direct comparison if not both of them are arrays of points + return routePoints == savedPoints; +} diff --git a/src/frontend/src/Components/map/layers/advisoriesLayer.js b/src/frontend/src/Components/map/layers/advisoriesLayer.js index 2b93b5d49..be96b1a35 100644 --- a/src/frontend/src/Components/map/layers/advisoriesLayer.js +++ b/src/frontend/src/Components/map/layers/advisoriesLayer.js @@ -1,5 +1,5 @@ // Components and functions -import { transformFeature } from '../helper.js'; +import { transformFeature } from '../helpers'; // OpenLayers import { Polygon } from 'ol/geom'; @@ -28,7 +28,7 @@ export function getAdvisoriesLayer( advisories.forEach(advisory => { // Build a new OpenLayers feature const olGeometry = new Polygon(advisory.geometry.coordinates); - const olFeature = new ol.Feature({ geometry: olGeometry }); + const olFeature = new ol.Feature({ geometry: olGeometry, type: 'advisory' }); // Transform the projection const olFeatureForMap = transformFeature( diff --git a/src/frontend/src/Components/map/layers/camerasLayer.js b/src/frontend/src/Components/map/layers/camerasLayer.js index 466ab6e32..134306082 100644 --- a/src/frontend/src/Components/map/layers/camerasLayer.js +++ b/src/frontend/src/Components/map/layers/camerasLayer.js @@ -1,5 +1,5 @@ // Components and functions -import { transformFeature } from '../helper.js'; +import { transformFeature } from '../helpers'; // OpenLayers import { Point } from 'ol/geom'; @@ -11,7 +11,7 @@ import VectorSource from 'ol/source/Vector'; // Styling import { cameraStyles } from '../../data/featureStyleDefinitions.js'; -export function getCamerasLayer(cameras, projectionCode, mapContext) { +export function getCamerasLayer(cameras, projectionCode, mapContext, referenceData, updateReferenceFeature) { return new VectorLayer({ classname: 'webcams', visible: mapContext.visible_layers.highwayCams, @@ -24,11 +24,10 @@ export function getCamerasLayer(cameras, projectionCode, mapContext) { cameras.forEach(camera => { // Build a new OpenLayers feature const olGeometry = new Point(camera.location.coordinates); - const olFeature = new ol.Feature({ geometry: olGeometry }); + const olFeature = new ol.Feature({ geometry: olGeometry, type: 'camera' }); // Transfer properties olFeature.setProperties(camera); - olFeature.set('type', 'camera'); // Transform the projection const olFeatureForMap = transformFeature( @@ -40,6 +39,15 @@ export function getCamerasLayer(cameras, projectionCode, mapContext) { olFeatureForMap.setId(camera.id); vectorSource.addFeature(olFeatureForMap); + + if (referenceData?.type === 'camera') { + // Update the reference feature if one of the cameras is the reference + olFeatureForMap.getProperties().camGroup.forEach((cam) => { + if (cam.id == referenceData.id) { + updateReferenceFeature(olFeatureForMap); + } + }); + } }); }, }), diff --git a/src/frontend/src/Components/map/layers/weatherLayer.js b/src/frontend/src/Components/map/layers/currentWeatherLayer.js similarity index 88% rename from src/frontend/src/Components/map/layers/weatherLayer.js rename to src/frontend/src/Components/map/layers/currentWeatherLayer.js index d3d89b6a8..544db1d7c 100644 --- a/src/frontend/src/Components/map/layers/weatherLayer.js +++ b/src/frontend/src/Components/map/layers/currentWeatherLayer.js @@ -1,5 +1,5 @@ // Components and functions -import { transformFeature } from '../helper.js'; +import { transformFeature } from '../helpers'; // OpenLayers import { Point } from 'ol/geom'; @@ -11,7 +11,7 @@ import VectorSource from 'ol/source/Vector'; // Styling import { roadWeatherStyles } from '../../data/featureStyleDefinitions.js'; -export function loadWeatherLayers(weatherData, mapContext, projectionCode) { +export function getCurrentWeatherLayer(weatherData, projectionCode, mapContext) { return new VectorLayer({ classname: 'weather', visible: mapContext.visible_layers.weather, @@ -29,11 +29,10 @@ export function loadWeatherLayers(weatherData, mapContext, projectionCode) { const lat = weather.location.coordinates[0]; const lng = weather.location.coordinates[1] const olGeometry = new Point([lat, lng]); - const olFeature = new ol.Feature({ geometry: olGeometry }); + const olFeature = new ol.Feature({ geometry: olGeometry, type: 'currentWeather' }); // Transfer properties olFeature.setProperties(weather) - olFeature.set('type', 'weather'); // Transform the projection const olFeatureForMap = transformFeature( diff --git a/src/frontend/src/Components/map/layers/eventsLayer.js b/src/frontend/src/Components/map/layers/eventsLayer.js index 3d1da32f9..83e67dcf8 100644 --- a/src/frontend/src/Components/map/layers/eventsLayer.js +++ b/src/frontend/src/Components/map/layers/eventsLayer.js @@ -1,24 +1,14 @@ // Components and functions -import { setEventStyle, transformFeature } from '../helper.js'; +import { setEventStyle } from '../helpers'; // OpenLayers import { Point, LineString, Polygon } from 'ol/geom'; import * as ol from 'ol'; import GeoJSON from 'ol/format/GeoJSON.js'; -import { Fill, Icon, Stroke, Style } from 'ol/style.js'; -import Layer from 'ol/layer/Layer.js'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; -import { eventStyles } from '../../data/featureStyleDefinitions.js'; - - -export function loadEventsLayers( - eventsData, - mapContext, - mapLayers, - mapRef -) { +export function loadEventsLayers(eventsData, mapContext, mapLayers, mapRef, referenceData, updateReferenceFeature) { // Helper function for initializing vss const createVS = () => new VectorSource({ format: new GeoJSON() @@ -88,6 +78,10 @@ export function loadEventsLayers( pointFeature.getGeometry().transform('EPSG:4326', currentProjection); addFeature(pointFeature, event.display_category); + if (referenceData?.type === 'event' && event.id == referenceData?.id) { + updateReferenceFeature(pointFeature); + } + // polygons are generated backend and used if available if (event.polygon) { const feature = new ol.Feature({ @@ -100,6 +94,7 @@ export function loadEventsLayers( feature.getGeometry().transform('EPSG:4326', currentProjection); addFeature(feature, event.display_category); pointFeature.set('altFeature', feature); + } else { const features = locationData.reduce((all, location, ii) => { const geometry = location.type === 'LineString' @@ -119,6 +114,7 @@ export function loadEventsLayers( all.push(feature); return all; }, []); + pointFeature.set('altFeature', features); } }); diff --git a/src/frontend/src/Components/map/layers/ferriesLayer.js b/src/frontend/src/Components/map/layers/ferriesLayer.js index 9438d4cd2..6ebed7dd2 100644 --- a/src/frontend/src/Components/map/layers/ferriesLayer.js +++ b/src/frontend/src/Components/map/layers/ferriesLayer.js @@ -1,5 +1,5 @@ // Components and functions -import { transformFeature } from '../helper.js'; +import { transformFeature } from '../helpers'; // OpenLayers import { Point } from 'ol/geom'; @@ -24,11 +24,10 @@ export function getFerriesLayer(ferriesData, projectionCode, mapContext) { ferriesData.forEach(ferry => { // Build a new OpenLayers feature const olGeometry = new Point(ferry.location.coordinates); - const olFeature = new ol.Feature({ geometry: olGeometry }); + const olFeature = new ol.Feature({ geometry: olGeometry, type: 'ferry'}); // Transfer properties olFeature.setProperties(ferry); - olFeature.set('type', 'ferry'); // Transform the projection const olFeatureForMap = transformFeature( diff --git a/src/frontend/src/Components/map/layers/index.js b/src/frontend/src/Components/map/layers/index.js new file mode 100644 index 000000000..7e5c57244 --- /dev/null +++ b/src/frontend/src/Components/map/layers/index.js @@ -0,0 +1,76 @@ +import { getAdvisoriesLayer } from './advisoriesLayer.js'; +import { getCamerasLayer } from './camerasLayer.js'; +import { getCurrentWeatherLayer } from './currentWeatherLayer.js'; +import { getFerriesLayer } from './ferriesLayer.js'; +import { getRegionalWeatherLayer } from './regionalWeatherLayer.js'; +import { getRestStopsLayer } from './restStopsLayer.js'; +import { getRouteLayer } from './routeLayer.js'; +import { loadEventsLayers } from './eventsLayer.js'; + +const layerFuncMap = { + advisoriesLayer: getAdvisoriesLayer, + highwayCams: getCamerasLayer, + weather: getCurrentWeatherLayer, + inlandFerries: getFerriesLayer, + regional: getRegionalWeatherLayer, + restStops: getRestStopsLayer, + routeLayer: getRouteLayer, +} + +export const loadLayer = (mapLayers, mapRef, mapContext, key, dataList, zIndex, referenceData, updateReferenceFeature) => { + // Remove layer if it already exists + if (mapLayers.current[key]) { + mapRef.current.removeLayer(mapLayers.current[key]); + } + + // Add layer if array exists + if (dataList) { + // Generate and add layer + mapLayers.current[key] = layerFuncMap[key]( + dataList, + mapRef.current.getView().getProjection().getCode(), + mapContext, + referenceData, + updateReferenceFeature + ); + + mapRef.current.addLayer(mapLayers.current[key]); + mapLayers.current[key].setZIndex(zIndex); + } +} + +export const enableReferencedLayer = (referenceData, mapContext) => { + // Do nothing if no reference data + if (!referenceData) return; + + const featureType = referenceData.type; + + // Enable layers based on reference feature type + if (featureType === 'camera') { + mapContext.visible_layers['highwayCams'] = true; + + // reference features can only be cams or events + } else { + const featureDisplayCategory = referenceData.display_category; + switch (featureDisplayCategory) { + case 'closures': + mapContext.visible_layers['closures'] = true; + mapContext.visible_layers['closuresLines'] = true; + break; + case 'majorEvents': + mapContext.visible_layers['majorEvents'] = true; + mapContext.visible_layers['majorEventsLines'] = true; + break; + case 'minorEvents': + mapContext.visible_layers['minorEvents'] = true; + mapContext.visible_layers['minorEventsLines'] = true; + break; + case 'futureEVents': + mapContext.visible_layers['futureEvents'] = true; + mapContext.visible_layers['futureEventsLines'] = true; + break; + } + } +} + +export { loadEventsLayers }; diff --git a/src/frontend/src/Components/map/layers/regionalLayer.js b/src/frontend/src/Components/map/layers/regionalWeatherLayer.js similarity index 88% rename from src/frontend/src/Components/map/layers/regionalLayer.js rename to src/frontend/src/Components/map/layers/regionalWeatherLayer.js index af3a73d2b..01b2ad29c 100644 --- a/src/frontend/src/Components/map/layers/regionalLayer.js +++ b/src/frontend/src/Components/map/layers/regionalWeatherLayer.js @@ -1,5 +1,5 @@ // Components and functions -import { transformFeature } from '../helper.js'; +import { transformFeature } from '../helpers'; // OpenLayers import { Point } from 'ol/geom'; @@ -11,7 +11,7 @@ import VectorSource from 'ol/source/Vector'; // Styling import { regionalStyles } from '../../data/featureStyleDefinitions.js'; -export function loadRegionalLayers(weatherData, mapContext, projectionCode) { +export function getRegionalWeatherLayer(weatherData, projectionCode, mapContext) { return new VectorLayer({ classname: 'regional', visible: mapContext.visible_layers.weather, @@ -29,11 +29,10 @@ export function loadRegionalLayers(weatherData, mapContext, projectionCode) { const lat = weather.location.coordinates[1]; const lng = weather.location.coordinates[0] const olGeometry = new Point([lng, lat]); - const olFeature = new ol.Feature({ geometry: olGeometry }); + const olFeature = new ol.Feature({ geometry: olGeometry, type: 'regionalWeather' }); // Transfer properties olFeature.setProperties(weather) - olFeature.set('type', 'regional'); // Transform the projection const olFeatureForMap = transformFeature( diff --git a/src/frontend/src/Components/map/layers/restStopsLayer.js b/src/frontend/src/Components/map/layers/restStopsLayer.js index 6f9711c8b..c84503631 100644 --- a/src/frontend/src/Components/map/layers/restStopsLayer.js +++ b/src/frontend/src/Components/map/layers/restStopsLayer.js @@ -1,5 +1,5 @@ // Components and functions -import { transformFeature } from '../helper.js'; +import { transformFeature } from '../helpers'; // OpenLayers import { Point } from 'ol/geom'; @@ -25,11 +25,10 @@ export function getRestStopsLayer(restStopsData, projectionCode, mapContext) { restStopsData.forEach(restStop => { // Build a new OpenLayers feature const olGeometry = new Point(restStop.location.coordinates); - const olFeature = new ol.Feature({ geometry: olGeometry }); + const olFeature = new ol.Feature({ geometry: olGeometry, type: 'restStop' }); // Transfer properties olFeature.setProperties(restStop); - olFeature.set('type', 'rest'); // Transform the projection const olFeatureForMap = transformFeature( @@ -56,7 +55,7 @@ export function getRestStopsLayer(restStopsData, projectionCode, mapContext) { else{ style = restStopStyles['static']; } - } + } olFeatureForMap.setStyle(style); vectorSource.addFeature(olFeatureForMap); }); diff --git a/src/frontend/src/Components/map/routeLayer.js b/src/frontend/src/Components/map/layers/routeLayer.js similarity index 93% rename from src/frontend/src/Components/map/routeLayer.js rename to src/frontend/src/Components/map/layers/routeLayer.js index efc70079c..a9ec6c037 100644 --- a/src/frontend/src/Components/map/routeLayer.js +++ b/src/frontend/src/Components/map/layers/routeLayer.js @@ -1,5 +1,5 @@ // Components and functions -import { transformFeature } from './helper.js'; +import { transformFeature } from '../helpers'; // OpenLayers import { Point, LineString } from 'ol/geom'; @@ -23,7 +23,7 @@ export function getRouteLayer(routeData, projectionCode) { let centroidFeatureForMap = null; olGeometry = new LineString(routeData.route); - const olFeature = new ol.Feature({ geometry: olGeometry }); + const olFeature = new ol.Feature({ geometry: olGeometry, type: 'route' }); // Transfer properties olFeature.setProperties(routeData); diff --git a/src/frontend/src/Components/map/camPopup.js b/src/frontend/src/Components/map/panels/camPopup.js similarity index 95% rename from src/frontend/src/Components/map/camPopup.js rename to src/frontend/src/Components/map/panels/camPopup.js index 088893d0a..513044b46 100644 --- a/src/frontend/src/Components/map/camPopup.js +++ b/src/frontend/src/Components/map/panels/camPopup.js @@ -6,14 +6,14 @@ import { useNavigate } from 'react-router-dom'; // Third party packages import Button from 'react-bootstrap/Button'; -import FriendlyTime from '../FriendlyTime'; +import FriendlyTime from '../../shared/FriendlyTime'; import parse from 'html-react-parser'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faVideoSlash, faVideo } from '@fortawesome/pro-solid-svg-icons'; -import { getCameraOrientation } from '../cameras/helper.js'; +import { getCameraOrientation } from '../../cameras/helper'; -import colocatedCamIcon from '../../images/colocated-camera.svg'; +import colocatedCamIcon from '../../../images/colocated-camera.svg'; import './mapPopup.scss'; export default function CamPopup(props) { diff --git a/src/frontend/src/Components/map/panels/index.js b/src/frontend/src/Components/map/panels/index.js new file mode 100644 index 000000000..a8694e828 --- /dev/null +++ b/src/frontend/src/Components/map/panels/index.js @@ -0,0 +1,49 @@ +// React +import React from 'react'; + +import { + getEventPopup, + getFerryPopup, + getWeatherPopup, + getRegionalPopup, + getRestStopPopup, +} from './mapPopup'; +import CamPopup from './camPopup'; + +export const renderPanel = (clickedFeature, isCamDetail) => { + if (clickedFeature) { + switch (clickedFeature.get('type')) { + case 'camera': + return ; + case 'event': + return getEventPopup(clickedFeature); + case 'ferry': + return getFerryPopup(clickedFeature); + case 'currentWeather': + return getWeatherPopup(clickedFeature); + case 'regionalWeather': + return getRegionalPopup(clickedFeature); + case 'restStop': + return getRestStopPopup(clickedFeature); + } + } +} + +export const maximizePanel = (panelRef) => { + if (panelRef.current.classList.contains('open')) { + if (!panelRef.current.classList.contains('maximized')) { + panelRef.current.classList.add('maximized'); + + } else { + panelRef.current.classList.remove('maximized'); + } + } +} + +export const togglePanel = (panelRef, resetClickedStates, clickedFeatureRef, updateClickedFeature) => { + panelRef.current.classList.toggle('open'); + panelRef.current.classList.remove('maximized'); + if (!panelRef.current.classList.contains('open')) { + resetClickedStates(null, clickedFeatureRef, updateClickedFeature); + } +} diff --git a/src/frontend/src/Components/map/mapPopup.js b/src/frontend/src/Components/map/panels/mapPopup.js similarity index 99% rename from src/frontend/src/Components/map/mapPopup.js rename to src/frontend/src/Components/map/panels/mapPopup.js index 19a411d66..a6128c8a1 100644 --- a/src/frontend/src/Components/map/mapPopup.js +++ b/src/frontend/src/Components/map/panels/mapPopup.js @@ -2,9 +2,9 @@ import React from 'react'; // Third party packages -import EventTypeIcon from '../EventTypeIcon'; +import EventTypeIcon from '../../events/EventTypeIcon'; import RestStopTypeIcon from '../RestStopTypeIcon'; -import FriendlyTime from '../FriendlyTime'; +import FriendlyTime from '../../shared/FriendlyTime'; import parse from 'html-react-parser'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { @@ -17,12 +17,8 @@ import { faEye, faTriangleExclamation, faToilet, - faBath, - faRestroom, faClock, faDoorOpen, - faTruck, - faTable, faWifi, faRoad, faChargingStation, @@ -38,7 +34,7 @@ import WeatherIcon from '../WeatherIcon'; import Tooltip from 'react-bootstrap/Tooltip'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import OpenSeason from '../OpenSeason'; -import { isRestStopClosed } from '../data/restStops'; +import { isRestStopClosed } from '../../data/restStops'; function convertCategory(event) { switch (event.display_category) { diff --git a/src/frontend/src/Components/map/mapPopup.scss b/src/frontend/src/Components/map/panels/mapPopup.scss similarity index 99% rename from src/frontend/src/Components/map/mapPopup.scss rename to src/frontend/src/Components/map/panels/mapPopup.scss index 093294fdd..f89a21060 100644 --- a/src/frontend/src/Components/map/mapPopup.scss +++ b/src/frontend/src/Components/map/panels/mapPopup.scss @@ -1,4 +1,4 @@ -@import "../../styles/variables"; +@import "../../../styles/variables"; .popup { &__title { @@ -138,7 +138,7 @@ .camera-orientations { padding: 0 1rem; } - + } //Ferries layer @@ -264,7 +264,7 @@ &:hover { color: $Type-Link; } - } + } } } @@ -297,7 +297,7 @@ &:hover { color: $Type-Link; } - } + } } } } @@ -499,7 +499,7 @@ .popup__title { background-color: #E8EAF4; border-top: 4px solid #273F94; - + &__icon { background: #273F94; } @@ -508,7 +508,7 @@ color: #273F94; } } - + .popup__content { &__title { @@ -521,7 +521,7 @@ text-transform: capitalize; } } - + .location { color: $Type-Secondary; font-size: 0.875rem; @@ -548,7 +548,7 @@ margin-right: 20px; color: $BC-Blue; } - + p { margin-bottom: 0; font-size: 0.875rem; @@ -633,4 +633,3 @@ } } } - diff --git a/src/frontend/src/Components/map/LocationSearch.js b/src/frontend/src/Components/routing/LocationSearch.js similarity index 98% rename from src/frontend/src/Components/map/LocationSearch.js rename to src/frontend/src/Components/routing/LocationSearch.js index 6388fb311..938f0eb7d 100644 --- a/src/frontend/src/Components/map/LocationSearch.js +++ b/src/frontend/src/Components/routing/LocationSearch.js @@ -8,7 +8,7 @@ import 'react-bootstrap-typeahead/css/Typeahead.css'; // Components and functions import { getLocations } from '../data/locations.js'; -import trackEvent from '../TrackEvent.js'; +import trackEvent from '../shared/TrackEvent.js'; // Styling import './LocationSearch.scss'; diff --git a/src/frontend/src/Components/map/LocationSearch.scss b/src/frontend/src/Components/routing/LocationSearch.scss similarity index 100% rename from src/frontend/src/Components/map/LocationSearch.scss rename to src/frontend/src/Components/routing/LocationSearch.scss diff --git a/src/frontend/src/Components/map/RouteSearch.js b/src/frontend/src/Components/routing/RouteSearch.js similarity index 100% rename from src/frontend/src/Components/map/RouteSearch.js rename to src/frontend/src/Components/routing/RouteSearch.js diff --git a/src/frontend/src/Components/map/RouteSearch.scss b/src/frontend/src/Components/routing/RouteSearch.scss similarity index 100% rename from src/frontend/src/Components/map/RouteSearch.scss rename to src/frontend/src/Components/routing/RouteSearch.scss diff --git a/src/frontend/src/Components/advisories/ExitSurvey.js b/src/frontend/src/Components/shared/ExitSurvey.js similarity index 100% rename from src/frontend/src/Components/advisories/ExitSurvey.js rename to src/frontend/src/Components/shared/ExitSurvey.js diff --git a/src/frontend/src/Components/advisories/ExitSurvey.scss b/src/frontend/src/Components/shared/ExitSurvey.scss similarity index 99% rename from src/frontend/src/Components/advisories/ExitSurvey.scss rename to src/frontend/src/Components/shared/ExitSurvey.scss index 9c0ac8626..575d3d85f 100644 --- a/src/frontend/src/Components/advisories/ExitSurvey.scss +++ b/src/frontend/src/Components/shared/ExitSurvey.scss @@ -12,7 +12,7 @@ border-radius: 4px; box-shadow: 0px 1.937px 4.358px 0px rgba(0, 0, 0, 0.13), 0px 0.363px 1.089px 0px rgba(0, 0, 0, 0.10); z-index: 22; - + &.mobile { position: relative; bottom: inherit; diff --git a/src/frontend/src/Components/shared/Filters.js b/src/frontend/src/Components/shared/Filters.js new file mode 100644 index 000000000..2751392c6 --- /dev/null +++ b/src/frontend/src/Components/shared/Filters.js @@ -0,0 +1,390 @@ +// React +import React, { useState, useContext } from 'react'; + +// Third party packages +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faFilter, + faXmark, + faMinusCircle, + faCalendarDays, + faVideo, + faFerry, + faSunCloud, +} from '@fortawesome/pro-solid-svg-icons'; +import Button from 'react-bootstrap/Button'; +import Tooltip from 'react-bootstrap/Tooltip'; +import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; +import {useMediaQuery} from '@uidotdev/usehooks'; +import trackEvent from './TrackEvent'; + +// Components and functions +import { MapContext } from '../../App.js'; + +// Styling +import './Filters.scss'; + +export default function Filters(props) { + // Misc + const largeScreen = useMediaQuery('only screen and (min-width : 768px)'); + + // Context + const { mapContext, setMapContext } = useContext(MapContext); + + // Props + const { + mapLayers, + disableFeatures, + enableRoadConditions, + textOverride, + isCamDetail, + referenceData + } = props; + + // Const for enabling layer that the reference event belongs to + const eventCategory = referenceData ? referenceData.display_category : false; + + // States + // Show layer menu by default on main page, desktop only + const [open, setOpen] = useState(largeScreen && !textOverride); + + const tooltipClosures = ( + +

Travel is not possible in one or both directions on this road. Find an alternate route or a detour where possible.

+
+ ); + + const tooltipMajor = ( + +

Expect delays of at least 30 minutes or more on this road. This could be due to a traffic incident, road work, or construction.

+
+ ); + + const tooltipMinor = ( + +

Expect delays up to 30 minutes on this road. This could be due to a traffic incident, road work, or construction.

+
+ ); + + const tooltipFutureevents = ( + +

Future road work or construction is planned for this road.

+
+ ); + + const tooltipHighwaycameras = ( + +

Look at recent pictures from cameras near the highway.

+
+ ); + + const tooltipRoadconditions = ( + +

States of the road that may impact drivability.

+
+ ); + + const tooltipInlandferries = ( + +

Travel requires the use of an inland ferry.

+
+ ); + const tooltipWeather = ( + +

Weather updates for roads.

+
+ ); + const tooltipRestStops = ( + +

Travel requires the use of a rest stop.

+
+ ); + + // States for toggles + const [closures, setClosures] = useState(eventCategory && eventCategory == 'closures' ? true : mapContext.visible_layers.closures); + const [majorEvents, setMajorEvents] = useState(eventCategory && eventCategory == 'majorEvents' ? true : mapContext.visible_layers.majorEvents); + const [minorEvents, setMinorEvents] = useState(eventCategory && eventCategory == 'minorEvents' ? true : mapContext.visible_layers.minorEvents); + const [futureEvents, setFutureEvents] = useState(eventCategory && eventCategory == 'futureEvents' ? true : mapContext.visible_layers.futureEvents); + const [roadConditions, setRoadConditions] = useState(mapContext.visible_layers.roadConditions); + const [highwayCams, setHighwayCams] = useState(isCamDetail ? isCamDetail : mapContext.visible_layers.highwayCams); + const [inlandFerries, setInlandFerries] = useState(mapContext.visible_layers.inlandFerries); + const [weather, setWeather] = useState(mapContext.visible_layers.weather); + const [restStops, setRestStops] = useState(mapContext.visible_layers.restStops); + + // Helpers + const toggleLayer = (layer, checked) => { + mapLayers.current[layer].setVisible(checked); + + // Set context and local storage + mapContext.visible_layers[layer] = checked; + setMapContext(mapContext); + localStorage.setItem('mapContext', JSON.stringify(mapContext)); + } + + return ( +
+ + + {open && +
+

Filters

+ + +
+
+

Delays

+
+
+
+ { + trackEvent('click', 'map', 'Toggle closures layer') + toggleLayer('closures', e.target.checked); + toggleLayer('closuresLines', e.target.checked); + setClosures(!closures) + }} + defaultChecked={eventCategory && eventCategory == 'closures' ? true : mapContext.visible_layers.closures} + /> + + + + ? + +
+ +
+ { + trackEvent('click', 'map', 'Toggle major events layer'); + toggleLayer('majorEvents', e.target.checked); + toggleLayer('majorEventsLines', e.target.checked); + setMajorEvents(!majorEvents); + }} + defaultChecked={eventCategory && eventCategory == 'majorEvents' ? true : mapContext.visible_layers.majorEvents} + /> + + + ? + +
+ +
+ { + trackEvent('click', 'map', 'Toggle minor events layer') + toggleLayer('minorEvents', e.target.checked); + toggleLayer('minorEventsLines', e.target.checked); + setMinorEvents(!minorEvents); + }} + defaultChecked={eventCategory && eventCategory == 'minorEvents' ? true : mapContext.visible_layers.minorEvents} + /> + + + ? + +
+ +
+ { + trackEvent('click', 'map', 'Toggle future events layer') + toggleLayer('futureEvents', e.target.checked); + toggleLayer('futureEventsLines', e.target.checked); + setFutureEvents(!futureEvents); + }} + defaultChecked={eventCategory && eventCategory == 'futureEvents' ? true : mapContext.visible_layers.futureEvents} + /> + + + ? + +
+
+
+
+ +
+

Conditions and features

+
+
+
+ { + trackEvent('click', 'map', 'Toggle highway cameras layer'); + toggleLayer('highwayCams', e.target.checked); + setHighwayCams(!highwayCams); + }} + defaultChecked={isCamDetail || mapContext.visible_layers.highwayCams} + disabled={isCamDetail || disableFeatures} + /> + + + ? + +
+ +
+ { + trackEvent('click', 'map', 'Toggle road conditions layer') + toggleLayer('roadConditions', e.target.checked); + toggleLayer('roadConditionsLines', e.target.checked); + setRoadConditions(!roadConditions); + }} + defaultChecked={mapContext.visible_layers.roadConditions} + disabled={(disableFeatures && !enableRoadConditions)} + /> + + + ? + +
+ +
+ { + trackEvent('click', 'map', 'Toggle inland ferries layer') + toggleLayer('inlandFerries', e.target.checked); setInlandFerries(!inlandFerries)}} + defaultChecked={mapContext.visible_layers.inlandFerries} + disabled={disableFeatures} + /> + + + ? + +
+ +
+ { + trackEvent('click', 'map', 'Toggle weather layer') + toggleLayer('weather', e.target.checked); + toggleLayer('regional', e.target.checked); + setWeather(!weather)} + } + defaultChecked={mapContext.visible_layers.weather} + disabled={disableFeatures} + /> + + + ? + +
+
+ { + trackEvent('click', 'map', 'Toggle rest stops layer') + toggleLayer('restStops', e.target.checked); setRestStops(!restStops)}} + defaultChecked={mapContext.visible_layers.restStops} + disabled={disableFeatures} + /> + + + ? + +
+
+
+
+
+
+ } +
+ ); +} diff --git a/src/frontend/src/Components/Filters.scss b/src/frontend/src/Components/shared/Filters.scss similarity index 97% rename from src/frontend/src/Components/Filters.scss rename to src/frontend/src/Components/shared/Filters.scss index 40fb961ab..1889a34cb 100644 --- a/src/frontend/src/Components/Filters.scss +++ b/src/frontend/src/Components/shared/Filters.scss @@ -1,4 +1,4 @@ -@import "../styles/variables.scss"; +@import "../../styles/variables.scss"; button.btn.open-filters { svg { @@ -162,7 +162,7 @@ button.btn.open-filters { &:focus + label { text-decoration: underline; - outline: 2px solid #2E5DD7; + outline: 2px solid #2E5DD7; } } diff --git a/src/frontend/src/Components/FriendlyTime.js b/src/frontend/src/Components/shared/FriendlyTime.js similarity index 100% rename from src/frontend/src/Components/FriendlyTime.js rename to src/frontend/src/Components/shared/FriendlyTime.js diff --git a/src/frontend/src/Components/FriendlyTime.scss b/src/frontend/src/Components/shared/FriendlyTime.scss similarity index 95% rename from src/frontend/src/Components/FriendlyTime.scss rename to src/frontend/src/Components/shared/FriendlyTime.scss index 2f2809111..6a230e8ab 100644 --- a/src/frontend/src/Components/FriendlyTime.scss +++ b/src/frontend/src/Components/shared/FriendlyTime.scss @@ -1,4 +1,4 @@ -@import "../styles/variables.scss"; +@import "../../styles/variables.scss"; .friendly-time { text-decoration: underline; @@ -45,4 +45,4 @@ .friendly-time-text { margin-bottom: 0; -} \ No newline at end of file +} diff --git a/src/frontend/src/Components/ScrollToTop.js b/src/frontend/src/Components/shared/ScrollToTop.js similarity index 100% rename from src/frontend/src/Components/ScrollToTop.js rename to src/frontend/src/Components/shared/ScrollToTop.js diff --git a/src/frontend/src/Components/SocialSharing.js b/src/frontend/src/Components/shared/SocialSharing.js similarity index 99% rename from src/frontend/src/Components/SocialSharing.js rename to src/frontend/src/Components/shared/SocialSharing.js index c48b22b6f..c03feaea1 100644 --- a/src/frontend/src/Components/SocialSharing.js +++ b/src/frontend/src/Components/shared/SocialSharing.js @@ -35,4 +35,4 @@ export default function SocialSharing() {
); -} \ No newline at end of file +} diff --git a/src/frontend/src/Components/TrackEvent.js b/src/frontend/src/Components/shared/TrackEvent.js similarity index 100% rename from src/frontend/src/Components/TrackEvent.js rename to src/frontend/src/Components/shared/TrackEvent.js diff --git a/src/frontend/src/Header.js b/src/frontend/src/Header.js index ba9bebf07..93829e259 100644 --- a/src/frontend/src/Header.js +++ b/src/frontend/src/Header.js @@ -2,13 +2,12 @@ import React, { useState } from "react"; // Third party packages -import {LinkContainer} from 'react-router-bootstrap'; +import { faComment } from '@fortawesome/pro-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { LinkContainer } from 'react-router-bootstrap'; import Container from 'react-bootstrap/Container'; import Nav from 'react-bootstrap/Nav'; import Navbar from 'react-bootstrap/Navbar'; -import Button from 'react-bootstrap/Button'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faComment} from '@fortawesome/pro-solid-svg-icons'; // Static files import logo from './images/dbc-logo-beta.svg'; diff --git a/src/frontend/src/expireReducer.js b/src/frontend/src/expireReducer.js index eef2a50cc..0327c1a58 100644 --- a/src/frontend/src/expireReducer.js +++ b/src/frontend/src/expireReducer.js @@ -5,11 +5,6 @@ const { createTransform } = require('redux-persist'); const transformRehydrate = (outboundState, config) => { outboundState = outboundState || null; - // Temporary fix for the issue with expiry - if (config.expiredState) { - return config.expiredState; - } - // Check for the possible expiry if state has the persisted date if (config.expireSeconds && outboundState.timeStamp) { const startTime = new Date(outboundState.timeStamp).getTime(); diff --git a/src/frontend/src/index.js b/src/frontend/src/index.js index d0e5429e5..49eb84368 100644 --- a/src/frontend/src/index.js +++ b/src/frontend/src/index.js @@ -69,8 +69,6 @@ if (!window.location.hash) { } // - - root.render( diff --git a/src/frontend/src/pages/AdvisoryDetailsPage.js b/src/frontend/src/pages/AdvisoryDetailsPage.js index 46c668e5f..a6a83c6eb 100644 --- a/src/frontend/src/pages/AdvisoryDetailsPage.js +++ b/src/frontend/src/pages/AdvisoryDetailsPage.js @@ -19,7 +19,7 @@ import { NetworkError, ServerError } from '../Components/data/helper'; import NetworkErrorPopup from '../Components//map/errors/NetworkError'; import ServerErrorPopup from '../Components//map/errors/ServerError'; import Footer from '../Footer'; -import FriendlyTime from '../Components/FriendlyTime'; +import FriendlyTime from '../Components/shared/FriendlyTime'; // Styling import './AdvisoryDetailsPage.scss'; diff --git a/src/frontend/src/pages/BulletinDetailsPage.js b/src/frontend/src/pages/BulletinDetailsPage.js index 75ccf891e..f66aee158 100644 --- a/src/frontend/src/pages/BulletinDetailsPage.js +++ b/src/frontend/src/pages/BulletinDetailsPage.js @@ -12,7 +12,7 @@ import { NetworkError, ServerError } from '../Components/data/helper'; import NetworkErrorPopup from '../Components//map/errors/NetworkError'; import ServerErrorPopup from '../Components//map/errors/ServerError'; import Footer from '../Footer.js'; -import FriendlyTime from '../Components/FriendlyTime'; +import FriendlyTime from '../Components/shared/FriendlyTime'; // Styling import './BulletinDetailsPage.scss'; diff --git a/src/frontend/src/pages/CameraDetailsPage.js b/src/frontend/src/pages/CameraDetailsPage.js index 931c50e37..c02dbb102 100644 --- a/src/frontend/src/pages/CameraDetailsPage.js +++ b/src/frontend/src/pages/CameraDetailsPage.js @@ -34,16 +34,16 @@ import { getWebcamReplay } from '../Components/data/webcams'; import { NetworkError, ServerError } from '../Components/data/helper'; import NetworkErrorPopup from '../Components//map/errors/NetworkError'; import ServerErrorPopup from '../Components//map/errors/ServerError'; -import Map from '../Components/Map.js'; +import MapWrapper from '../Components/map/MapWrapper'; import Footer from '../Footer.js'; -import FriendlyTime from '../Components/FriendlyTime'; -import highwayShield from '../Components/highwayShield'; -import CurrentCameraIcon from '../Components/CurrentCameraIcon'; +import FriendlyTime from '../Components/shared/FriendlyTime'; +import highwayShield from '../Components/cameras/highwayShield'; +import CurrentCameraIcon from '../Components/cameras/CurrentCameraIcon'; import { getCameraOrientation } from '../Components/cameras/helper.js'; // Styling import './CameraDetailsPage.scss'; -import '../Components/Map.scss'; +import '../Components/map/Map.scss'; import colocatedCamIcon from '../images/colocated-camera.svg'; @@ -140,8 +140,14 @@ export default function CameraDetailsPage() { }; const mapViewRoute = () => { - navigate('/', { state: camera }); - }; + const refCamData = { ...camera }; + refCamData.type = 'camera'; + + // Remove geometry from reference data since it can't be serialized + refCamData.geometry = null; + + navigate("/", { state: refCamData }); + } // ReplayTheDay const refImg = useRef(null); @@ -510,12 +516,7 @@ export default function CameraDetailsPage() {
- +
diff --git a/src/frontend/src/pages/CamerasListPage.js b/src/frontend/src/pages/CamerasListPage.js index 04acbf7fd..68b86f6c3 100644 --- a/src/frontend/src/pages/CamerasListPage.js +++ b/src/frontend/src/pages/CamerasListPage.js @@ -10,14 +10,13 @@ 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 { compareRoutePoints, filterByRoute -} from '../Components/map/helper'; +} from '../Components/map/helpers'; import { getAdvisories } from '../Components/data/advisories'; import { collator, getCameras, addCameraGroups } from '../Components/data/webcams'; import { NetworkError, ServerError } from '../Components/data/helper'; @@ -27,8 +26,9 @@ import Advisories from '../Components/advisories/Advisories'; import CameraList from '../Components/cameras/CameraList'; import Footer from '../Footer'; import PageHeader from '../PageHeader'; -import RouteSearch from '../Components/map/RouteSearch'; -import trackEvent from '../Components/TrackEvent.js'; +import RouteSearch from '../Components/routing/RouteSearch'; +import trackEvent from '../Components/shared/TrackEvent.js'; + // Styling import './CamerasListPage.scss'; @@ -235,7 +235,7 @@ export default function CamerasListPage() { - + {!(displayedCameras && displayedCameras.length) && diff --git a/src/frontend/src/pages/EventsListPage.js b/src/frontend/src/pages/EventsListPage.js index cac15da5a..ab75f8a12 100644 --- a/src/frontend/src/pages/EventsListPage.js +++ b/src/frontend/src/pages/EventsListPage.js @@ -12,21 +12,17 @@ import { updateEvents } from '../slices/feedsSlice'; import { booleanIntersects, point, lineString, polygon } from '@turf/turf'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { - faAngleDown, - faMapLocationDot + faAngleDown } from '@fortawesome/pro-solid-svg-icons'; import { useMediaQuery } from '@uidotdev/usehooks'; -import Button from 'react-bootstrap/Button'; import Container from 'react-bootstrap/Container'; import Dropdown from 'react-bootstrap/Dropdown'; -import DropdownButton from 'react-bootstrap/DropdownButton'; -import InfiniteScroll from 'react-infinite-scroll-component'; // Internal imports import { compareRoutePoints, filterByRoute, -} from '../Components/map/helper'; +} from '../Components/map/helpers'; import { getAdvisories } from '../Components/data/advisories'; import { getEvents } from '../Components/data/events'; import { MapContext } from '../App.js'; @@ -37,16 +33,16 @@ import ServerErrorPopup from '../Components//map/errors/ServerError'; import Advisories from '../Components/advisories/Advisories'; import EventCard from '../Components/events/EventCard'; import EventsTable from '../Components/events/EventsTable'; -import EventTypeIcon from '../Components/EventTypeIcon'; -import Filters from '../Components/Filters.js'; +import EventTypeIcon from '../Components/events/EventTypeIcon'; +import Filters from '../Components/shared/Filters.js'; import Footer from '../Footer.js'; -import FriendlyTime from '../Components/FriendlyTime'; import PageHeader from '../PageHeader'; -import RouteSearch from '../Components/map/RouteSearch'; -import trackEvent from '../Components/TrackEvent.js'; +import RouteSearch from '../Components/routing/RouteSearch'; +import trackEvent from '../Components/shared/TrackEvent.js'; + // Styling import './EventsListPage.scss'; -import '../Components/Filters.scss'; +import '../Components/shared/Filters.scss'; // Helpers const sortEvents = (events, key) => { @@ -243,7 +239,11 @@ export default function EventsListPage() { const handleRoute = (event) => { trackEvent('click', 'event', 'events list page', event.event_type, event.event_sub_type); - navigate('/', {state: event}); + + const refEventData = { ...event }; + refEventData.type = 'event'; + + navigate('/', { state: refEventData }); }; const sortHandler = (e, key) => { diff --git a/src/frontend/src/pages/FeedbackPage.js b/src/frontend/src/pages/FeedbackPage.js index ccbaa2576..487f5653d 100644 --- a/src/frontend/src/pages/FeedbackPage.js +++ b/src/frontend/src/pages/FeedbackPage.js @@ -2,11 +2,10 @@ import React, { useCallback, useEffect, useState } from 'react'; // External Components -import { GoogleReCaptcha } from 'react-google-recaptcha-v3'; +import { useGoogleReCaptcha } from 'react-google-recaptcha-v3' import Button from 'react-bootstrap/Button'; import Container from 'react-bootstrap/Container'; import Form from 'react-bootstrap/Form'; -import { useGoogleReCaptcha } from 'react-google-recaptcha-v3' // Styling import './FeedbackPage.scss'; diff --git a/src/frontend/src/pages/MapPage.js b/src/frontend/src/pages/MapPage.js index 5cae4b09b..998ff96c5 100644 --- a/src/frontend/src/pages/MapPage.js +++ b/src/frontend/src/pages/MapPage.js @@ -7,10 +7,10 @@ import { DndProvider } from 'react-dnd-multi-backend'; import { HTML5toTouch } from 'rdndmb-html5-to-touch'; // Components and functions -import Map from '../Components/Map.js'; +import MapWrapper from '../Components/map/MapWrapper.js'; // Styling -import '../Components/Map.scss'; +import '../Components/map/Map.scss'; export default function MapPage() { const { state } = useLocation(); @@ -20,7 +20,7 @@ export default function MapPage() { return (
- +
); diff --git a/src/frontend/src/slices/index.js b/src/frontend/src/slices/index.js new file mode 100644 index 000000000..d57283039 --- /dev/null +++ b/src/frontend/src/slices/index.js @@ -0,0 +1,26 @@ +import { updateAdvisories, updateBulletins } from './cmsSlice'; +import { + updateCameras, + updateEvents, + updateFerries, + updateRegional, + updateRestStops, + updateWeather +} from './feedsSlice'; +import { updateMapState } from './mapSlice'; +import { updateSelectedRoute, updateSearchLocationFrom, updateSearchLocationTo } from './routesSlice'; + +export { + updateAdvisories, + updateBulletins, + updateCameras, + updateEvents, + updateFerries, + updateMapState, + updateRegional, + updateRestStops, + updateSearchLocationFrom, + updateSearchLocationTo, + updateSelectedRoute, + updateWeather +}; diff --git a/src/frontend/src/store.js b/src/frontend/src/store.js index da6fe3151..c43979d54 100644 --- a/src/frontend/src/store.js +++ b/src/frontend/src/store.js @@ -2,13 +2,7 @@ import { configureStore } from '@reduxjs/toolkit'; import { persistStore, - persistReducer, - FLUSH, - REHYDRATE, - PAUSE, - PERSIST, - PURGE, - REGISTER, + persistReducer } from 'redux-persist'; import localforage from 'localforage'; @@ -50,9 +44,9 @@ const store = configureStore({ }, middleware: getDefaultMiddleware => getDefaultMiddleware({ - serializableCheck: { - ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], - }, + // Disable checks to prevent rendering lag in dev mode + serializableCheck: false, + immutableCheck: false, }), });