Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DBC22-1935: advisories rendering, notification only show if in view extent #354

Merged
merged 2 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 90 additions & 4 deletions src/frontend/src/Components/Map.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
updateRestStops,
} from '../slices/feedsSlice';
import { updateMapState } from '../slices/mapSlice';
import { updateAdvisories } from '../slices/cmsSlice';

// External Components
import Button from 'react-bootstrap/Button';
Expand All @@ -33,17 +34,19 @@ import {

// Components and functions
import CamPopup from './map/camPopup.js';
import { getCamerasLayer } from './map/layers/camerasLayer.js';
import {
getEventPopup,
getFerryPopup,
getWeatherPopup,
getRegionalPopup,
getRestStopPopup,
} from './map/mapPopup.js';
import { getAdvisories } from './data/advisories.js';
import { getEvents } from './data/events.js';
import { getWeather, getRegional } from './data/weather.js';
import { getRestStops } from './data/restStops.js';
import { getAdvisoriesLayer } from './map/layers/advisoriesLayer.js';
import { getCamerasLayer } from './map/layers/camerasLayer.js';
import { getRestStopsLayer } from './map/layers/restStopsLayer.js';
import { loadEventsLayers } from './map/layers/eventsLayer.js';
import { loadWeatherLayers } from './map/layers/weatherLayer.js';
Expand All @@ -68,17 +71,19 @@ import CurrentCameraIcon from './CurrentCameraIcon';
import Filters from './Filters.js';
import RouteSearch from './map/RouteSearch.js';

// OpenLayers
// OpenLayers & turf
import { applyStyle } from 'ol-mapbox-style';
import { fromLonLat, toLonLat, transformExtent } from 'ol/proj';
import { ScaleLine } from 'ol/control.js';
import { getBottomLeft, getTopRight } from 'ol/extent';
import Map from 'ol/Map';
import Overlay from 'ol/Overlay.js';
import Geolocation from 'ol/Geolocation.js';
import MVT from 'ol/format/MVT.js';
import VectorTileLayer from 'ol/layer/VectorTile.js';
import VectorTileSource from 'ol/source/VectorTile.js';
import View from 'ol/View';
import { booleanIntersects, polygon } from '@turf/turf';

// Styling
import {
Expand All @@ -103,8 +108,9 @@ export default function MapWrapper({
camTimeStamp, // Cameras
events,
eventTimeStamp, // Events
advisories, // CMS
ferries,
ferriesTimeStamp, // CMS
ferriesTimeStamp, // Ferries
weather,
weatherTimeStamp, // Current Weather
regional,
Expand All @@ -125,6 +131,8 @@ export default function MapWrapper({
events: state.feeds.events.list,
eventTimeStamp: state.feeds.events.routeTimeStamp,
// CMS
advisories: state.cms.advisories.list,
// Ferries
ferries: state.feeds.ferries.list,
ferriesTimeStamp: state.feeds.ferries.routeTimeStamp,
// Current Weather
Expand Down Expand Up @@ -207,6 +215,8 @@ export default function MapWrapper({
setClickedRestStop(feature);
}

const [advisoriesInView, setAdvisoriesInView] = useState([]);

// Define the function to be executed after the delay
function resetCameraPopupRef() {
cameraPopupRef.current = null;
Expand Down Expand Up @@ -631,6 +641,7 @@ export default function MapWrapper({
}
}, [searchLocationFrom]);

// Route layer
useEffect(() => {
if (isInitialMountRoute.current) {
// Do nothing on first load
Expand All @@ -646,6 +657,7 @@ export default function MapWrapper({
loadData(false);
}, [selectedRoute]);

// Camera layer
useEffect(() => {
// Remove layer if it already exists
if (mapLayers.current['highwayCams']) {
Expand Down Expand Up @@ -687,6 +699,7 @@ export default function MapWrapper({
}
};

// Event layers
useEffect(() => {
loadEventsLayers(events, mapContext, mapLayers, mapRef);
}, [events]);
Expand All @@ -706,6 +719,7 @@ export default function MapWrapper({
}
};

// Ferries and rest stops layers
useEffect(() => {
// Remove layer if it already exists
if (mapLayers.current['inlandFerries']) {
Expand Down Expand Up @@ -767,6 +781,7 @@ export default function MapWrapper({
}
};

// Weather layers
useEffect(() => {
if (mapLayers.current['weather']) {
mapRef.current.removeLayer(mapLayers.current['weather']);
Expand Down Expand Up @@ -797,6 +812,75 @@ export default function MapWrapper({
}
};

// Advisories helper functions
function wrapLon(value) {
const worlds = Math.floor((value + 180) / 360);
return value - worlds * 360;
}

function onMoveEnd(evt) {
// calculate polygon based on map extent
const map = evt.map;
const extent = map.getView().calculateExtent(map.getSize());
const bottomLeft = toLonLat(getBottomLeft(extent));
const topRight = toLonLat(getTopRight(extent));

const mapPoly = polygon([[
[wrapLon(bottomLeft[0]), topRight[1]], // Top left
[wrapLon(bottomLeft[0]), bottomLeft[1]], // Bottom left
[wrapLon(topRight[0]), bottomLeft[1]], // Bottom right
[wrapLon(topRight[0]), topRight[1]], // Top right
[wrapLon(bottomLeft[0]), topRight[1]], // Top left
]]);

// Update state with advisories that intersect with map extent
const resAdvisories = [];
if (advisories && advisories.length > 0) {
advisories.forEach(advisory => {
const advPoly = polygon(advisory.geometry.coordinates);
if (booleanIntersects(mapPoly, advPoly)) {
resAdvisories.push(advisory);
}
});
}
setAdvisoriesInView(resAdvisories);
}

// Advisories layer
const loadAdvisories = async () => {
// Fetch data if it doesn't already exist
if (!advisories) {
dispatch(updateAdvisories({
list: await getAdvisories(),
timeStamp: new Date().getTime()
}));
}
};

useEffect(() => {
// Remove layer if it already exists
if (mapLayers.current['advisoriesLayer']) {
mapRef.current.removeLayer(mapLayers.current['advisoriesLayer']);
}

// Add layer if array exists
if (advisories) {
// Generate and add layer
mapLayers.current['advisoriesLayer'] = getAdvisoriesLayer(
advisories,
mapRef.current.getView().getProjection().getCode(),
mapContext,
);

mapRef.current.addLayer(mapLayers.current['advisoriesLayer']);
mapLayers.current['advisoriesLayer'].setZIndex(55);

if (mapRef.current) {
mapRef.current.on('moveend', onMoveEnd);
}
}
}, [advisories]);

useEffect(() => {
if (mapLayers.current['regional']) {
mapRef.current.removeLayer(mapLayers.current['regional']);
Expand Down Expand Up @@ -844,6 +928,7 @@ export default function MapWrapper({
loadWeather();
loadRegional();
loadRestStops();
loadAdvisories();

// Zoom/pan to route on route updates
if (!isInitialMount) {
Expand All @@ -857,6 +942,7 @@ export default function MapWrapper({
loadWeather();
loadRegional();
loadRestStops();
loadAdvisories();
}
};

Expand Down Expand Up @@ -1105,7 +1191,7 @@ export default function MapWrapper({
{!isPreview && (
<div className="routing-outer-container">
<RouteSearch routeEdit={true} />
<AdvisoriesOnMap />
<AdvisoriesOnMap advisories={advisoriesInView} />
</div>
)}

Expand Down
36 changes: 3 additions & 33 deletions src/frontend/src/Components/advisories/AdvisoriesOnMap.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
// React
import React, { useCallback, useEffect, useRef, useState } from 'react';

// Redux
import { useSelector, useDispatch } from 'react-redux'
import { memoize } from 'proxy-memoize'
import { updateAdvisories } from '../../slices/cmsSlice';
import React, { useState } from 'react';

// Components and functions
import { getAdvisories } from '../data/advisories.js';
import AdvisoriesList from './AdvisoriesList';

// Third party packages
Expand All @@ -21,33 +15,9 @@ import Button from 'react-bootstrap/Button';
// Styling
import './AdvisoriesOnMap.scss';

export default function AdvisoriesOnMap() {
// Redux
const dispatch = useDispatch();
const { advisories } = useSelector(useCallback(memoize(state => ({
advisories: state.cms.advisories.list,
}))));

// Refs
const isInitialMount = useRef(true);

// Data loading
const loadAdvisories = async () => {
if (!advisories) {
dispatch(updateAdvisories({
list: await getAdvisories(),
timeStamp: new Date().getTime()
}));
}
}
export default function AdvisoriesOnMap(props) {
const { advisories } = props;

useEffect(() => {
if (isInitialMount.current) { // Only run on initial load
loadAdvisories();
isInitialMount.current = false;
}
});

// States
const [open, setOpen] = useState(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

.advisories-on-map {
position: relative;
top: 1rem;
top: 6rem;
left: 1rem;
width: fit-content;

Expand Down
15 changes: 14 additions & 1 deletion src/frontend/src/Components/data/featureStyleDefinitions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Icon, Stroke, Style } from 'ol/style.js';
import { Fill, Icon, Stroke, Style } from 'ol/style.js';

// Static assets
// Cameras
Expand Down Expand Up @@ -61,6 +61,19 @@ import genericDelaysActiveIcon from '../../images/mapIcons/incident-minor-active
import genericDelaysHoverIcon from '../../images/mapIcons/incident-minor-hover.png';
import genericDelaysStaticIcon from '../../images/mapIcons/incident-minor-static.png';

// Map advisory styles
export const advisoryStyles = {
polygon: new Style({
stroke: new Stroke({
color: 'rgb(255, 90, 0)',
width: 2,
}),
fill: new Fill({
color: 'rgba(255, 217, 105, 0.4)',
}),
})
};

// Camera icon styles
export const cameraStyles = {
static: new Style({
Expand Down
50 changes: 50 additions & 0 deletions src/frontend/src/Components/map/layers/advisoriesLayer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Components and functions
import { transformFeature } from '../helper.js';

// OpenLayers
import { Polygon } from 'ol/geom';
import * as ol from 'ol';
import GeoJSON from 'ol/format/GeoJSON.js';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';

// Styling
import { advisoryStyles } from '../../data/featureStyleDefinitions.js';

export function getAdvisoriesLayer(
advisories,
projectionCode,
mapContext,
) {
return new VectorLayer({
classname: 'advisories',
visible: true,
source: new VectorSource({
format: new GeoJSON(),
loader: function (extent, resolution, projection) {
const vectorSource = this;
vectorSource.clear();

advisories.forEach(advisory => {
// Build a new OpenLayers feature
const olGeometry = new Polygon(advisory.geometry.coordinates);
const olFeature = new ol.Feature({ geometry: olGeometry });

// Transform the projection
const olFeatureForMap = transformFeature(
olFeature,
'EPSG:4326',
projectionCode,
);

// feature ID to advisory ID for retrieval
olFeatureForMap.setId(advisory.id);

vectorSource.addFeature(olFeatureForMap);
});
},
}),

style: () => advisoryStyles.polygon,
});
}
7 changes: 3 additions & 4 deletions src/frontend/src/pages/AdvisoryDetailsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,11 @@ function getMap(advisoryData) {
}),
'Polygon': new Style({
stroke: new Stroke({
color: 'blue',
lineDash: [4],
width: 3,
color: 'rgb(255, 90, 0)',
width: 2,
}),
fill: new Fill({
color: 'rgba(0, 0, 255, 0.1)',
color: 'rgba(255, 217, 105, 0.4)',
}),
})
};
Expand Down
Loading