@@ -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(