From b6d3e5cbd1d91b7a18b664654e208fc1e9061dd5 Mon Sep 17 00:00:00 2001 From: ray Date: Mon, 18 Dec 2023 16:22:43 -0800 Subject: [PATCH] DBC22-1265: commented unused tooltips DBC22-1265: disable road conditions in events list page DBC22-904: refactoring, events filters now use context DBC22-1429: fixed zoom button references DBC22-1266: updated map icons DBC22-1266: fixed click and hover handlers DBC22-1266: layers panel styling fix DBC22-1266: layer toggling and zindex fixes DBC22-1266: reworked event layers DBC22-1265: replaced event_type with display_category for display logic --- src/backend/apps/event/enums.py | 28 ++ src/backend/apps/event/serializers.py | 22 +- src/frontend/.eslintrc.js | 3 +- src/frontend/src/App.js | 8 +- src/frontend/src/Components/EventTypeIcon.js | 25 +- src/frontend/src/Components/Layers.js | 102 ++++--- src/frontend/src/Components/Layers.scss | 13 +- src/frontend/src/Components/Map.js | 271 ++++++++---------- src/frontend/src/Components/map/helper.js | 28 +- .../src/Components/map/layers/camerasLayer.js | 3 +- .../src/Components/map/layers/eventsLayer.js | 207 +++++++------ .../src/Components/map/layers/ferriesLayer.js | 3 +- src/frontend/src/Components/map/mapPopup.js | 4 +- src/frontend/src/pages/EventsPage.js | 81 +++--- 14 files changed, 448 insertions(+), 350 deletions(-) diff --git a/src/backend/apps/event/enums.py b/src/backend/apps/event/enums.py index 1ae543acc..a9a39c9f4 100644 --- a/src/backend/apps/event/enums.py +++ b/src/backend/apps/event/enums.py @@ -3,6 +3,7 @@ class EVENT_TYPE: INCIDENT = "INCIDENT" SPECIAL_EVENT = "SPECIAL_EVENT" WEATHER_CONDITION = "WEATHER_CONDITION" + ROAD_CONDITION = "ROAD_CONDITION" EVENT_TYPE_CHOICES = ( @@ -10,6 +11,7 @@ class EVENT_TYPE: (EVENT_TYPE.INCIDENT, "Incident"), (EVENT_TYPE.SPECIAL_EVENT, "Special event"), (EVENT_TYPE.WEATHER_CONDITION, "Weather condition"), + (EVENT_TYPE.ROAD_CONDITION, "Road condition"), ) @@ -20,6 +22,10 @@ class EVENT_SUB_TYPE: ROAD_CONSTRUCTION = "ROAD_CONSTRUCTION" ROAD_MAINTENANCE = "ROAD_MAINTENANCE" PARTLY_ICY = "PARTLY_ICY" + ICE_COVERED = "ICE_COVERED" + SNOW_PACKED = "SNOW_PACKED" + PARTLY_SNOW_PACKED = "PARTLY_SNOW_PACKED" + MUD = "MUD" PLANNED_EVENT = "PLANNED_EVENT" POOR_VISIBILITY = "POOR_VISIBILITY" @@ -31,6 +37,10 @@ class EVENT_SUB_TYPE: (EVENT_SUB_TYPE.ROAD_CONSTRUCTION, "Road construction"), (EVENT_SUB_TYPE.ROAD_MAINTENANCE, "Road maintenance"), (EVENT_SUB_TYPE.PARTLY_ICY, "Partly Icy"), + (EVENT_SUB_TYPE.ICE_COVERED, "Ice Covered"), + (EVENT_SUB_TYPE.SNOW_PACKED, "Snow Packed"), + (EVENT_SUB_TYPE.PARTLY_SNOW_PACKED, "Partly Snow Packed"), + (EVENT_SUB_TYPE.MUD, "Mud"), (EVENT_SUB_TYPE.PLANNED_EVENT, "Planned event"), (EVENT_SUB_TYPE.POOR_VISIBILITY, "Poor visibility"), ) @@ -86,3 +96,21 @@ class EVENT_DIRECTION: EVENT_DIFF_FIELDS = [ 'last_updated' ] + + +class EVENT_DISPLAY_CATEGORY: + MAJOR_DELAYS = 'majorEvents' + MINOR_DELAYS = 'minorEvents' + FUTURE_DELAYS = 'futureEvents' + ROAD_CONDITION = 'roadConditions' + HIGHWAY_CAMERAS = 'highwayCams' + + +EVENT_DISPLAY_CATEGORY_MAP = { + # Only road conditions are directly mapped currently + EVENT_SUB_TYPE.POOR_VISIBILITY: EVENT_DISPLAY_CATEGORY.ROAD_CONDITION, + EVENT_SUB_TYPE.PARTLY_ICY: EVENT_DISPLAY_CATEGORY.ROAD_CONDITION, + EVENT_SUB_TYPE.ICE_COVERED: EVENT_DISPLAY_CATEGORY.ROAD_CONDITION, + EVENT_SUB_TYPE.SNOW_PACKED: EVENT_DISPLAY_CATEGORY.ROAD_CONDITION, + EVENT_SUB_TYPE.PARTLY_SNOW_PACKED: EVENT_DISPLAY_CATEGORY.ROAD_CONDITION, +} diff --git a/src/backend/apps/event/serializers.py b/src/backend/apps/event/serializers.py index ec7d106e2..e101ac766 100644 --- a/src/backend/apps/event/serializers.py +++ b/src/backend/apps/event/serializers.py @@ -1,4 +1,12 @@ -from apps.event.enums import EVENT_DIRECTION_DISPLAY +import datetime + +import pytz +from apps.event.enums import ( + EVENT_DIRECTION_DISPLAY, + EVENT_DISPLAY_CATEGORY, + EVENT_DISPLAY_CATEGORY_MAP, + EVENT_SEVERITY, +) from apps.event.models import Event from rest_framework import serializers @@ -11,6 +19,7 @@ class ScheduleSerializer(serializers.Serializer): class EventSerializer(serializers.ModelSerializer): + display_category = serializers.SerializerMethodField() direction_display = serializers.SerializerMethodField() route_display = serializers.SerializerMethodField() schedule = ScheduleSerializer() @@ -41,3 +50,14 @@ def get_route_display(self, obj): res += " to " + obj.route_to return res + + def get_display_category(self, obj): + if obj.event_sub_type in EVENT_DISPLAY_CATEGORY_MAP: + return EVENT_DISPLAY_CATEGORY_MAP[obj.event_sub_type] + + if obj.start and datetime.datetime.now(pytz.utc) < obj.start: + return EVENT_DISPLAY_CATEGORY.FUTURE_DELAYS + + return EVENT_DISPLAY_CATEGORY.MAJOR_DELAYS \ + if obj.severity == EVENT_SEVERITY.MAJOR \ + else EVENT_DISPLAY_CATEGORY.MINOR_DELAYS diff --git a/src/frontend/.eslintrc.js b/src/frontend/.eslintrc.js index 9724dce3c..0f8aa9d63 100644 --- a/src/frontend/.eslintrc.js +++ b/src/frontend/.eslintrc.js @@ -41,6 +41,7 @@ module.exports = { 'FunctionExpression': false, }, }], - "react/prop-types": "off" + "react/prop-types": "off", + 'camelcase': 'off', }, }; diff --git a/src/frontend/src/App.js b/src/frontend/src/App.js index 2db6d0976..0728b013a 100644 --- a/src/frontend/src/App.js +++ b/src/frontend/src/App.js @@ -27,8 +27,12 @@ function App() { ? JSON.parse(context) : { visible_layers: { - eventsLayer: true, - webcamsLayer: true, + majorEvents: true, + minorEvents: true, + futureEvents: true, + roadConditions: true, + highwayCams: true, + inlandFerries: true, }, }; } diff --git a/src/frontend/src/Components/EventTypeIcon.js b/src/frontend/src/Components/EventTypeIcon.js index 45c4e2b22..26e4db37d 100644 --- a/src/frontend/src/Components/EventTypeIcon.js +++ b/src/frontend/src/Components/EventTypeIcon.js @@ -4,23 +4,24 @@ import React from 'react'; // Third Party packages import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { - faTriangleExclamation, + faExclamationTriangle, + faExclamationCircle, faPersonDigging, faCalendarDays, faSnowflake, } from "@fortawesome/free-solid-svg-icons"; -export default function EventTypeIcon({eventType}) { - switch(eventType) { - case "incident": - return ; - case "construction": - return ; - case "special_event": - return ; - case "weather_condition": - return ; +export default function EventTypeIcon({ displayCategory }) { + switch (displayCategory) { + case "majorEvents": + return ; + case "minorEvents": + return ; + case "futureEvents": + return ; + case "roadConditions": + return ; default: - return ; + return ; } } diff --git a/src/frontend/src/Components/Layers.js b/src/frontend/src/Components/Layers.js index 03226ac23..7c43fcedf 100644 --- a/src/frontend/src/Components/Layers.js +++ b/src/frontend/src/Components/Layers.js @@ -10,8 +10,9 @@ import { faVideo, faSnowflake, faFerry, - faRestroom, - faCloudSun } from '@fortawesome/free-solid-svg-icons'; +// faRestroom, +// faCloudSun +} from '@fortawesome/free-solid-svg-icons'; import Button from 'react-bootstrap/Button'; import Tooltip from 'react-bootstrap/Tooltip'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; @@ -26,7 +27,7 @@ import './Layers.scss'; export default function Layers({ open, setLayersOpen, toggleLayer }) { const { mapContext } = useContext(MapContext); - + const tooltipMajor = (

Indicates a significant delay of more than 20 minutes to travel in at least one direction on this road. A Major Delay may be a traffic incident or a road event (such as road work, construction, or restoration).

@@ -63,36 +64,25 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) {
); - const tooltipReststops = ( - -

Shows the location of Provincial Rest Stops on highways throughout the province.

-
- ); +// const tooltipReststops = ( +// +//

Shows the location of Provincial Rest Stops on highways throughout the province.

+//
+// ); - const tooltipWeather = ( - -

Regional current and forecasted weather from Environment Canada for this area.

-
- ); - - - // States for events - const [filterChecked, setFilterChecked] = useState(false); - const toggleChecked = () => { - setFilterChecked(!filterChecked); - }; +// const tooltipWeather = ( +// +//

Regional current and forecasted weather from Environment Canada for this area.

+//
+// ); - // States for cameras - const [filterCheckedCams, setFilterCheckedCams] = useState(false); - const toggleCheckedCams = () => { - setFilterCheckedCams(!filterCheckedCams); - }; - - // States for ferries - const [filterCheckedFerries, setFilterCheckedFerries] = useState(false); - const toggleCheckedFerries = () => { - setFilterCheckedFerries(!filterCheckedFerries); - }; + // States for toggles + 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 largeScreen = useMediaQuery('only screen and (min-width : 768px)'); @@ -108,8 +98,8 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) { Filters - - { open && + + { open &&
{ !largeScreen &&
@@ -128,7 +118,7 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) {

Delays

-
+
@@ -139,6 +129,12 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) { type="checkbox" name="major" id="filter--major" + onChange={e => { + toggleLayer('majorEvents', e.target.checked); + toggleLayer('majorEventsLines', e.target.checked); + setMajorEvents(!majorEvents) + }} + defaultChecked={mapContext.visible_layers.majorEvents} /> @@ -146,7 +142,7 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) {
-
+
@@ -157,6 +153,12 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) { type="checkbox" name="minor" id="filter--minor" + onChange={e => { + toggleLayer('minorEvents', e.target.checked); + toggleLayer('minorEventsLines', e.target.checked); + setMinorEvents(!minorEvents); + }} + defaultChecked={mapContext.visible_layers.minorEvents} /> @@ -164,7 +166,7 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) {
-
+
@@ -172,7 +174,12 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) { type="checkbox" name="future events" id="filter--future-events" - onChange={e => {toggleLayer('eventsLayer', e.target.checked); toggleChecked()}} + onChange={e => { + toggleLayer('futureEvents', e.target.checked); + toggleLayer('futureEventsLines', e.target.checked); + setFutureEvents(!futureEvents); + }} + defaultChecked={mapContext.visible_layers.futureEvents} /> @@ -187,7 +194,7 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) {

Conditions and features

-
+
@@ -195,8 +202,8 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) { type="checkbox" name="highway cameras" id="filter--highway-cameras" - onChange={e => {toggleLayer('webcamsLayer', e.target.checked); toggleCheckedCams()}} - defaultChecked={mapContext.visible_layers.webcamsLayer} + onChange={e => {toggleLayer('highwayCams', e.target.checked); setHighwayCams(!highwayCams)}} + defaultChecked={mapContext.visible_layers.highwayCams} /> @@ -204,7 +211,7 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) {
-
+
@@ -212,6 +219,12 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) { type="checkbox" name="road conditions" id="filter--road-conditions" + onChange={e => { + toggleLayer('roadConditions', e.target.checked); + toggleLayer('roadConditionsLines', e.target.checked); + setRoadConditions(!roadConditions); + }} + defaultChecked={mapContext.visible_layers.roadConditions} /> @@ -219,7 +232,7 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) {
-
+
@@ -227,7 +240,8 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) { type="checkbox" name="inland ferries" id="filter--inland-ferries" - onChange={e => {toggleLayer('ferriesLayer', e.target.checked); toggleCheckedFerries()}} + onChange={e => {toggleLayer('inlandFerries', e.target.checked); setInlandFerries(!inlandFerries)}} + defaultChecked={mapContext.visible_layers.inlandFerries} /> @@ -235,6 +249,7 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) {
+ {/*
@@ -264,6 +279,7 @@ export default function Layers({ open, setLayersOpen, toggleLayer }) { ?
+ */}
diff --git a/src/frontend/src/Components/Layers.scss b/src/frontend/src/Components/Layers.scss index d01d45e7c..129385ab0 100644 --- a/src/frontend/src/Components/Layers.scss +++ b/src/frontend/src/Components/Layers.scss @@ -7,6 +7,10 @@ button.open-layers { } } +::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} + .layers { position: absolute; bottom: 0; @@ -19,6 +23,7 @@ button.open-layers { 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); padding: 1rem; z-index: 20; + overflow-y: scroll; @media (min-width: 576px) { top: 5rem; @@ -77,7 +82,7 @@ button.open-layers { .filter-item__icon { background-color: $Type-Link; border-color: $Type-Link; - + svg { color: $White; } @@ -85,7 +90,7 @@ button.open-layers { .customIcon-bg { fill: white; } - + .customIcon-fg { fill: $Type-Link; } @@ -97,7 +102,7 @@ button.open-layers { .filter-item__icon { background-color: $Divider; border-color: $Divider; - + svg { color: $Type-Disabled; } @@ -105,7 +110,7 @@ button.open-layers { .customIcon-bg { fill: $Type-Disabled; } - + .customIcon-fg { fill: $Divider; } diff --git a/src/frontend/src/Components/Map.js b/src/frontend/src/Components/Map.js index c35dd793c..df47ac11d 100644 --- a/src/frontend/src/Components/Map.js +++ b/src/frontend/src/Components/Map.js @@ -21,7 +21,7 @@ import { import { getCamerasLayer } from './map/layers/camerasLayer.js'; import { getCamPopup, getEventPopup, getFerryPopup } from './map/mapPopup.js' import { getEvents } from './data/events.js'; -import { getEventsLayer } from './map/layers/eventsLayer.js'; +import { loadEventsLayers } from './map/layers/eventsLayer.js'; import { fitMap, blueLocationMarkup, @@ -80,13 +80,11 @@ export default function MapWrapper({ const mapElement = useRef(); const mapRef = useRef(); const popup = useRef(); - const layers = useRef({}); + const mapLayers = useRef({}); const mapView = useRef(); const container = useRef(); const geolocation = useRef(null); - const hoveredCamera = useRef(); - const hoveredEvent = useRef(); - const hoveredFerry = useRef(); + const hoveredFeature = useRef(); const locationPinRef = useRef(null); const cameraPopupRef = useRef(null); @@ -154,8 +152,8 @@ export default function MapWrapper({ }), }); - // initialize starting optional layers - layers.current = { + // initialize starting optional mapLayers + mapLayers.current = { tid: Date.now(), }; @@ -221,22 +219,27 @@ export default function MapWrapper({ }); // Click states - const resetClickedStates = (clickedFeature) => { - if (clickedCameraRef.current && clickedFeature != clickedCameraRef.current) { + const resetClickedStates = (targetFeature) => { + if (clickedCameraRef.current && targetFeature != clickedCameraRef.current) { clickedCameraRef.current.setStyle(cameraStyles['static']); updateClickedCamera(null); } - if (clickedEventRef.current && clickedFeature != clickedEventRef.current) { + if (clickedEventRef.current && targetFeature != clickedEventRef.current) { clickedEventRef.current.setStyle( getEventIcon(clickedEventRef.current, 'static'), ); - setRelatedGeometry(clickedEventRef.current, 'static'); + // Set associated line/point feature + const altFeature = clickedEventRef.current.getProperties()['altFeature']; + if (altFeature) { + altFeature.setStyle(getEventIcon(altFeature, 'static')); + } + updateClickedEvent(null); } - if (clickedFerryRef.current && clickedFeature != clickedFerryRef.current) { + if (clickedFerryRef.current && targetFeature != clickedFerryRef.current) { clickedFerryRef.current.setStyle(ferryStyles['static']); updateClickedFerry(null); } @@ -266,11 +269,16 @@ export default function MapWrapper({ resetClickedStates(feature); // set new clicked event feature - feature.setStyle( - getEventIcon(feature, 'active'), - ); - setRelatedGeometry(feature, 'active'); + feature.setStyle(getEventIcon(feature, 'active')); feature.setProperties({ clicked: true }, true); + + // Set associated line/point feature + const altFeature = feature.getProperties()['altFeature']; + if (altFeature) { + altFeature.setStyle(getEventIcon(altFeature, 'active')); + altFeature.setProperties({ clicked: true }, true); + } + updateClickedEvent(feature); popup.current.setPosition( @@ -295,31 +303,23 @@ export default function MapWrapper({ } mapRef.current.on('click', async (e) => { - // check if it was a webcam icon that was clicked - const camFeatures = layers.current.webcamsLayer.getVisible() ? - await layers.current.webcamsLayer.getFeatures(e.pixel) : []; - - if (camFeatures.length) { - camClickHandler(camFeatures[0]); - return; - } - - // if it wasn't a webcam icon, check if it was an event - const eventFeatures = layers.current.eventsLayer.getVisible() ? - await layers.current.eventsLayer.getFeatures(e.pixel) : []; - - if (eventFeatures.length) { - eventClickHandler(eventFeatures[0]); - return; - } - - // if it wasn't a event icon, check if it was a ferry - const ferryFeatures = layers.current.ferriesLayer.getVisible() ? - await layers.current.ferriesLayer.getFeatures(e.pixel) : []; + const features = mapRef.current.getFeaturesAtPixel(e.pixel, { + hitTolerance: 20, + }); - if (ferryFeatures.length) { - ferryClickHandler(ferryFeatures[0]); - return; + if (features.length) { + const clickedFeature = features[0]; + switch(clickedFeature.getProperties()['type']) { + case 'camera': + camClickHandler(clickedFeature); + return; + case 'event': + eventClickHandler(clickedFeature); + return; + case 'ferry': + ferryClickHandler(clickedFeature); + return; + } } // Close popups if clicked on blank space @@ -327,94 +327,72 @@ export default function MapWrapper({ }); // Hover states - const resetHoveredStates = (hoveredFeature) => { - if (hoveredCamera.current && hoveredFeature != hoveredCamera.current) { - if (!hoveredCamera.current.getProperties().clicked) { - hoveredCamera.current.setStyle(cameraStyles['static']); - } - - hoveredCamera.current = null; - } - - if (hoveredEvent.current && hoveredFeature != hoveredEvent.current) { - if (!hoveredEvent.current.getProperties().clicked) { - hoveredEvent.current.setStyle( - getEventIcon(hoveredEvent.current, 'static'), - ); - setRelatedGeometry(hoveredEvent.current, 'static'); - } - hoveredEvent.current = null; - } - - if (hoveredFerry.current && hoveredFeature != hoveredFerry.current) { - if (!hoveredFerry.current.getProperties().clicked) { - hoveredFerry.current.setStyle(ferryStyles['static']); + 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': { + hoveredFeature.current.setStyle(getEventIcon(hoveredFeature.current, 'static')); + + // Set associated line/point feature + const altFeature = hoveredFeature.current.getProperties()['altFeature']; + if (altFeature) { + altFeature.setStyle(getEventIcon(altFeature, 'static')); + } + break; + } + case 'ferry': + hoveredFeature.current.setStyle(ferryStyles['static']); + break; + } } - hoveredFerry.current = null; + hoveredFeature.current = null; } } mapRef.current.on('pointermove', async (e) => { - if (layers.current && 'webcamsLayer' in layers.current) { - // check if it was a camera icon that was hovered on - const hoveredCameras = await layers.current['webcamsLayer'].getFeatures(e.pixel); - if (hoveredCameras.length) { - const feature = hoveredCameras[0]; - - resetHoveredStates(feature); - - hoveredCamera.current = feature; - if (!hoveredCamera.current.getProperties().clicked) { - hoveredCamera.current.setStyle(cameraStyles['hover']); - } - - return; - } - } - - // if it wasn't a camera icon, check if it was an event - if (layers.current && 'eventsLayer' in layers.current) { - const hoveredEvents = await layers.current['eventsLayer'].getFeatures(e.pixel); - if (hoveredEvents.length) { - const feature = hoveredEvents[0]; - - resetHoveredStates(feature); - - hoveredEvent.current = feature; - if (!hoveredEvent.current.getProperties().clicked) { - hoveredEvent.current.setStyle( - getEventIcon(hoveredEvent.current, 'hover'), - ); - setRelatedGeometry(hoveredEvent.current, 'hover'); - } - - return; - } - } - - // if it wasn't a event icon, check if it was a ferry - if (layers.current && 'ferriesLayer' in layers.current) { - // check if it was a camera icon that was hovered on - const hoveredFerries = await layers.current['ferriesLayer'].getFeatures(e.pixel); - if (hoveredFerries.length) { - const feature = hoveredFerries[0]; - - resetHoveredStates(feature); - - hoveredFerry.current = feature; - if (!hoveredFerry.current.getProperties().clicked) { - hoveredFerry.current.setStyle(ferryStyles['hover']); - } + const features = mapRef.current.getFeaturesAtPixel(e.pixel, { + hitTolerance: 20, + }); - return; + 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) { + targetFeature.setStyle(getEventIcon(targetFeature, 'hover')); + + // Set associated line/point feature + const altFeature = targetFeature.getProperties()['altFeature']; + if (altFeature) { + altFeature.setStyle(getEventIcon(altFeature, 'hover')); + } + } + return; + case 'ferry': + if (!targetFeature.getProperties().clicked) { + targetFeature.setStyle(ferryStyles['hover']); + } + return; } } // Reset on blank space resetHoveredStates(null); }); - if(!camera){ + + if (!camera) { // if there is no parameter for shifting the view, pan to my location toggleMyLocation(); } @@ -436,13 +414,13 @@ export default function MapWrapper({ }, [searchLocationFrom]); useEffect(() => { - if (layers.current['routeLayer']) { - mapRef.current.removeLayer(layers.current['routeLayer']); + if (mapLayers.current['routeLayer']) { + mapRef.current.removeLayer(mapLayers.current['routeLayer']); } if (selectedRoute && selectedRoute.routeFound) { const routeLayer = getRouteLayer(selectedRoute, mapRef.current.getView().getProjection().getCode()); - layers.current['routeLayer'] = routeLayer; + mapLayers.current['routeLayer'] = routeLayer; mapRef.current.addLayer(routeLayer); loadEvents(selectedRoute.points); @@ -461,11 +439,11 @@ export default function MapWrapper({ async function loadCameras(route) { const webcamResults = await getWebcams(route); - if (layers.current['webcamsLayer']) { - mapRef.current.removeLayer(layers.current['webcamsLayer']); + if (mapLayers.current['highwayCams']) { + mapRef.current.removeLayer(mapLayers.current['highwayCams']); } - layers.current['webcamsLayer'] = getCamerasLayer( + mapLayers.current['highwayCams'] = getCamerasLayer( groupCameras(webcamResults), mapRef.current.getView().getProjection().getCode(), mapContext, @@ -473,42 +451,30 @@ export default function MapWrapper({ updateClickedCamera, ) - mapRef.current.addLayer(layers.current['webcamsLayer']); - layers.current['webcamsLayer'].setZIndex(1); + mapRef.current.addLayer(mapLayers.current['highwayCams']); + mapLayers.current['highwayCams'].setZIndex(4); } async function loadEvents(route) { const eventsData = await getEvents(route); - - if (layers.current['eventsLayer']) { - mapRef.current.removeLayer(layers.current['eventsLayer']); - } - - layers.current['eventsLayer'] = getEventsLayer( - eventsData, - mapRef.current.getView().getProjection().getCode(), - mapContext, - camera, - updateClickedEvent, - ) - - mapRef.current.addLayer(layers.current['eventsLayer']); + loadEventsLayers(eventsData, mapContext, mapLayers, mapRef); } async function loadFerries() { const ferriesData = await getFerries(); - if (layers.current['ferriesLayer']) { - mapRef.current.removeLayer(layers.current['ferriesLayer']); + if (mapLayers.current['inlandFerries']) { + mapRef.current.removeLayer(mapLayers.current['inlandFerries']); } - layers.current['ferriesLayer'] = getFerriesLayer( + mapLayers.current['inlandFerries'] = getFerriesLayer( ferriesData, mapRef.current.getView().getProjection().getCode(), mapContext ) - mapRef.current.addLayer(layers.current['ferriesLayer']); + mapRef.current.addLayer(mapLayers.current['inlandFerries']); + mapLayers.current['inlandFerries'].setZIndex(8); } function closePopup() { @@ -526,8 +492,18 @@ export default function MapWrapper({ clickedEventRef.current.setStyle( getEventIcon(clickedEventRef.current, 'static'), ); - setRelatedGeometry(clickedEventRef.current, 'static'); clickedEventRef.current.set('clicked', false); + + // Set associated line/point feature + const altFeature = clickedEventRef.current.getProperties()['altFeature']; + if (altFeature) { + altFeature.setStyle( + getEventIcon(altFeature, 'static'), + ); + + altFeature.set('clicked', false); + } + updateClickedEvent(null); } @@ -542,15 +518,6 @@ export default function MapWrapper({ cameraPopupRef.current = null; } - const setRelatedGeometry = (event, state) => { - if (event.getId()) { - const relatedFeature = layers.current['eventsLayer'] - .getSource() - .getFeatureById(event.ol_uid); - relatedFeature.setStyle(getEventIcon(relatedFeature, state)); - } - }; - function toggleMyLocation() { if ('geolocation' in navigator) { navigator.geolocation.getCurrentPosition( @@ -611,7 +578,7 @@ export default function MapWrapper({ } function toggleLayer(layer, checked) { - layers.current[layer].setVisible(checked); + mapLayers.current[layer].setVisible(checked); // Set context and local storage mapContext.visible_layers[layer] = checked; diff --git a/src/frontend/src/Components/map/helper.js b/src/frontend/src/Components/map/helper.js index cbf1d126d..99c39b1a7 100644 --- a/src/frontend/src/Components/map/helper.js +++ b/src/frontend/src/Components/map/helper.js @@ -11,34 +11,30 @@ import { eventStyles } from '../data/featureStyleDefinitions.js'; // Static assets export const getEventIcon = (event, state) => { const severity = event.get('severity').toLowerCase(); - const type = event.get('event_type').toLowerCase(); + const display_category = event.get('display_category').toLowerCase(); const geometry = event.getGeometry().getType(); if (geometry === 'Point') { if (severity === 'major') { - switch (type) { - case 'incident': + switch (display_category) { + case 'majorEvents': return eventStyles['major_incident'][state]; - case 'construction': - return eventStyles['major_construction'][state]; - case 'special_event': + case 'futureEvents': return eventStyles['major_special_event'][state]; - case 'weather_condition': + case 'roadConditions': return eventStyles['major_weather_condition'][state]; default: return eventStyles['major_incident'][state]; } } else { - switch (type) { - case 'incident': - return eventStyles['incident'][state]; - case 'construction': + switch (display_category) { + case 'minorEvents': return eventStyles['construction'][state]; - case 'special_event': + case 'futureEvents': return eventStyles['special_event'][state]; - case 'weather_condition': + case 'roadConditions': return eventStyles['weather_condition'][state]; default: - return eventStyles['incident'][state]; + return eventStyles['construction'][state]; } } } else { @@ -88,7 +84,7 @@ export const zoomIn = (mapView) => { return; } - setZoomPan(mapView, mapView.getZoom() + 1); + setZoomPan(mapView, mapView.current.getZoom() + 1); } export const zoomOut = (mapView) => { @@ -96,7 +92,7 @@ export const zoomOut = (mapView) => { return; } - setZoomPan(mapView, mapView.getZoom() - 1); + setZoomPan(mapView, mapView.current.getZoom() - 1); } // Location pins diff --git a/src/frontend/src/Components/map/layers/camerasLayer.js b/src/frontend/src/Components/map/layers/camerasLayer.js index 93aed478d..5969141c4 100644 --- a/src/frontend/src/Components/map/layers/camerasLayer.js +++ b/src/frontend/src/Components/map/layers/camerasLayer.js @@ -20,7 +20,7 @@ export function getCamerasLayer( ) { return new VectorLayer({ classname: 'webcams', - visible: mapContext.visible_layers.webcamsLayer, + visible: mapContext.visible_layers.highwayCams, source: new VectorSource({ format: new GeoJSON(), loader: function (extent, resolution, projection) { @@ -34,6 +34,7 @@ export function getCamerasLayer( // Transfer properties olFeature.setProperties(camera); + olFeature.set('type', 'camera'); // 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 1d0e9ce5f..1bcd5833b 100644 --- a/src/frontend/src/Components/map/layers/eventsLayer.js +++ b/src/frontend/src/Components/map/layers/eventsLayer.js @@ -8,89 +8,136 @@ import GeoJSON from 'ol/format/GeoJSON.js'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; -export function getEventsLayer( +export function loadEventsLayers( eventsData, - projectionCode, mapContext, - passedEvent, - updateClickedEvent, + mapLayers, + mapRef ) { - return new VectorLayer({ - classname: 'events', - visible: mapContext.visible_layers.eventsLayer, - source: new VectorSource({ - format: new GeoJSON(), - loader: function (extent, resolution, projection) { - const vectorSource = this; - vectorSource.clear(); - if (eventsData) { - eventsData.forEach(record => { - let olGeometry = null; - let centroidFeatureForMap = null; - switch (record.location.type) { - case 'Point': - olGeometry = new Point(record.location.coordinates); - break; - case 'LineString': - olGeometry = new LineString(record.location.coordinates); - break; - default: - console.log(Error); - } - const olFeature = new ol.Feature({ geometry: olGeometry }); - - // Transfer properties - olFeature.setProperties(record); - - // Transform the projection - const olFeatureForMap = transformFeature( - olFeature, - 'EPSG:4326', - projectionCode, - ); - - if (olFeature.getGeometry().getType() === 'LineString') { - const centroidGeometry = new Point( - olFeature.getGeometry().getCoordinates()[ - Math.floor( - olFeature.getGeometry().getCoordinates().length / 2, - ) - ], - ); - const centroidFeature = new ol.Feature({ - geometry: centroidGeometry, - }); - // Transfer properties - centroidFeature.setProperties(record); - // Transform the projection - centroidFeatureForMap = transformFeature( - centroidFeature, - 'EPSG:4326', - projectionCode, - ); - centroidFeatureForMap.setId(olFeatureForMap.ol_uid); - vectorSource.addFeature(centroidFeatureForMap); - olFeatureForMap.setId(centroidFeatureForMap.ol_uid); - } - - vectorSource.addFeature(olFeatureForMap); - - if ( - passedEvent && - passedEvent.id === olFeatureForMap.getProperties().id - ) { - olFeatureForMap.setProperties({ clicked: true }, true); - updateClickedEvent(olFeatureForMap); - } - }); + // Helper function for initializing vss + const createVS = () => new VectorSource({ + format: new GeoJSON() + }); + + if (eventsData) { + // Initialize vss + const majorEventsVS = createVS(); + const majorEventsLinesVS = createVS(); + const minorEventsVS = createVS(); + const minorEventsLinesVS = createVS(); + const futureEventsVS = createVS(); + const futureEventsLinesVS = createVS(); + const roadConditionsVS = createVS(); + const roadConditionsLinesVS = createVS(); + + // Helper function to add features to relative vs + const addFeature = (feature, display_category) => { + const isLineSegment = feature.getGeometry().getType() === 'LineString'; + + const vsMap = { + majorEvents: majorEventsVS, + minorEvents: minorEventsVS, + futureEvents: futureEventsVS, + roadConditions: roadConditionsVS + } + + const lineVsMap = { + majorEvents: majorEventsLinesVS, + minorEvents: minorEventsLinesVS, + futureEvents: futureEventsLinesVS, + roadConditions: roadConditionsLinesVS + } + + // Add feature to vs + const vs = isLineSegment ? lineVsMap[display_category] : vsMap[display_category]; + vs.addFeature(feature); + } + + // Helper function to call transform with set projections + const transform = (feature) => { + return transformFeature( + feature, + 'EPSG:4326', + mapRef.current.getView().getProjection().getCode(), + ) + } + + // Add features to vss for each event + eventsData.forEach(event => { + // Create linestring features + if (event.location.type == 'LineString') { + // Center point display + const eventCoords = event.location.coordinates; + const eventFeature = new ol.Feature({ + geometry: new Point( + eventCoords[ Math.floor(eventCoords.length / 2) ] + ) + }); + eventFeature.setProperties(event); + eventFeature.set('type', 'event'); + + // Line display + const eventLineFeature = new ol.Feature({ + geometry: new LineString(event.location.coordinates) + }); + eventLineFeature.setProperties(event); + eventLineFeature.set('type', 'event'); + + // Transform event coordinates + const eventTransformed = transform(eventFeature); + const eventLineTransformed = transform(eventLineFeature); + + // Associate two features together for click handler + eventTransformed.set('altFeature', eventLineTransformed); + eventLineTransformed.set('altFeature', eventTransformed); + + // Add features to linestring and relative vs + addFeature(eventTransformed, event.display_category); + addFeature(eventLineTransformed, event.display_category); + + // Create point feature + } else { + const eventFeature = new ol.Feature({ + geometry: new Point(event.location.coordinates) + }); + eventFeature.setProperties(event); + eventFeature.set('type', 'event'); + + // Transform event coordinates + const eventTransformed = transform(eventFeature); + + // Add feature to relative vs + addFeature(eventTransformed, event.display_category); + } + + // Helper function to add layer to map + const addLayer = (name, vs, zIndex) => { + if (mapLayers.current[name]) { + mapRef.current.removeLayer(mapLayers.current[name]); } - }, - }), - style: function (feature, resolution) { - if (passedEvent && passedEvent.id === feature.getProperties().id) { - return getEventIcon(feature, 'active'); + + mapLayers.current[name] = new VectorLayer({ + classname: 'events', + visible: mapContext.visible_layers[name], + source: vs, + style: function (feature, resolution) { + return getEventIcon(feature, 'static'); + }, + }); + + mapRef.current.addLayer(mapLayers.current[name]); + mapLayers.current[name].setZIndex(zIndex); } - return getEventIcon(feature, 'static'); - }, - }); + + // Add layer to map for each vs + addLayer('majorEvents', majorEventsVS, 128); + addLayer('majorEventsLines', majorEventsLinesVS, 2); + addLayer('minorEvents', minorEventsVS, 128); + addLayer('minorEventsLines', minorEventsLinesVS, 2); + addLayer('futureEvents', futureEventsVS, 128); + addLayer('futureEventsLines', futureEventsLinesVS, 2); + addLayer('roadConditions', roadConditionsVS, 128); + addLayer('roadConditionsLines', roadConditionsLinesVS, 2); + }); + } } diff --git a/src/frontend/src/Components/map/layers/ferriesLayer.js b/src/frontend/src/Components/map/layers/ferriesLayer.js index 5a1cd35df..9438d4cd2 100644 --- a/src/frontend/src/Components/map/layers/ferriesLayer.js +++ b/src/frontend/src/Components/map/layers/ferriesLayer.js @@ -14,7 +14,7 @@ import { ferryStyles } from '../../data/featureStyleDefinitions.js'; export function getFerriesLayer(ferriesData, projectionCode, mapContext) { return new VectorLayer({ classname: 'ferries', - visible: mapContext.visible_layers.ferriesLayer, + visible: mapContext.visible_layers.inlandFerries, source: new VectorSource({ format: new GeoJSON(), loader: function (extent, resolution, projection) { @@ -28,6 +28,7 @@ export function getFerriesLayer(ferriesData, projectionCode, mapContext) { // Transfer properties olFeature.setProperties(ferry); + olFeature.set('type', 'ferry'); // Transform the projection const olFeatureForMap = transformFeature( diff --git a/src/frontend/src/Components/map/mapPopup.js b/src/frontend/src/Components/map/mapPopup.js index 6b83b5604..11cb649ed 100644 --- a/src/frontend/src/Components/map/mapPopup.js +++ b/src/frontend/src/Components/map/mapPopup.js @@ -90,10 +90,8 @@ export function getCamPopup(camFeature, setClickedCamera, navigate, cameraPopupR } export function getEventPopup(eventFeature) { - console.log(eventFeature.ol_uid); const eventData = eventFeature.ol_uid ? eventFeature.getProperties() : eventFeature; const severity = eventData.severity.toLowerCase(); - const eventType = eventData.event_type.toLowerCase(); return (
@@ -106,7 +104,7 @@ export function getEventPopup(eventFeature) {
- +

{severity} delays

diff --git a/src/frontend/src/pages/EventsPage.js b/src/frontend/src/pages/EventsPage.js index 46ce6de3c..5ee53b525 100644 --- a/src/frontend/src/pages/EventsPage.js +++ b/src/frontend/src/pages/EventsPage.js @@ -1,21 +1,22 @@ // React -import React, {useEffect, useRef, useState} from 'react'; -import {useNavigate} from 'react-router-dom'; +import React, { useContext, useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; // Third party packages -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faMapLocationDot, faFilter, } from '@fortawesome/free-solid-svg-icons'; -import {useMediaQuery} from '@uidotdev/usehooks'; +import { useMediaQuery } from '@uidotdev/usehooks'; import Container from 'react-bootstrap/Container'; import Dropdown from 'react-bootstrap/Dropdown'; import Form from 'react-bootstrap/Form'; import InfiniteScroll from 'react-infinite-scroll-component'; // Components and functions -import {getEvents} from '../Components/data/events'; +import { getEvents } from '../Components/data/events'; +import { MapContext } from '../App.js'; import EventCard from '../Components/events/EventCard'; import EventsTable from '../Components/events/EventsTable'; import FriendlyTime from '../Components/FriendlyTime'; @@ -28,6 +29,9 @@ import Advisories from '../Components/advisories/Advisories'; import './EventsPage.scss'; export default function EventsPage() { + // Context + const { mapContext, setMapContext } = useContext(MapContext); + const isInitialMount = useRef(true); const navigate = useNavigate(); @@ -35,8 +39,8 @@ export default function EventsPage() { const columns = [ { header: 'Type', - accessorKey: 'event_type', - cell: (props) => , + accessorKey: 'display_category', + cell: (props) => , }, { header: 'Severity', @@ -72,33 +76,33 @@ export default function EventsPage() { const filterProps = [ { id: 'checkbox-filter-incident', - label: 'Incidents', - value: 'INCIDENT', - }, - { - id: 'checkbox-filter-weather', - label: 'Road Conditions', - value: 'WEATHER_CONDITION', + label: 'Major Delays', + value: 'majorEvents', }, { id: 'checkbox-filter-construction', - label: 'Current Events', - value: 'CONSTRUCTION', + label: 'Minor Delays', + value: 'minorEvents', }, { id: 'checkbox-filter-special', - label: 'Future Events', - value: 'SPECIAL_EVENT', + label: 'Future Delays', + value: 'futureEvents', }, + { + id: 'checkbox-filter-weather', + label: 'Road Conditions', + value: 'roadConditions', + } ]; const [sortingColumns, setSortingColumns] = useState([]); - const [eventTypeFilter, setEventTypeFilter] = useState({ - 'CONSTRUCTION': false, - 'INCIDENT': false, - 'SPECIAL_EVENT': false, - 'WEATHER_CONDITION': false, + const [eventCategoryFilter, setEventCategoryFilter] = useState({ + 'majorEvents': mapContext.visible_layers.majorEvents, + 'minorEvents': mapContext.visible_layers.minorEvents, + 'futureEvents': mapContext.visible_layers.futureEvents, + 'roadConditions': false, }); const [events, setEvents] = useState([]); @@ -131,13 +135,16 @@ export default function EventsPage() { const processEvents = () => { const hasTrue = (val) => !!val; - const hasFilterOn = Object.values(eventTypeFilter).some(hasTrue); + const hasFilterOn = Object.values(eventCategoryFilter).some(hasTrue); let res = [...events]; // Filter if (hasFilterOn) { - res = res.filter((e) => !!eventTypeFilter[e.event_type]); + res = res.filter((e) => !!eventCategoryFilter[e.display_category]); + + } else { + res = res.filter((e) => e.display_category != 'roadConditions'); } // Sort @@ -174,15 +181,20 @@ export default function EventsPage() { if (!isInitialMount.current) { // Do not run on startup processEvents(); } - }, [events, eventTypeFilter, sortingColumns]); + }, [events, eventCategoryFilter, sortingColumns]); + + const eventCategoryFilterHandler = (e) => { + const targetCategory = e.target.value; - const eventTypeFilterHandler = (e) => { - const eventType = e.target.value; + const newFilter = {...eventCategoryFilter}; + newFilter[targetCategory] = !newFilter[targetCategory]; // Toggle/invert value - const newFilter = {...eventTypeFilter}; - newFilter[eventType] = !newFilter[eventType]; + setEventCategoryFilter(newFilter); - setEventTypeFilter(newFilter); + // Set context and local storage + mapContext.visible_layers[targetCategory] = newFilter[targetCategory]; // Set identical to newFilter after change + setMapContext(mapContext); + localStorage.setItem('mapContext', JSON.stringify(mapContext)); }; const largeScreen = useMediaQuery('only screen and (min-width : 768px)'); @@ -211,14 +223,15 @@ export default function EventsPage() { {filterProps.map((fp) => ( {fp.icon}{fp.label} } value={fp.value} - checked={eventTypeFilter[fp.value]} - onChange={eventTypeFilterHandler} /> + checked={eventCategoryFilter[fp.value]} + onChange={eventCategoryFilterHandler} /> ))} @@ -243,7 +256,7 @@ export default function EventsPage() { } + icon= {} />
),