- {activeStoryOrder.map((storyId, i) => {
+ {storyOrder.map((storyId, i) => {
const story = stories[storyId];
return (
-
- {message}
+
+
+ {messageTitle}
+
+
+ {message}
+
{onDismiss && (
@@ -106,6 +112,7 @@ AlertUtil.propTypes = {
id: PropTypes.string,
isOpen: PropTypes.bool,
message: PropTypes.string,
+ messageTitle: PropTypes.string,
noPortal: PropTypes.bool,
onClick: PropTypes.func,
onDismiss: PropTypes.func,
diff --git a/web/js/containers/alertDropdown.js b/web/js/containers/alertDropdown.js
new file mode 100644
index 0000000000..1afce2c519
--- /dev/null
+++ b/web/js/containers/alertDropdown.js
@@ -0,0 +1,36 @@
+import React, { useState, useRef } from 'react';
+import { useSelector } from 'react-redux';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import FeatureAlert from '../components/feature-alert/alert';
+import Alerts from './alerts';
+
+export default function AlertDropdown(isTourActive) {
+ const [dropdownOpen, setDropdownOpen] = useState(false);
+ const containerRef = useRef(null);
+ const notifications = containerRef?.current?.children.length;
+ const toggle = () => setDropdownOpen((prevState) => !prevState);
+ const { isTourActive: tourActive } = isTourActive;
+ const isDistractionFreeModeActive = useSelector((state) => state.ui.isDistractionFreeModeActive);
+ const isMobile = useSelector((state) => state.screenSize.isMobileDevice);
+
+ return (
+
+
+
+ Multiple Layer Alerts
+ {dropdownOpen ? : }
+
+
+
+ Select an issue above for details
+
+
+ );
+}
diff --git a/web/js/containers/alerts.js b/web/js/containers/alerts.js
index 20bbc3f7ae..e65941795d 100644
--- a/web/js/containers/alerts.js
+++ b/web/js/containers/alerts.js
@@ -9,11 +9,15 @@ import { DISABLE_VECTOR_ZOOM_ALERT, DISABLE_VECTOR_EXCEEDED_ALERT, MODAL_PROPERT
import safeLocalStorage from '../util/local-storage';
import { getActiveLayers, subdailyLayersActive } from '../modules/layers/selectors';
+const { granuleModalProps, zoomModalProps } = MODAL_PROPERTIES;
+
const HAS_LOCAL_STORAGE = safeLocalStorage.enabled;
const {
DISMISSED_COMPARE_ALERT,
DISMISSED_DISTRACTION_FREE_ALERT,
DISMISSED_EVENT_VIS_ALERT,
+ DISSMISSED_DDV_ZOOM_ALERT,
+ DISSMISSED_DDV_LOCATION_ALERT,
} = safeLocalStorage.keys;
class DismissableAlerts extends React.Component {
@@ -24,6 +28,8 @@ class DismissableAlerts extends React.Component {
hasDismissedEvents: !!safeLocalStorage.getItem(DISMISSED_EVENT_VIS_ALERT),
hasDismissedCompare: !!safeLocalStorage.getItem(DISMISSED_COMPARE_ALERT),
hasDismissedDistractionFree: !!safeLocalStorage.getItem(DISMISSED_DISTRACTION_FREE_ALERT),
+ hasDismissedDDVZoom: !!safeLocalStorage.getItem(DISSMISSED_DDV_ZOOM_ALERT),
+ hasDismissedDDVLocation: !!safeLocalStorage.getItem(DISSMISSED_DDV_LOCATION_ALERT),
distractionFreeModeInitLoad: false,
};
}
@@ -75,12 +81,20 @@ class DismissableAlerts extends React.Component {
isVectorZoomAlertPresent,
isVectorExceededAlertPresent,
openAlertModal,
+ isDDVZoomAlertPresent,
+ isDDVLocationAlertPresent,
+ openGranuleAlertModal,
+ openZoomAlertModal,
+ ddvZoomAlerts,
+ ddvLocationAlerts,
} = this.props;
const {
hasDismissedEvents,
hasDismissedCompare,
hasDismissedDistractionFree,
distractionFreeModeInitLoad,
+ hasDismissedDDVZoom,
+ hasDismissedDDVLocation,
} = this.state;
const { eventModalProps, compareModalProps, vectorModalProps } = MODAL_PROPERTIES;
const hasFailCondition = !HAS_LOCAL_STORAGE
@@ -91,6 +105,8 @@ class DismissableAlerts extends React.Component {
const showEventsAlert = !isSmall && !hasDismissedEvents && isEventsActive;
const showCompareAlert = !isSmall && !hasDismissedCompare && isCompareActive;
const showAnimationAlert = isMobile && isAnimationActive && hasSubdailyLayers;
+ const showDDVZoomAlert = isDDVZoomAlertPresent && !hasDismissedDDVZoom;
+ const showDDVLocationAlert = isDDVLocationAlertPresent && !hasDismissedDDVLocation;
return isDistractionFreeModeActive
? !hasDismissedDistractionFree && (
@@ -148,7 +164,32 @@ class DismissableAlerts extends React.Component {
onDismiss={() => {}}
/>
)}
-
+ {showDDVZoomAlert
+ && ddvZoomAlerts.map((layer) => (
+
this.dismissAlert(DISSMISSED_DDV_ZOOM_ALERT, 'hasDismissedDDVZoom')}
+ onClick={openZoomAlertModal}
+ />
+ ))}
+ { showDDVLocationAlert
+ && ddvLocationAlerts.map((layer) => (
+ this.dismissAlert(DISSMISSED_DDV_LOCATION_ALERT, 'hasDismissedDDVLocation')}
+ onClick={openGranuleAlertModal}
+ />
+ ))}
>
);
}
@@ -157,6 +198,14 @@ const mapDispatchToProps = (dispatch) => ({
openAlertModal: ({ id, props }) => {
dispatch(openCustomContent(id, props));
},
+ openGranuleAlertModal: () => {
+ const { id, props } = granuleModalProps;
+ dispatch(openCustomContent(id, props));
+ },
+ openZoomAlertModal: () => {
+ const { id, props } = zoomModalProps;
+ dispatch(openCustomContent(id, props));
+ },
dismissVectorZoomAlert: () => dispatch({ type: DISABLE_VECTOR_ZOOM_ALERT }),
dismissVectorExceededAlert: () => dispatch({ type: DISABLE_VECTOR_EXCEEDED_ALERT }),
});
@@ -164,12 +213,23 @@ const mapStateToProps = (state) => {
const {
embed, events, sidebar, compare, alerts, ui, animation, screenSize,
} = state;
- const { isVectorZoomAlertPresent, isVectorExceededAlertPresent } = alerts;
+ const {
+ isVectorZoomAlertPresent,
+ isVectorExceededAlertPresent,
+ isDDVZoomAlertPresent,
+ isDDVLocationAlertPresent,
+ ddvZoomAlerts,
+ ddvLocationAlerts,
+ } = alerts;
const activeLayers = getActiveLayers(state);
const hasActiveVectorLayers = hasVectorLayers(activeLayers);
return {
+ ddvLocationAlerts,
+ ddvZoomAlerts,
isCompareActive: compare.active,
+ isDDVZoomAlertPresent,
+ isDDVLocationAlertPresent,
isDistractionFreeModeActive: ui.isDistractionFreeModeActive,
isEmbedModeActive: embed.isEmbedModeActive,
isEventsActive: !!(events.selected.id && sidebar.activeTab === 'events'),
@@ -200,4 +260,9 @@ DismissableAlerts.propTypes = {
isVectorZoomAlertPresent: PropTypes.bool,
isVectorExceededAlertPresent: PropTypes.bool,
openAlertModal: PropTypes.func,
+ isDDVZoomAlertPresent: PropTypes.bool,
+ isDDVLocationAlertPresent: PropTypes.bool,
+ openGranuleAlertModal: PropTypes.func,
+ openZoomAlertModal: PropTypes.func,
+ activeDDVLayer: PropTypes.object,
};
diff --git a/web/js/containers/map-interactions/ol-vector-interactions.js b/web/js/containers/map-interactions/ol-vector-interactions.js
index 27106b4154..93e6acb8e9 100644
--- a/web/js/containers/map-interactions/ol-vector-interactions.js
+++ b/web/js/containers/map-interactions/ol-vector-interactions.js
@@ -124,7 +124,7 @@ export class VectorInteractions extends React.Component {
const layerExtent = layer.get('extent');
const pixelCoords = map.getCoordinateFromPixel(pixel);
const featureOutsideExtent = layerExtent && !olExtent.containsCoordinate(layerExtent, pixelCoords);
- if (!def || lodashIncludes(def.clickDisabledFeatures, feature.getType()) || featureOutsideExtent) return;
+ if (!def || lodashIncludes(def.clickDisabledFeatures, feature.getGeometry().getType()) || featureOutsideExtent) return;
const isWrapped = proj.id === 'geographic' && (def.wrapadjacentdays || def.wrapX);
const isRenderedFeature = isWrapped ? lon > -250 || lon < 250 || lat > -90 || lat < 90 : true;
if (isRenderedFeature && isFromActiveCompareRegion(pixel, layer.wv.group, compareState, swipeOffset)) {
@@ -181,8 +181,10 @@ export class VectorInteractions extends React.Component {
if (measureIsActive || isCoordinateSearchActive) return;
const isVectorModalOpen = modalState.id.includes('vector_dialog') && modalState.isOpen;
const pixels = e.pixel;
- const clickObj = getDialogObject(pixels, map);
+ let clickObj = getDialogObject(pixels, map);
const metaArray = clickObj.metaArray || [];
+ const isAeronet = !!metaArray[0] && metaArray[0].id.includes('AERONET');
+ clickObj = getDialogObject(pixels, map, isMobile ? screenSize.screenWidth : isAeronet ? 250 : 445);
const selected = clickObj.selected || {};
const offsetLeft = clickObj.offsetLeft || 10;
const offsetTop = clickObj.offsetTop || 100;
@@ -201,10 +203,10 @@ export class VectorInteractions extends React.Component {
}
if (metaArray.length) {
- if (hasNonClickableVectorLayerType) {
+ if (hasNonClickableVectorLayerType && !isAeronet) {
activateVectorZoomAlert();
} else {
- openVectorDialog(dialogId, metaArray, offsetLeft, offsetTop, screenSize, isEmbedModeActive);
+ openVectorDialog(dialogId, metaArray, offsetLeft, offsetTop, screenSize, isEmbedModeActive, isAeronet);
if (exceededLengthLimit) {
activateVectorExceededResultsAlert();
} else if (isVectorExceededAlertPresent) {
@@ -271,7 +273,7 @@ function mapStateToProps(state) {
screenSize,
isCoordinateSearchActive,
compareState: compare,
- getDialogObject: (pixels, olMap) => onMapClickGetVectorFeatures(pixels, olMap, state, swipeOffset),
+ getDialogObject: (pixels, olMap, modalWidth) => onMapClickGetVectorFeatures(pixels, olMap, state, swipeOffset, modalWidth),
isDistractionFreeModeActive: ui.isDistractionFreeModeActive,
isEmbedModeActive: embed.isEmbedModeActive,
isVectorExceededAlertPresent,
@@ -307,13 +309,13 @@ const mapDispatchToProps = (dispatch) => ({
activateVectorZoomAlert: () => dispatch({ type: ACTIVATE_VECTOR_ZOOM_ALERT }),
activateVectorExceededResultsAlert: () => dispatch({ type: ACTIVATE_VECTOR_EXCEEDED_ALERT }),
clearVectorExceededResultsAlert: () => dispatch({ type: DISABLE_VECTOR_EXCEEDED_ALERT }),
- openVectorDialog: (dialogId, metaArray, offsetLeft, offsetTop, screenSize, isEmbedModeActive) => {
+ openVectorDialog: (dialogId, metaArray, offsetLeft, offsetTop, screenSize, isEmbedModeActive, isAeronet) => {
const { screenHeight, screenWidth } = screenSize;
const isMobile = screenSize.isMobileDevice;
const dialogKey = new Date().getUTCMilliseconds();
const modalClassName = isEmbedModeActive && !isMobile ? 'vector-modal light modal-embed' : 'vector-modal light';
const mobileTopOffset = 106;
- const modalWidth = isMobile ? screenWidth : 445;
+ const modalWidth = isMobile ? screenWidth : isAeronet ? 250 : 445;
const modalHeight = isMobile ? screenHeight - mobileTopOffset : 300;
dispatch(openCustomContent(
diff --git a/web/js/containers/sidebar/layer-row.js b/web/js/containers/sidebar/layer-row.js
index 982e7905c0..7e9f999422 100644
--- a/web/js/containers/sidebar/layer-row.js
+++ b/web/js/containers/sidebar/layer-row.js
@@ -2,7 +2,7 @@
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable react/no-danger */
import React, { useState, useEffect } from 'react';
-import { useSelector, connect } from 'react-redux';
+import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Draggable } from 'react-beautiful-dnd';
import { isEmpty as lodashIsEmpty, get as lodashGet } from 'lodash';
@@ -40,6 +40,9 @@ import {
updateActiveChartingLayerAction,
} from '../../modules/charting/actions';
import AlertUtil from '../../components/util/alert';
+import {
+ enableDDVZoomAlert, enableDDVLocationAlert, disableDDVLocationAlert, disableDDVZoomAlert,
+} from '../../modules/alerts/actions';
const { events } = util;
const { vectorModalProps, granuleModalProps, zoomModalProps } = MODAL_PROPERTIES;
@@ -57,6 +60,8 @@ function LayerRow (props) {
layer,
compareState,
collections,
+ ddvLocationAlerts,
+ ddvZoomAlerts,
paletteLegends,
getPalette,
palette,
@@ -91,6 +96,12 @@ function LayerRow (props) {
isChartingActive,
activeChartingLayer,
updateActiveChartingLayer,
+ enableDDVZoomAlert,
+ enableDDVLocationAlert,
+ disableDDVLocationAlert,
+ disableDDVZoomAlert,
+ map,
+ selectedDate,
} = props;
const encodedLayerId = util.encodeId(layer.id);
@@ -114,8 +125,11 @@ function LayerRow (props) {
const [showGranuleAlert, setShowGranuleAlert] = useState(false);
const [hideZoomAlert, setHideZoomAlert] = useState(false);
const [hideGranuleAlert, setHideGranuleAlert] = useState(false);
- const map = useSelector((state) => state.map);
- const selectedDate = useSelector((state) => state.date.selected);
+
+ const ddvLayerZoomNoticeActive = ddvZoomAlerts.includes(layer.title);
+ const ddvLayerLocationNoticeActive = ddvLocationAlerts.includes(layer.title);
+ // All DDV layer notices are dismissable + Reflectance (Nadir BRDF-Adjusted) + DSWx-HLS
+ const isLayerNotificationDismissable = layer.type === 'titiler' || layer.title === 'Reflectance (Nadir BRDF-Adjusted)' || layer.subtitle === 'DSWx-HLS';
useEffect(() => {
const asyncFunc = async () => {
@@ -134,11 +148,16 @@ function LayerRow (props) {
}
return maxExtent[i];
});
- const olderRes = await fetch(`https://cmr.earthdata.nasa.gov/search/granules.json?collection_concept_id=${conceptID}&bounding_box=${extent.join(',')}&temporal=P0Y0M0DT0H0M/${zeroedDate}&sort_key=-start_date&pageSize=1`);
- const newerRes = await fetch(`https://cmr.earthdata.nasa.gov/search/granules.json?collection_concept_id=${conceptID}&bounding_box=${extent.join(',')}&temporal=${zeroedDate}/P0Y0M1DT0H0M&sort_key=-start_date&pageSize=1`);
+ const olderUrl = `https://cmr.earthdata.nasa.gov/search/granules.json?collection_concept_id=${conceptID}&bounding_box=${extent.join(',')}&temporal=P0Y0M0DT0H0M/${zeroedDate}&sort_key=-start_date&pageSize=1`;
+ const newerUrl = `https://cmr.earthdata.nasa.gov/search/granules.json?collection_concept_id=${conceptID}&bounding_box=${extent.join(',')}&temporal=${zeroedDate}/P0Y0M1DT0H0M&sort_key=-start_date&pageSize=1`;
+ const headers = { 'Client-Id': 'Worldview' };
+ const requests = [fetch(olderUrl, { headers }), fetch(newerUrl, { headers })];
+ const responses = await Promise.allSettled(requests);
+ const [olderRes, newerRes] = responses.filter(({ status }) => status === 'fulfilled').map(({ value }) => value);
if (!olderRes.ok || !newerRes.ok) return;
- const olderGranules = await olderRes.json();
- const newerGranules = await newerRes.json();
+ const jsonRequests = [olderRes.json(), newerRes.json()];
+ const jsonResponses = await Promise.allSettled(jsonRequests);
+ const [olderGranules, newerGranules] = jsonResponses.filter(({ status }) => status === 'fulfilled').map(({ value }) => value);
const olderEntries = olderGranules?.feed?.entry || [];
const newerEntries = newerGranules?.feed?.entry || [];
const granules = [...olderEntries, ...newerEntries];
@@ -164,6 +183,30 @@ function LayerRow (props) {
asyncFunc();
}, [map.extent, zot, selectedDate, isVisible]);
+ // hook that checks if the ddv layer zoom alert should be enabled or disabled
+ useEffect(() => {
+ const { title } = layer;
+ // if layer is ddv && layer IS NOT already in zoom alert list && zoom is at alertable level
+ if (isLayerNotificationDismissable && !ddvLayerZoomNoticeActive && showZoomAlert) {
+ enableDDVZoomAlert(title);
+ // if layer is ddv && layer IS already in zoom alert list && zoom is NOT at alertable level
+ } else if (isLayerNotificationDismissable && ddvLayerZoomNoticeActive && !showZoomAlert) {
+ disableDDVZoomAlert(title);
+ }
+ }, [showZoomAlert]);
+
+ // hook that checks if the ddv layer location alert should be enabled or disabled
+ useEffect(() => {
+ const { title } = layer;
+ // if layer is ddv && layer IS NOT already in location alert list && location is at alertable coordinates
+ if (isLayerNotificationDismissable && !ddvLayerLocationNoticeActive && showGranuleAlert) {
+ enableDDVLocationAlert(title);
+ // if layer is ddv && layer IS NOT already in location alert list && location is at alertable coordinates
+ } else if (isLayerNotificationDismissable && ddvLayerLocationNoticeActive && !showGranuleAlert) {
+ disableDDVLocationAlert(title);
+ }
+ }, [showGranuleAlert]);
+
useEffect(() => {
events.on(MAP_RUNNING_DATA, setRunningDataObj);
return () => {
@@ -209,6 +252,7 @@ function LayerRow (props) {
isEmbedModeActive={isEmbedModeActive}
isMobile={isMobile}
palettes={palettes}
+ showingVectorHand={isVectorLayer && isVisible}
/>
);
}
@@ -253,6 +297,21 @@ function LayerRow (props) {
e.preventDefault();
};
+ // function called on click when removing a layer
+ const removeLayer = () => {
+ const { id, title } = layer;
+ // remove ddv location alert
+ if (ddvLayerLocationNoticeActive) {
+ disableDDVLocationAlert(title);
+ }
+ // remove ddv zoom alert
+ if (ddvLayerZoomNoticeActive) {
+ disableDDVZoomAlert(title);
+ }
+ // remove layer
+ onRemoveClick(id);
+ };
+
const renderDropdownMenu = () => (
@@ -280,7 +339,7 @@ function LayerRow (props) {
onRemoveClick(layer.id)}
+ onClick={() => removeLayer()}
className="button wv-layers-options layer-options-dropdown-item"
>
{removeLayerBtnTitle}
@@ -297,7 +356,7 @@ function LayerRow (props) {
id={removeLayerBtnId}
aria-label={removeLayerBtnTitle}
className={isMobile ? 'hidden wv-layers-options' : 'button wv-layers-close'}
- onClick={() => onRemoveClick(layer.id)}
+ onClick={() => removeLayer()}
>
{removeLayerBtnTitle}
@@ -503,22 +562,24 @@ function LayerRow (props) {
))}
)}
- {showZoomAlert && !hideZoomAlert && (
+ {showZoomAlert && !hideZoomAlert && !isLayerNotificationDismissable && (