+
,
document.getElementById('app'),
);
- combineUi(models, config, store); // Legacy UI
+ // combineUi(models, config, store); // Legacy UI
util.errorReport(errors);
}
diff --git a/web/js/map/compare/swipe.js b/web/js/map/compare/swipe.js
index e2e62dd205..dd09075273 100644
--- a/web/js/map/compare/swipe.js
+++ b/web/js/map/compare/swipe.js
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import lodashRound from 'lodash/round';
import lodashEach from 'lodash/each';
+import { getRenderPixel } from 'ol/render';
import util from '../../util/util';
import { getCompareDates } from '../../modules/compare/selectors';
@@ -26,11 +27,11 @@ const restore = function(event) {
};
const applyListenersA = function(layer) {
- layer.on('prerender', this.clipA);
+ layer.on('prerender', this.setClipMaskA);
layer.on('postrender', restore);
};
const applyListenersB = function(layer) {
- layer.on('prerender', this.clipB);
+ layer.on('prerender', this.setClipMaskB);
layer.on('postrender', restore);
};
@@ -65,11 +66,11 @@ export default class Swipe {
destroy = () => {
mapCase.removeChild(line);
lodashEach(layersSideA, (layer) => {
- layer.un('prerender', this.clipA);
+ layer.un('prerender', this.setClipMaskA);
layer.un('postrender', restore);
});
lodashEach(layersSideB, (layer) => {
- layer.un('prerender', this.clipB);
+ layer.un('prerender', this.setClipMaskB);
layer.un('postrender', restore);
});
layersSideA = [];
@@ -110,35 +111,68 @@ export default class Swipe {
}
/**
- * Clip the reverse so users don't see this layerGroup when the other
- * Layer group is transparent
- * @param {Object} event | OL Precompose event object
+ * Set Clipping mask for the "A" side of a comparison.
+ * Note: The "B" side is layered above the "A" side on the DOM.
+ * We must mask the "A" side in case the B side has no imagery
+ * @param {Object} event | Openlayers Precompose event object
*/
- clipA = (event) => {
+ setClipMaskA = (event) => {
const ctx = event.context;
- const viewportWidth = event.frameState.size[0];
- const width = ctx.canvas.width * (1 - swipeOffset / viewportWidth);
- ctx.save();
- ctx.beginPath();
- ctx.rect(0, 0, ctx.canvas.width - width, ctx.canvas.height);
- ctx.clip();
+ const mapSize = this.map.getSize();
+ const widthSideA = mapSize[0] * percentSwipe;
+
+ const coordinates = {
+ topLeft: getRenderPixel(event, [0, 0]),
+ bottomLeft: getRenderPixel(event, [0, mapSize[1]]),
+ bottomRight: getRenderPixel(event, [widthSideA, mapSize[1]]),
+ topRight: getRenderPixel(event, [widthSideA, 0]),
+ };
+ setRectClipMask(ctx, coordinates);
}
/**
- * Clip the top layer at the right xOffset
- * @param {Object} event | OL Precompose event object
+ * Set Clipping mask for the "B" side of a comparison.
+ * Note: The "B" side is layered above the "A" side on the DOM.
+ * @param {Object} event | Openlayers Precompose event object
*/
- clipB = (event) => {
+ setClipMaskB = (event) => {
const ctx = event.context;
- const viewportWidth = event.frameState.size[0];
- const width = ctx.canvas.width * (swipeOffset / viewportWidth);
- ctx.save();
- ctx.beginPath();
- ctx.rect(width, 0, ctx.canvas.width - width, ctx.canvas.height);
- ctx.clip();
+ const mapSize = this.map.getSize();
+ const widthSideB = mapSize[0] * percentSwipe;
+ const coordinates = {
+ topLeft: getRenderPixel(event, [widthSideB, 0]),
+ bottomLeft: getRenderPixel(event, [widthSideB, mapSize[1]]),
+ bottomRight: getRenderPixel(event, mapSize),
+ topRight: getRenderPixel(event, [mapSize[0], 0]),
+ };
+ setRectClipMask(ctx, coordinates);
}
}
+/**
+ * Apply a rectangular clipping mask from the provided coordinates
+ * @param {CanvasRenderingContext2D} context | Canvas context requiring a clipping mask
+ * @param {Object} coordinates | Contains 4 points of the rectangle to be created
+ * @param {property} topLeft | Top Left positional coordinates in XY format
+ * @param {property} bottomLeft | Bottom Left positional coordinates in XY format
+ * @param {property} bottomRight | Bottom Right positional coordinates in XY format
+ * @param {property} topRight | Top Right positional coordinates in XY format
+ */
+const setRectClipMask = function(context, coordinates) {
+ const {
+ topLeft, bottomLeft, bottomRight, topRight,
+ } = coordinates;
+
+ context.save();
+ context.beginPath();
+ context.moveTo(topLeft[0], topLeft[1]);
+ context.lineTo(bottomLeft[0], bottomLeft[1]);
+ context.lineTo(bottomRight[0], bottomRight[1]);
+ context.lineTo(topRight[0], topRight[1]);
+ context.closePath();
+ context.clip();
+};
+
/**
* Add Swiper
* @param {Object} map | OL map object
diff --git a/web/js/map/layerbuilder.js b/web/js/map/layerbuilder.js
index 65dc8e0c8e..d8b17703a5 100644
--- a/web/js/map/layerbuilder.js
+++ b/web/js/map/layerbuilder.js
@@ -5,8 +5,8 @@ import OlSourceWMTS from 'ol/source/WMTS';
import OlSourceTileWMS from 'ol/source/TileWMS';
import OlLayerGroup from 'ol/layer/Group';
import OlLayerTile from 'ol/layer/Tile';
+import TileState from 'ol/TileState';
import OlTileGridTileGrid from 'ol/tilegrid/TileGrid';
-
import MVT from 'ol/format/MVT';
import LayerVectorTile from 'ol/layer/VectorTile';
import SourceVectorTile from 'ol/source/VectorTile';
@@ -14,12 +14,15 @@ import lodashCloneDeep from 'lodash/cloneDeep';
import lodashMerge from 'lodash/merge';
import lodashEach from 'lodash/each';
import lodashGet from 'lodash/get';
+
import util from '../util/util';
import lookupFactory from '../ol/lookupimagetile';
import granuleLayerBuilder from './granule/granule-layer-builder';
import { getGranuleTileLayerExtent } from './granule/util';
import { createVectorUrl, getGeographicResolutionWMS, mergeBreakpointLayerAttributes } from './util';
import { datesInDateRanges, prevDateInDateRange } from '../modules/layers/util';
+import { updateLayerDateCollection, updateLayerCollection } from '../modules/layers/actions';
+import { getCollections } from '../modules/layers/selectors';
import { getSelectedDate } from '../modules/date/selectors';
import {
isActive as isPaletteActive,
@@ -39,11 +42,9 @@ import {
LEFT_WING_EXTENT, RIGHT_WING_EXTENT, LEFT_WING_ORIGIN, RIGHT_WING_ORIGIN, CENTER_MAP_ORIGIN,
} from '../modules/map/constants';
-
export default function mapLayerBuilder(config, cache, store) {
const { getGranuleLayer } = granuleLayerBuilder(cache, store, createLayerWMTS);
-
/**
* Return a layer, or layergroup, created with the supplied function
* @param {*} createLayerFunc
@@ -86,6 +87,61 @@ export default function mapLayerBuilder(config, cache, store) {
};
};
+ const updateStoreCollectionDates = (id, version, type, date) => {
+ store.dispatch(updateLayerDateCollection({
+ id,
+ date,
+ collection: {
+ version,
+ type,
+ },
+ }));
+ };
+
+ const updateStoreCollections = (id) => {
+ store.dispatch(updateLayerCollection(id));
+ };
+
+ /**
+ * We define our own tile loading function in order to capture custom header values
+ *
+ * @param {*} tile
+ * @param {*} src
+ */
+ const tileLoadFunction = (layer, layerDate) => async function(tile, src) {
+ const date = layerDate.toISOString().split('T')[0];
+ let actualId;
+
+ const updateCollections = (headers) => {
+ actualId = headers.get('layer-identifier-actual');
+ if (!actualId) return;
+ const state = store.getState();
+ const { layers } = state;
+ const collectionCheck = getCollections(layers, date, layer);
+ // check if the collection & dates already exist for layer so we don't dispatch actions
+ if (!collectionCheck) {
+ updateStoreCollections(layer.id);
+ const parts = actualId.split('_');
+ const version = parts[parts.length - 2];
+ const type = parts[parts.length - 1];
+ updateStoreCollectionDates(layer.id, version, type, date);
+ }
+ };
+
+ try {
+ const response = await fetch(src);
+ const data = await response.blob();
+ updateCollections(response.headers);
+ if (data !== undefined) {
+ tile.getImage().src = URL.createObjectURL(data);
+ } else {
+ tile.setState(TileState.ERROR);
+ }
+ } catch (e) {
+ tile.setState(TileState.ERROR);
+ }
+ };
+
/**
* Create a new OpenLayers Layer
* @param {object} def
@@ -419,6 +475,7 @@ export default function mapLayerBuilder(config, cache, store) {
tileGrid: new OlTileGridWMTS(tileGridOptions),
wrapX: false,
style: typeof style === 'undefined' ? 'default' : style,
+ tileLoadFunction: tileLoadFunction(def, layerDate),
};
if (isPaletteActive(id, options.group, state)) {
const lookup = getPaletteLookup(id, options.group, state);
diff --git a/web/js/map/ui.js b/web/js/map/ui.js
deleted file mode 100644
index 3febaae29c..0000000000
--- a/web/js/map/ui.js
+++ /dev/null
@@ -1,1285 +0,0 @@
-/* eslint-disable no-multi-assign */
-/* eslint-disable no-shadow */
-/* eslint-disable no-param-reassign */
-/* eslint-disable no-nested-ternary */
-import {
- throttle as lodashThrottle,
- forOwn as lodashForOwn,
- each as lodashEach,
- findIndex as lodashFindIndex,
- get as lodashGet,
- debounce as lodashDebounce,
- cloneDeep as lodashCloneDeep,
- find as lodashFind,
-} from 'lodash';
-import OlMap from 'ol/Map';
-import OlView from 'ol/View';
-import OlKinetic from 'ol/Kinetic';
-import OlControlScaleLine from 'ol/control/ScaleLine';
-import { altKeyOnly } from 'ol/events/condition';
-import OlInteractionPinchRotate from 'ol/interaction/PinchRotate';
-import OlInteractionDragRotate from 'ol/interaction/DragRotate';
-import OlInteractionDoubleClickZoom from 'ol/interaction/DoubleClickZoom';
-import OlInteractionPinchZoom from 'ol/interaction/PinchZoom';
-import OlInteractionDragPan from 'ol/interaction/DragPan';
-import OlInteractionMouseWheelZoom from 'ol/interaction/MouseWheelZoom';
-import OlInteractionDragZoom from 'ol/interaction/DragZoom';
-import OlLayerGroup from 'ol/layer/Group';
-import * as olProj from 'ol/proj';
-import Cache from 'cachai';
-// eslint-disable-next-line import/no-unresolved
-import PQueue from 'p-queue';
-import { SET_SCREEN_INFO } from '../modules/screen-size/constants';
-import mapLayerBuilder from './layerbuilder';
-import MapRunningData from './runningdata';
-import { fly, saveRotation } from './util';
-import mapCompare from './compare/compare';
-import { granuleFootprint } from './granule/util';
-import { LOCATION_POP_ACTION } from '../redux-location-state-customs';
-import { CHANGE_PROJECTION } from '../modules/projection/constants';
-import {
- REMOVE_MARKER,
- SET_MARKER,
- TOGGLE_DIALOG_VISIBLE,
-} from '../modules/location-search/constants';
-import { setGeocodeResults, removeMarker } from '../modules/location-search/actions';
-import * as dateConstants from '../modules/date/constants';
-import util from '../util/util';
-import * as layerConstants from '../modules/layers/constants';
-import * as compareConstants from '../modules/compare/constants';
-import * as paletteConstants from '../modules/palettes/constants';
-import * as vectorStyleConstants from '../modules/vector-styles/constants';
-import { setStyleFunction } from '../modules/vector-styles/selectors';
-import {
- getLayers,
- getActiveLayers,
- getActiveLayerGroup,
- isRenderable as isRenderableLayer,
- getMaxZoomLevelLayerCollection,
- getAllActiveLayers,
- getGranuleCount,
- getGranuleLayer,
- getActiveGranuleFootPrints,
-} from '../modules/layers/selectors';
-import { getSelectedDate } from '../modules/date/selectors';
-import { getNumberStepsBetween, getNextDateTime } from '../modules/date/util';
-import { EXIT_ANIMATION, STOP_ANIMATION } from '../modules/animation/constants';
-import {
- RENDERED, UPDATE_MAP_UI, UPDATE_MAP_EXTENT, UPDATE_MAP_ROTATION, FITTED_TO_LEADING_EXTENT, REFRESH_ROTATE, CLEAR_ROTATE,
-} from '../modules/map/constants';
-import { getLeadingExtent, promiseImageryForTime } from '../modules/map/util';
-import { updateVectorSelection } from '../modules/vector-styles/util';
-import { animateCoordinates, getCoordinatesMarker, areCoordinatesWithinExtent } from '../modules/location-search/util';
-import { getNormalizedCoordinate } from '../components/location-search/util';
-import { reverseGeocode } from '../modules/location-search/util-api';
-import { startLoading, stopLoading, MAP_LOADING } from '../modules/loading/actions';
-import {
- MAP_DISABLE_CLICK_ZOOM,
- MAP_ENABLE_CLICK_ZOOM,
- REDUX_ACTION_DISPATCHED,
- GRANULE_HOVERED,
- GRANULE_HOVER_UPDATE,
- MAP_DRAG,
- MAP_MOUSE_MOVE,
- MAP_MOUSE_OUT,
- MAP_MOVE_START,
- MAP_ZOOMING,
-} from '../util/constants';
-
-const { events } = util;
-
-export default function mapui(models, config, store) {
- const animationDuration = 250;
- const granuleFootprints = {};
- const compareMapUi = mapCompare(store);
- const runningdata = new MapRunningData(compareMapUi, store);
- const doubleClickZoom = new OlInteractionDoubleClickZoom({
- duration: animationDuration,
- });
- const cache = new Cache(400);
- const layerQueue = new PQueue({ concurrency: 3 });
- const { createLayer, layerKey } = mapLayerBuilder(config, cache, store);
- const self = {
- cache,
- mapIsbeingDragged: false,
- mapIsbeingZoomed: false,
- proj: {}, // One map for each projection
- selected: null, // The map for the selected projection
- selectedVectors: {},
- markers: [],
- runningdata,
- layerKey,
- createLayer,
- processingPromise: null,
- };
-
- /**
- * Subscribe to redux store and listen for
- * specific action types
- */
- const subscribeToStore = function(action) {
- const state = store.getState();
- switch (action.type) {
- case layerConstants.UPDATE_GRANULE_LAYER_OPTIONS: {
- const granuleOptions = {
- id: action.id,
- reset: null,
- };
- return reloadLayers(granuleOptions);
- }
- case layerConstants.RESET_GRANULE_LAYER_OPTIONS: {
- const granuleOptions = {
- id: action.id,
- reset: action.id,
- };
- return reloadLayers(granuleOptions);
- }
- case layerConstants.ADD_LAYER: {
- const def = lodashFind(action.layers, { id: action.id });
- if (def.type === 'granule') {
- self.processingPromise = new Promise((resolve) => {
- resolve(addLayer(def));
- });
- return self.processingPromise;
- }
- store.dispatch({ type: dateConstants.CLEAR_PRELOAD });
- return addLayer(def);
- }
- case REMOVE_MARKER:
- return removeCoordinatesMarker(action.coordinates);
- case SET_MARKER: {
- if (action.flyToExistingMarker) {
- return flyToMarker(action.coordinates);
- }
- return addMarkerAndUpdateStore(true, action.reverseGeocodeResults, action.isCoordinatesSearchActive, action.coordinates);
- }
- case TOGGLE_DIALOG_VISIBLE:
- return addMarkerAndUpdateStore(false);
- case CLEAR_ROTATE: {
- self.selected.getView().animate({
- duration: 500,
- rotation: 0,
- });
- return;
- }
- case REFRESH_ROTATE: {
- self.selected.getView().animate({
- rotation: action.rotation,
- duration: 500,
- });
- return;
- }
- case LOCATION_POP_ACTION: {
- const newState = util.fromQueryString(action.payload.search);
- const extent = lodashGet(state, 'map.extent');
- const rotate = lodashGet(state, 'map.rotation') || 0;
- setTimeout(() => {
- updateProjection();
- if (newState.v && !newState.e && extent) {
- flyToNewExtent(extent, rotate);
- }
- }, 200);
- return;
- }
- case layerConstants.REMOVE_GROUP:
- case layerConstants.REMOVE_LAYER:
- return removeLayer(action.layersToRemove);
- case layerConstants.UPDATE_OPACITY:
- return updateOpacity(action);
- case compareConstants.CHANGE_STATE:
- if (store.getState().compare.mode === 'spy') {
- return reloadLayers();
- }
- return;
- case layerConstants.TOGGLE_OVERLAY_GROUPS:
- return reloadLayers();
- case layerConstants.REORDER_LAYERS:
- case layerConstants.REORDER_OVERLAY_GROUPS:
- case compareConstants.TOGGLE_ON_OFF:
- case compareConstants.CHANGE_MODE:
- reloadLayers();
- preloadForCompareMode();
- return;
- case CHANGE_PROJECTION:
- return updateProjection();
- case paletteConstants.SET_THRESHOLD_RANGE_AND_SQUASH:
- case paletteConstants.SET_CUSTOM:
- case paletteConstants.SET_DISABLED_CLASSIFICATION:
- case paletteConstants.CLEAR_CUSTOM:
- case layerConstants.ADD_LAYERS_FOR_EVENT:
- return setTimeout(reloadLayers, 100);
- case vectorStyleConstants.SET_FILTER_RANGE:
- case vectorStyleConstants.SET_VECTORSTYLE:
- case vectorStyleConstants.CLEAR_VECTORSTYLE:
- case SET_SCREEN_INFO:
- return onResize();
- case vectorStyleConstants.SET_SELECTED_VECTORS: {
- const type = 'selection';
- const newSelection = action.payload;
- updateVectorSelection(
- action.payload,
- self.selectedVectors,
- getActiveLayers(state),
- type,
- state,
- );
- self.selectedVectors = newSelection;
- return;
- }
- case STOP_ANIMATION:
- case EXIT_ANIMATION:
- return onStopAnimation();
- case dateConstants.CHANGE_CUSTOM_INTERVAL:
- case dateConstants.CHANGE_INTERVAL:
- return preloadNextTiles();
- case dateConstants.SELECT_DATE:
- if (self.processingPromise) {
- return new Promise((resolve) => {
- resolve(self.processingPromise);
- }).then(() => {
- self.processingPromise = null;
- return updateDate(action.outOfStep);
- });
- }
- return updateDate(action.outOfStep);
- case layerConstants.TOGGLE_LAYER_VISIBILITY:
- case layerConstants.TOGGLE_OVERLAY_GROUP_VISIBILITY: {
- updateDate();
- break;
- }
- case dateConstants.ARROW_DOWN:
- bufferQuickAnimate(action.value);
- break;
- default:
- break;
- }
- };
-
- const onGranuleHover = (platform, date, update) => {
- const state = store.getState();
- const proj = self.selected.getView().getProjection().getCode();
- let geometry;
- if (platform && date) {
- geometry = getActiveGranuleFootPrints(state)[date];
- }
- granuleFootprints[proj].addFootprint(geometry, date);
- };
-
- const onGranuleHoverUpdate = (platform, date) => {
- const state = store.getState();
- const proj = self.selected.getView().getProjection().getCode();
- let geometry;
- if (platform && date) {
- geometry = getActiveGranuleFootPrints(state)[date];
- }
- granuleFootprints[proj].updateFootprint(geometry, date);
- };
-
- /**
- * During animation we swap Vector tiles for WMS for better performance.
- * Once animation completes, we need to call reloadLayers to reload and replace
- * the WMS tiles with Vector tiles.
- *
- * We also disable granule layer state updates due to performance reasons and so
- * need to trigger a layer state update once animation fisnishes.
- */
- const onStopAnimation = function() {
- const state = store.getState();
- const activeLayers = getActiveLayers(state);
- const needsRefresh = activeLayers.some(({ type }) => type === 'granule' || type === 'vector');
- if (needsRefresh) {
- // The SELECT_DATE and STOP_ANIMATION actions happen back to back and both
- // try to modify map layers asynchronously so we need to set a timeout to allow
- // the updateDate() function to complete before trying to call reloadLayers() here
- setTimeout(reloadLayers, 100);
- }
- };
-
- const init = function() {
- // NOTE: iOS sometimes bombs if this is _.each instead. In that case,
- // it is possible that config.projections somehow becomes array-like.
- lodashForOwn(config.projections, (proj) => {
- const map = createMap(proj);
- self.proj[proj.id] = map;
- });
- events.on(MAP_DISABLE_CLICK_ZOOM, () => {
- doubleClickZoom.setActive(false);
- });
- events.on(MAP_ENABLE_CLICK_ZOOM, () => {
- setTimeout(() => {
- doubleClickZoom.setActive(true);
- }, 100);
- });
- events.on(REDUX_ACTION_DISPATCHED, subscribeToStore);
- events.on(GRANULE_HOVERED, onGranuleHover);
- events.on(GRANULE_HOVER_UPDATE, onGranuleHoverUpdate);
- window.addEventListener('orientationchange', () => {
- setTimeout(() => { updateProjection(true); }, 200);
- });
- updateProjection(true);
- };
-
-
-
- /*
- * Remove coordinates marker from all projections
- *
- * @method removeCoordinatesMarker
- * @static
- *
- * @returns {void}
- */
- const removeCoordinatesMarker = (coordinatesObject) => {
- self.markers.forEach((marker) => {
- if (marker.id === coordinatesObject.id) {
- marker.setMap(null);
- self.selected.removeOverlay(marker);
- }
- });
- };
-
- /*
- * Remove all coordinates markers
- *
- * @method removeAllCoordinatesMarkers
- * @static
- *
- * @returns {void}
- */
- const removeAllCoordinatesMarkers = () => {
- self.markers.forEach((marker) => {
- marker.setMap(null);
- self.selected.removeOverlay(marker);
- });
- };
-
- /*
- * Handle reverse geocode and add map marker with results
- *
- * @method handleActiveMapMarker
- * @static
- *
- * @returns {void}
- */
- const handleActiveMapMarker = () => {
- const state = store.getState();
- const { locationSearch, proj } = state;
- const { coordinates } = locationSearch;
- removeAllCoordinatesMarkers();
-
- if (coordinates && coordinates.length > 0) {
- coordinates.forEach((coordinatesObject) => {
- const { longitude, latitude } = coordinatesObject;
- const coord = [longitude, latitude];
- if (!areCoordinatesWithinExtent(proj, coord)) return;
- reverseGeocode(getNormalizedCoordinate(coord), config).then((results) => {
- addMarkerAndUpdateStore(true, results, null, coordinatesObject);
- });
- });
- }
- };
-
- const flyToMarker = (coordinatesObject) => {
- const state = store.getState();
- const { proj } = state;
- const { sources } = config;
- const { longitude, latitude } = coordinatesObject;
- const latestCoordinates = coordinatesObject && [longitude, latitude];
- const zoom = self.selected.getView().getZoom();
- const activeLayers = getActiveLayers(state).filter(({ projections }) => projections[proj.id]);
- const maxZoom = getMaxZoomLevelLayerCollection(activeLayers, zoom, proj.id, sources);
- animateCoordinates(self.selected, proj, latestCoordinates, maxZoom);
- };
-
- /*
- * Add map coordinate marker and update store
- *
- * @method addMarkerAndUpdateStore
- * @static
- *
- * @param {Object} geocodeResults
- * @param {Boolean} shouldFlyToCoordinates - if location search via input
- * @returns {void}
- */
- const addMarkerAndUpdateStore = (showDialog, geocodeResults, shouldFlyToCoordinates, coordinatesObject) => {
- const state = store.getState();
- const { proj, screenSize } = state;
- const results = geocodeResults;
- if (!results) return;
-
- const remove = () => store.dispatch(removeMarker(coordinatesObject));
- const marker = getCoordinatesMarker(
- proj,
- coordinatesObject,
- results,
- remove,
- screenSize.isMobileDevice,
- showDialog,
- );
-
- // prevent marker if outside of extent
- if (!marker) {
- return false;
- }
-
- self.markers.push(marker);
- self.selected.addOverlay(marker);
- self.selected.renderSync();
-
- if (shouldFlyToCoordinates) {
- flyToMarker(coordinatesObject);
- }
-
- store.dispatch(setGeocodeResults(geocodeResults));
- };
-
- const flyToNewExtent = function(extent, rotation) {
- const state = store.getState();
- const { proj } = state;
- const coordinateX = extent[0] + (extent[2] - extent[0]) / 2;
- const coordinateY = extent[1] + (extent[3] - extent[1]) / 2;
- const coordinates = [coordinateX, coordinateY];
- const resolution = self.selected.getView().getResolutionForExtent(extent);
- const zoom = self.selected.getView().getZoomForResolution(resolution);
- // Animate to extent, zoom & rotate:
- // Don't animate when an event is selected (Event selection already animates)
- return fly(self.selected, proj, coordinates, zoom, rotation);
- };
-
- /*
- * Changes visual projection
- *
- * @method updateProjection
- * @static
- *
- * @param {boolean} start - new extents are needed: true/false
- *
- * @returns {void}
- */
- function updateProjection(start) {
- const state = store.getState();
- const { proj } = state;
- if (self.selected) {
- // Keep track of center point on projection switch
- self.selected.previousCenter = self.selected.center;
- hideMap(self.selected);
- }
- self.selected = self.proj[proj.id];
- const map = self.selected;
-
- const isProjectionRotatable = proj.id !== 'geographic' && proj.id !== 'webmerc';
- const currentRotation = isProjectionRotatable ? map.getView().getRotation() : 0;
- const rotationStart = isProjectionRotatable ? models.map.rotation : 0;
-
- store.dispatch({
- type: UPDATE_MAP_UI,
- ui: self,
- rotation: start ? rotationStart : currentRotation,
- });
- reloadLayers();
-
- // If the browser was resized, the inactive map was not notified of
- // the event. Force the update no matter what and reposition the center
- // using the previous value.
- showMap(map);
- map.updateSize();
-
- if (self.selected.previousCenter) {
- self.selected.setCenter(self.selected.previousCenter);
- }
- // This is awkward and needs a refactoring
- if (start) {
- const projId = proj.selected.id;
- let extent = null;
- let callback = null;
- if (models.map.extent) {
- extent = models.map.extent;
- } else if (!models.map.extent && projId === 'geographic') {
- extent = getLeadingExtent(config.pageLoadTime);
- callback = () => {
- const view = map.getView();
- const extent = view.calculateExtent(map.getSize());
- store.dispatch({ type: FITTED_TO_LEADING_EXTENT, extent });
- };
- }
- if (projId !== 'geographic') {
- callback = () => {
- const view = map.getView();
- view.setRotation(rotationStart);
- };
- }
- if (extent) {
- map.getView().fit(extent, {
- constrainResolution: false,
- callback,
- });
- } else if (rotationStart && projId !== 'geographic') {
- callback();
- }
- }
- updateExtent();
- onResize();
- handleActiveMapMarker(start);
- }
-
- /*
- * When page is resized set for mobile or desktop
- *
- * @method onResize
- * @static
- *
- * @returns {void}
- */
- function onResize() {
- const state = store.getState();
- const { screenSize } = state;
- const isMobile = screenSize.isMobileDevice;
- const map = self.selected;
-
- if (isMobile) {
- map.removeControl(map.wv.scaleImperial);
- map.removeControl(map.wv.scaleMetric);
- } else {
- map.addControl(map.wv.scaleImperial);
- map.addControl(map.wv.scaleMetric);
- }
- }
-
- /*
- * Hide Map
- *
- * @method hideMap
- * @static
- *
- * @param {object} map - Openlayers Map obj
- *
- * @returns {void}
- */
- function hideMap(map) {
- document.getElementById(`${map.getTarget()}`).style.display = 'none';
- }
-
- /*
- * Show Map
- *
- * @method showMap
- * @static
- *
- * @param {object} map - Openlayers Map obj
- *
- * @returns {void}
- */
- function showMap(map) {
- document.getElementById(`${map.getTarget()}`).style.display = 'block';
- }
-
- /*
- * Remove Layers from map
- *
- * @method clearLayers
- * @static
- *
- * @param {object} map - Openlayers Map obj
- *
- * @returns {void}
- */
- const clearLayers = function() {
- const activeLayers = self.selected
- .getLayers()
- .getArray()
- .slice(0);
- lodashEach(activeLayers, (mapLayer) => {
- self.selected.removeLayer(mapLayer);
- });
- cache.clear();
- };
-
- /**
- * Get granule options for layerBuilding
- * @param {object} state
- * @param {Object} def
- * @param {String} layerGroupStr
- * @param {Object} options
- * @returns {Object}
- */
- const getGranuleOptions = (state, { id, count, type }, activeString, options) => {
- if (type !== 'granule') return {};
- const reset = options && options.reset === id;
-
- // TODO update
- const granuleState = getGranuleLayer(state, id, activeString);
- let granuleDates;
- let granuleCount;
- let geometry;
- if (granuleState) {
- granuleDates = !reset ? granuleState.dates : false;
- granuleCount = granuleState.count;
- geometry = granuleState.geometry;
- }
- return {
- granuleDates,
- granuleCount: granuleCount || count,
- geometry,
- };
- };
-
- /**
- * @method reloadLayers
- *
- * @param {object} map - Openlayers Map obj
- * @param {Object} granuleOptions (optional: only used for granule layers)
- * @param {Boolean} granuleDates - array of granule dates
- * @param {Boolean} id - layer id
- * @param {boolean} start - indicate init load
- * @returns {void}
- */
-
- async function reloadLayers(granuleOptions) {
- const map = self.selected;
- const state = store.getState();
- const { compare } = state;
-
- if (!config.features.compare || !compare.active) {
- const compareMapDestroyed = !compare.active && compareMapUi.active;
- if (compareMapDestroyed) {
- compareMapUi.destroy();
- }
- clearLayers();
- const defs = getLayers(state, { reverse: true });
- const layerPromises = defs.map((def) => {
- const options = getGranuleOptions(state, def, compare.activeString, granuleOptions);
- return createLayer(def, options);
- });
- const createdLayers = await Promise.all(layerPromises);
- lodashEach(createdLayers, (l) => { map.addLayer(l); });
- } else {
- const stateArray = [['active', 'selected'], ['activeB', 'selectedB']];
- if (compare && !compare.isCompareA && compare.mode === 'spy') {
- stateArray.reverse(); // Set Layer order based on active A|B group
- }
- clearLayers();
- const stateArrayGroups = stateArray.map(async (arr) => getCompareLayerGroup(arr, state, granuleOptions));
- const compareLayerGroups = await Promise.all(stateArrayGroups);
- compareLayerGroups.forEach((layerGroup) => map.addLayer(layerGroup));
- compareMapUi.create(map, compare.mode);
- }
- updateLayerVisibilities();
- }
-
-
- /**
- * Create a Layergroup given the date and layerGroups
- */
- async function getCompareLayerGroup([compareActiveString, compareDateString], state, granuleOptions) {
- const compareSideLayers = getActiveLayers(state, compareActiveString);
- const layers = getLayers(state, { reverse: true }, compareSideLayers)
- .map(async (def) => {
- const options = {
- ...getGranuleOptions(state, def, compareActiveString, granuleOptions),
- date: getSelectedDate(state, compareDateString),
- group: compareActiveString,
- };
- return createLayer(def, options);
- });
- const compareLayerGroup = await Promise.all(layers);
-
- return new OlLayerGroup({
- layers: compareLayerGroup,
- date: getSelectedDate(state, compareDateString),
- group: compareActiveString,
- });
- }
-
- /*
- * Function called when layers need to be updated
- * e.g: can be the result of new data or another display
- *
- * @method updateLayerVisibilities
- * @static
- *
- * @returns {void}
- */
- function updateLayerVisibilities() {
- const state = store.getState();
- const layerGroup = self.selected.getLayers();
-
- const setRenderable = (layer, parentCompareGroup) => {
- const { id, group } = layer.wv;
- const dateGroup = layer.get('date') || group === 'active' ? 'selected' : 'selectedB';
- const date = getSelectedDate(state, dateGroup);
- const layers = getActiveLayers(state, parentCompareGroup || group);
- const renderable = isRenderableLayer(id, layers, date, null, state);
- layer.setVisible(renderable);
- };
-
- layerGroup.forEach((layer) => {
- const compareActiveString = layer.get('group');
- const granule = layer.get('granuleGroup');
-
- // Not in A|B
- if (layer.wv && !granule) {
- setRenderable(layer);
-
- // If in A|B layer-group will have a 'group' string
- } else if (compareActiveString || granule) {
- const compareGrouplayers = layer.getLayers().getArray();
-
- compareGrouplayers.forEach((subLayer) => {
- if (!subLayer.wv) {
- return;
- }
- // TileLayers within granule LayerGroup
- if (subLayer.get('granuleGroup')) {
- const granuleLayers = subLayer.getLayers().getArray();
- granuleLayers.forEach((l) => setRenderable(l));
- subLayer.setVisible(true);
- }
- setRenderable(subLayer, compareActiveString);
- });
-
- layer.setVisible(true);
- }
- });
- }
-
- /*
- * Sets new opacity to granule layers
- *
- * @method updateGranuleLayerOpacity
- * @static
- *
- * @param {object} def
- * @param {sring} activeStr
- * @param {number} opacity
- * @param {object} compare
- *
- * @returns {void}
- */
- const updateGranuleLayerOpacity = (def, activeStr, opacity, compare) => {
- const { id } = def;
- const layers = self.selected.getLayers().getArray();
- lodashEach(Object.keys(layers), (index) => {
- const layer = layers[index];
- if (layer.className_ === 'ol-layer') {
- if (compare && compare.active) {
- const layerGroup = layer.getLayers().getArray();
- lodashEach(Object.keys(layerGroup), (groupIndex) => {
- const compareLayerGroup = layerGroup[groupIndex];
- if (compareLayerGroup.wv.id === id) {
- const tileLayer = compareLayerGroup.getLayers().getArray();
-
- // inner first granule group tile layer
- const firstTileLayer = tileLayer[0];
- if (firstTileLayer.wv.id === id) {
- if (firstTileLayer.wv.group === activeStr) {
- compareLayerGroup.setOpacity(opacity);
- }
- }
- }
- });
- } else if (layer.wv.id === id) {
- if (layer.wv.group === activeStr) {
- layer.setOpacity(opacity);
- }
- }
- }
- });
- };
-
- /**
- * Sets new opacity to layer
- * @param {object} def - layer Specs
- * @param {number} value - number value
- * @returns {void}
- */
- function updateOpacity(action) {
- const { id, opacity } = action;
- const state = store.getState();
- const { compare } = state;
- const activeStr = compare.isCompareA ? 'active' : 'activeB';
- const def = lodashFind(getActiveLayers(state), { id });
- if (def.type === 'granule') {
- updateGranuleLayerOpacity(def, activeStr, opacity, compare);
- } else {
- const layerGroup = findLayer(def, activeStr);
- layerGroup.getLayersArray().forEach((l) => {
- l.setOpacity(opacity);
- });
- }
- updateLayerVisibilities();
- }
-
- /**
- * Initiates the adding of a layer
- * @param {object} def - layer Specs
- * @returns {void}
- */
- const addLayer = async function(def, date, activeLayers) {
- const state = store.getState();
- const { compare } = state;
- date = date || getSelectedDate(state);
- activeLayers = activeLayers || getActiveLayers(state);
- const reverseLayers = lodashCloneDeep(activeLayers).reverse();
- const index = lodashFindIndex(reverseLayers, { id: def.id });
- const mapLayers = self.selected.getLayers().getArray();
- const firstLayer = mapLayers[0];
-
- if (firstLayer && firstLayer.get('group') && firstLayer.get('granule') !== true) {
- const activelayer = firstLayer.get('group') === compare.activeString
- ? firstLayer
- : mapLayers[1];
- const options = {
- date,
- group: compare.activeString,
- };
- const newLayer = await createLayer(def, options);
- activelayer.getLayers().insertAt(index, newLayer);
- compareMapUi.create(self.selected, compare.mode);
- } else {
- const newLayer = await createLayer(def);
- self.selected.getLayers().insertAt(index, newLayer);
- }
-
- updateLayerVisibilities();
- preloadNextTiles();
- };
-
-
- function removeLayer(layersToRemove) {
- const state = store.getState();
- const { compare } = state;
-
- layersToRemove.forEach((def) => {
- const layer = findLayer(def, compare.activeString);
- if (compare && compare.active) {
- const layerGroup = getActiveLayerGroup(state);
- if (layerGroup) layerGroup.getLayers().remove(layer);
- } else {
- self.selected.removeLayer(layer);
- }
- });
-
- updateLayerVisibilities();
- }
-
- function updateVectorStyles (def) {
- const state = store.getState();
- const activeLayers = getActiveLayers(state);
- const { vectorStyles } = config;
- const layerName = def.layer || def.id;
- let vectorStyleId;
-
- vectorStyleId = def.vectorStyle.id;
- if (activeLayers) {
- activeLayers.forEach((layer) => {
- if (layer.id === layerName && layer.custom) {
- vectorStyleId = layer.custom;
- }
- });
- }
- setStyleFunction(def, vectorStyleId, vectorStyles, null, state);
- }
-
- async function updateCompareLayer (def, index, layerCollection) {
- const state = store.getState();
- const { compare } = state;
- const options = {
- group: compare.activeString,
- date: getSelectedDate(state),
- ...getGranuleOptions(state, def, compare.activeString),
- };
- const updatedLayer = await createLayer(def, options);
- layerCollection.setAt(index, updatedLayer);
- compareMapUi.update(compare.activeString);
- }
-
- async function updateDate(outOfStepChange) {
- const state = store.getState();
- const { compare = {} } = state;
- const layerGroup = getActiveLayerGroup(state);
- const mapLayerCollection = layerGroup.getLayers();
- const layers = mapLayerCollection.getArray();
- const activeLayers = getAllActiveLayers(state);
- const visibleLayers = activeLayers.filter(
- ({ id }) => layers
- .map(({ wv }) => lodashGet(wv, 'def.id'))
- .includes(id),
- ).filter(({ visible }) => visible);
-
- const layerPromises = visibleLayers.map(async (def) => {
- const { id, type } = def;
- const temporalLayer = ['subdaily', 'daily', 'monthly', 'yearly']
- .includes(def.period);
- const index = findLayerIndex(def);
- const hasVectorStyles = config.vectorStyles && lodashGet(def, 'vectorStyle.id');
-
- if (compare.active && layers.length) {
- await updateCompareLayer(def, index, mapLayerCollection);
- } else if (temporalLayer) {
- if (index !== undefined && index !== -1) {
- const layerValue = layers[index];
- const layerOptions = type === 'granule'
- ? { granuleCount: getGranuleCount(state, id) }
- : { previousLayer: layerValue ? layerValue.wv : null };
- const updatedLayer = await createLayer(def, layerOptions);
- mapLayerCollection.setAt(index, updatedLayer);
- }
- }
- if (hasVectorStyles && temporalLayer) {
- updateVectorStyles(def);
- }
- });
- await Promise.all(layerPromises);
- updateLayerVisibilities();
- if (!outOfStepChange) {
- preloadNextTiles();
- }
- }
-
- /**
- * Preload tiles for the next and previous time interval so they are visible
- * as soon as the user changes the date. We will usually only end up actually requesting
- * either previous or next interval tiles since tiles are cached.
- * (e.g. user adjust from July 1 => July 2, we preload July 3 which is "next"
- * but no requests get made for "previous", July 1, since those are cached already.
- */
- async function preloadNextTiles(date, compareString) {
- const state = store.getState();
- const {
- lastPreloadDate, preloaded, lastArrowDirection, arrowDown,
- } = state.date;
- const { activeString } = state.compare;
- const useActiveString = compareString || activeString;
- const useDate = date || (preloaded ? lastPreloadDate : getSelectedDate(state));
- const nextDate = getNextDateTime(state, 1, useDate);
- const prevDate = getNextDateTime(state, -1, useDate);
- const subsequentDate = lastArrowDirection === 'right' ? nextDate : prevDate;
-
- // If we've preloaded N dates out, we need to use the latest
- // preloaded date the next time we call this function or the buffer
- // won't stay ahead of the 'animation' when holding down timetep arrows
- if (preloaded && lastArrowDirection) {
- store.dispatch({
- type: dateConstants.SET_PRELOAD,
- preloaded: true,
- lastPreloadDate: subsequentDate,
- });
- layerQueue.add(() => promiseImageryForTime(state, subsequentDate, useActiveString));
- return;
- }
-
- layerQueue.add(() => promiseImageryForTime(state, nextDate, useActiveString));
- layerQueue.add(() => promiseImageryForTime(state, prevDate, useActiveString));
-
- if (!date && !arrowDown) {
- preloadNextTiles(subsequentDate, useActiveString);
- }
- }
-
- function preloadForCompareMode() {
- const { date, compare } = store.getState();
- const { selected, selectedB } = date;
- preloadNextTiles(selected, 'active');
- if (compare.active) {
- preloadNextTiles(selectedB, 'activeB');
- }
- }
-
- async function bufferQuickAnimate(arrowDown) {
- const BUFFER_SIZE = 8;
- const preloadPromises = [];
- const state = store.getState();
- const { preloaded, lastPreloadDate } = state.date;
- const selectedDate = getSelectedDate(state);
- const currentBuffer = preloaded ? getNumberStepsBetween(state, selectedDate, lastPreloadDate) : 0;
-
- if (currentBuffer >= BUFFER_SIZE) {
- return;
- }
-
- const currentDate = preloaded ? lastPreloadDate : selectedDate;
- const direction = arrowDown === 'right' ? 1 : -1;
- let nextDate = getNextDateTime(state, direction, currentDate);
-
- for (let step = 1; step <= BUFFER_SIZE; step += 1) {
- preloadPromises.push(promiseImageryForTime(state, nextDate));
- if (step !== BUFFER_SIZE) {
- nextDate = getNextDateTime(state, direction, nextDate);
- }
- }
- await Promise.all(preloadPromises);
-
- store.dispatch({
- type: dateConstants.SET_PRELOAD,
- preloaded: true,
- lastPreloadDate: nextDate,
- });
- }
-
- /*
- * Get a layer object from id
- *
- * @method findLayer
- * @static
- *
- * @param {object} def - Layer Specs
- *
- *
- * @returns {object} Layer object
- */
- function findLayer(def, activeCompareState) {
- const layers = self.selected.getLayers().getArray();
- let layer = lodashFind(layers, {
- wv: {
- id: def.id,
- },
- });
-
- if (!layer && layers.length && (layers[0].get('group') || layers[0].get('granuleGroup'))) {
- let olGroupLayer;
- const layerKey = `${def.id}-${activeCompareState}`;
- lodashEach(layers, (layerGroup) => {
- if (layerGroup.get('layerId') === layerKey || layerGroup.get('group') === activeCompareState) {
- olGroupLayer = layerGroup;
- }
- });
- const subGroup = olGroupLayer.getLayers().getArray();
- layer = lodashFind(subGroup, {
- wv: {
- id: def.id,
- },
- });
- }
- return layer;
- }
-
- /*
- * Return an Index value for a layer in the OPenLayers layer array
- * @method findLayerIndex
- * @param {object} def - Layer Specs
-
- * @returns {number} Index of layer in OpenLayers layer array
- */
- function findLayerIndex({ id }) {
- const state = store.getState();
- const layerGroup = getActiveLayerGroup(state);
- const layers = layerGroup.getLayers().getArray();
- return lodashFindIndex(layers, {
- wv: { id },
- });
- }
-
- const updateExtent = () => {
- const map = self.selected;
- const view = map.getView();
- const extent = view.calculateExtent();
- store.dispatch({ type: UPDATE_MAP_EXTENT, extent });
- if (map.isRendered()) {
- store.dispatch({ type: dateConstants.CLEAR_PRELOAD });
- }
- };
-
- /*
- * Create map object
- *
- * @method createMap
- * @static
- *
- * @param {object} proj - Projection properties
- * @param {object} dateSelected
- *
- * @returns {object} OpenLayers Map Object
- */
- function createMap(proj, dateSelected) {
- const state = store.getState();
- dateSelected = dateSelected || getSelectedDate(state);
- const mapContainerEl = document.getElementById('wv-map');
- const mapEl = document.createElement('div');
- const id = `wv-map-${proj.id}`;
-
- mapEl.setAttribute('id', id);
- mapEl.setAttribute('data-proj', proj.id);
- mapEl.classList.add('wv-map');
- mapEl.style.display = 'none';
- mapContainerEl.insertAdjacentElement('afterbegin', mapEl);
-
-
- // Create two specific controls
- const scaleMetric = new OlControlScaleLine({
- className: 'wv-map-scale-metric',
- units: 'metric',
- });
- const scaleImperial = new OlControlScaleLine({
- className: 'wv-map-scale-imperial',
- units: 'imperial',
- });
- const rotateInteraction = new OlInteractionDragRotate({
- condition: altKeyOnly,
- duration: animationDuration,
- });
- const mobileRotation = new OlInteractionPinchRotate({
- duration: animationDuration,
- });
- const map = new OlMap({
- view: new OlView({
- maxResolution: proj.resolutions[0],
- projection: olProj.get(proj.crs),
- center: proj.startCenter,
- zoom: proj.startZoom,
- maxZoom: proj.numZoomLevels,
- enableRotation: true,
- extent: proj.id === 'geographic' ? [-250, -90, 250, 90] : proj.maxExtent,
- constrainOnlyCenter: true,
- }),
- target: id,
- renderer: ['canvas'],
- logo: false,
- controls: [scaleMetric, scaleImperial],
- interactions: [
- doubleClickZoom,
- new OlInteractionDragPan({
- kinetic: new OlKinetic(-0.005, 0.05, 100),
- }),
- new OlInteractionPinchZoom({
- duration: animationDuration,
- }),
- new OlInteractionMouseWheelZoom({
- duration: animationDuration,
- }),
- new OlInteractionDragZoom({
- duration: animationDuration,
- }),
- ],
- loadTilesWhileAnimating: true,
- loadTilesWhileInteracting: true,
- maxTilesLoading: 32,
- });
- map.wv = {
- scaleMetric,
- scaleImperial,
- };
- map.proj = proj.id;
- createMousePosSel(map, proj);
- map.getView().on('change:resolution', () => {
- events.trigger(MAP_MOVE_START);
- });
-
- // This component is inside the map viewport container. Allowing
- // mouse move events to bubble up displays map coordinates--let those
- // be blank when over a component.
- document.querySelector('.wv-map-scale-metric').addEventListener('mousemove', (e) => e.stopPropagation());
- document.querySelector('.wv-map-scale-imperial').addEventListener('mousemove', (e) => e.stopPropagation());
-
- // Allow rotation by dragging for polar projections
- if (proj.id !== 'geographic' && proj.id !== 'webmerc') {
- map.addInteraction(rotateInteraction);
- map.addInteraction(mobileRotation);
- }
-
- const onRotate = () => {
- const radians = map.getView().getRotation();
- store.dispatch({
- type: UPDATE_MAP_ROTATION,
- rotation: radians,
- });
- const currentDeg = radians * (180.0 / Math.PI);
- saveRotation(currentDeg, map.getView());
- updateExtent();
- };
-
- // Set event listeners for changes on the map view (when rotated, zoomed, panned)
- const debouncedUpdateExtent = lodashDebounce(updateExtent, 300);
- const debouncedOnRotate = lodashDebounce(onRotate, 300);
-
- map.getView().on('change:center', debouncedUpdateExtent);
- map.getView().on('change:resolution', debouncedUpdateExtent);
- map.getView().on('change:rotation', debouncedOnRotate);
-
- map.on('pointerdrag', () => {
- self.mapIsbeingDragged = true;
- events.trigger(MAP_DRAG);
- });
- map.getView().on('propertychange', (e) => {
- switch (e.key) {
- case 'resolution':
- self.mapIsbeingZoomed = true;
- events.trigger(MAP_ZOOMING);
- break;
- default:
- break;
- }
- });
- map.on('moveend', (e) => {
- setTimeout(() => {
- self.mapIsbeingDragged = false;
- self.mapIsbeingZoomed = false;
- }, 200);
- });
- const onRenderComplete = () => {
- store.dispatch({ type: RENDERED });
- store.dispatch({
- type: UPDATE_MAP_UI,
- ui: self,
- rotation: self.selected.getView().getRotation(),
- });
- setTimeout(preloadForCompareMode, 250);
- map.un('rendercomplete', onRenderComplete);
- };
-
- map.on('loadstart', () => {
- store.dispatch(startLoading(MAP_LOADING));
- });
- map.on('loadend', () => {
- store.dispatch(stopLoading(MAP_LOADING));
- });
- map.on('rendercomplete', onRenderComplete);
- granuleFootprints[proj.crs] = granuleFootprint(map);
- window.addEventListener('resize', () => {
- map.getView().changed();
- });
- return map;
- }
-
- /**
- * Creates map events based on mouse position
- * @param {object} map - OpenLayers Map Object
- * @returns {void}
- */
- function createMousePosSel(map) {
- const throttledOnMouseMove = lodashThrottle(({ pixel }) => {
- const state = store.getState();
- const {
- events, locationSearch, sidebar, animation, measure, screenSize,
- } = state;
- const { isCoordinateSearchActive } = locationSearch;
- const isMobile = screenSize.isMobileDevice;
- const coords = map.getCoordinateFromPixel(pixel);
- const isEventsTabActive = typeof events !== 'undefined' && events.active;
- const isMapAnimating = animation.isPlaying;
-
- if (map.proj !== state.map.ui.selected.proj) return;
- if (self.mapIsbeingZoomed) return;
- if (self.mapIsbeingDragged) return;
- if (compareMapUi && compareMapUi.dragging) return;
- if (isMobile) return;
- if (measure.isActive) return;
- if (isCoordinateSearchActive) return;
- if (!coords) return;
- if (isEventsTabActive || isMapAnimating || sidebar.activeTab === 'download') return;
-
- runningdata.newPoint(pixel, map);
- }, 300);
-
- events.on(MAP_MOUSE_MOVE, throttledOnMouseMove);
- events.on(MAP_MOUSE_OUT, (e) => {
- throttledOnMouseMove.cancel();
- runningdata.clearAll();
- });
- }
-
- if (document.getElementById('app')) {
- init();
- }
-
- return self;
-}
diff --git a/web/js/mapUI/combineUI.js b/web/js/mapUI/combineUI.js
new file mode 100644
index 0000000000..11742a965a
--- /dev/null
+++ b/web/js/mapUI/combineUI.js
@@ -0,0 +1,136 @@
+import React, { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import Cache from 'cachai';
+import PQueue from 'p-queue';
+import util from '../util/util';
+import MapRunningData from '../map/runningdata';
+import {
+ REDUX_ACTION_DISPATCHED,
+ MAP_MOUSE_OUT,
+ MAP_MOVE_END,
+ MAP_MOUSE_MOVE,
+ MAP_SINGLE_CLICK,
+ MAP_CONTEXT_MENU,
+} from '../util/constants';
+import mapCompare from '../map/compare/compare';
+import mapLayerBuilder from '../map/layerbuilder';
+import MapUI from './mapUI';
+
+const { events } = util;
+
+const CombineUI = (props) => {
+ const {
+ models,
+ config,
+ store,
+ } = props;
+
+ const registerMapMouseHandlers = (maps) => {
+ Object.values(maps).forEach((map) => {
+ const element = map.getTargetElement();
+ const crs = map.getView().getProjection().getCode();
+
+ element.addEventListener('mouseleave', (event) => {
+ events.trigger(MAP_MOUSE_OUT, event);
+ });
+ map.on('moveend', (event) => {
+ events.trigger(MAP_MOVE_END, event, map, crs);
+ });
+ map.on('pointermove', (event) => {
+ events.trigger(MAP_MOUSE_MOVE, event, map, crs);
+ });
+ map.on('singleclick', (event) => {
+ events.trigger(MAP_SINGLE_CLICK, event, map, crs);
+ });
+ map.on('contextmenu', (event) => {
+ events.trigger(MAP_CONTEXT_MENU, event, map, crs);
+ });
+ });
+ };
+
+ const cache = new Cache(400);
+ const layerQueue = new PQueue({ concurrency: 3 });
+ const compareMapUi = mapCompare(store);
+ const runningdata = new MapRunningData(compareMapUi, store);
+ const { createLayer, layerKey } = mapLayerBuilder(config, cache, store);
+
+ const [ui, setUI] = useState({
+ cache,
+ mapIsbeingDragged: false,
+ mapIsbeingZoomed: false,
+ proj: {}, // One map for each projection
+ selected: null, // The map for the selected projection
+ selectedVectors: {},
+ markers: [],
+ runningdata,
+ layerKey,
+ createLayer,
+ processingPromise: null,
+ });
+
+ const uiProperties = {};
+
+ const combineUiFunction = () => {
+ const subscribeToStore = function () {
+ const state = store.getState();
+ const action = state.lastAction;
+ return events.trigger(REDUX_ACTION_DISPATCHED, action);
+ };
+ store.subscribe(subscribeToStore);
+
+ uiProperties.map = ui;
+ uiProperties.supportsPassive = false;
+ try {
+ const opts = Object.defineProperty({}, 'passive', {
+ // eslint-disable-next-line getter-return
+ get() {
+ uiProperties.supportsPassive = true;
+ },
+ });
+ window.addEventListener('testPassive', null, opts);
+ window.removeEventListener('testPassive', null, opts);
+ } catch (e) {
+ util.warn(e);
+ }
+
+ registerMapMouseHandlers(uiProperties.map.proj);
+
+ // Sink all focus on inputs if click unhandled
+ document.addEventListener('click', (e) => {
+ if (e.target.nodeName !== 'INPUT') {
+ document.querySelectorAll('input').forEach((el) => el.blur());
+ }
+ });
+ document.activeElement.blur();
+ document.querySelectorAll('input').forEach((el) => el.blur());
+
+ return uiProperties;
+ };
+
+ useEffect(() => {
+ if (ui.proj) {
+ combineUiFunction();
+ }
+ }, [ui]);
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default CombineUI;
+
+CombineUI.propTypes = {
+ config: PropTypes.object,
+ models: PropTypes.object,
+ store: PropTypes.object,
+};
diff --git a/web/js/mapUI/components/buffer-quick-animate/bufferQuickAnimate.js b/web/js/mapUI/components/buffer-quick-animate/bufferQuickAnimate.js
new file mode 100644
index 0000000000..7af97c52ca
--- /dev/null
+++ b/web/js/mapUI/components/buffer-quick-animate/bufferQuickAnimate.js
@@ -0,0 +1,92 @@
+import { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { getNumberStepsBetween, getNextDateTime } from '../../../modules/date/util';
+import { getSelectedDate } from '../../../modules/date/selectors';
+import { promiseImageryForTime } from '../../../modules/map/util';
+import { setPreload } from '../../../modules/date/actions';
+
+const BufferQuickAnimate = (props) => {
+ const {
+ action,
+ date,
+ dateCompareState,
+ lastPreloadDate,
+ preloaded,
+ promiseImageryState,
+ setPreload,
+ } = props;
+
+ useEffect(() => {
+ if (action.value) {
+ bufferQuickAnimate(action.value);
+ }
+ }, [action]);
+
+ async function bufferQuickAnimate(arrowDown) {
+ const BUFFER_SIZE = 8;
+ const preloadPromises = [];
+ const selectedDate = getSelectedDate(dateCompareState);
+ const dateState = { date };
+ const currentBuffer = preloaded ? getNumberStepsBetween(dateState, selectedDate, lastPreloadDate) : 0;
+
+ if (currentBuffer >= BUFFER_SIZE) {
+ return;
+ }
+
+ const currentDate = preloaded ? lastPreloadDate : selectedDate;
+ const direction = arrowDown === 'right' ? 1 : -1;
+ let nextDate = getNextDateTime(dateCompareState, direction, currentDate);
+
+ for (let step = 1; step <= BUFFER_SIZE; step += 1) {
+ preloadPromises.push(promiseImageryForTime(promiseImageryState, nextDate));
+ if (step !== BUFFER_SIZE) {
+ nextDate = getNextDateTime(dateCompareState, direction, nextDate);
+ }
+ }
+ await Promise.all(preloadPromises);
+ setPreload(true, nextDate);
+ }
+
+ return null;
+};
+
+const mapStateToProps = (state) => {
+ const {
+ date, map, proj, embed, compare, layers, palettes, vectorStyles,
+ } = state;
+ const dateCompareState = { date, compare };
+ const { preloaded, lastPreloadDate } = date;
+ const promiseImageryState = {
+ map, proj, embed, compare, layers, palettes, vectorStyles,
+ };
+
+ return {
+ date,
+ dateCompareState,
+ lastPreloadDate,
+ preloaded,
+ promiseImageryState,
+ };
+};
+
+const mapDispatchToProps = (dispatch) => ({
+ setPreload: (preloaded, lastPreloadDate) => {
+ dispatch(setPreload(preloaded, lastPreloadDate));
+ },
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(BufferQuickAnimate);
+
+BufferQuickAnimate.propTypes = {
+ action: PropTypes.object,
+ date: PropTypes.object,
+ dateCompareState: PropTypes.object,
+ lastPreloadDate: PropTypes.object,
+ preloaded: PropTypes.bool,
+ promiseImageryState: PropTypes.object,
+ setPreload: PropTypes.func,
+};
diff --git a/web/js/mapUI/components/create-map/createMap.js b/web/js/mapUI/components/create-map/createMap.js
new file mode 100644
index 0000000000..eeb6ff51fb
--- /dev/null
+++ b/web/js/mapUI/components/create-map/createMap.js
@@ -0,0 +1,279 @@
+import { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import {
+ forOwn as lodashForOwn,
+ debounce as lodashDebounce,
+} from 'lodash';
+import OlMap from 'ol/Map';
+import OlView from 'ol/View';
+import OlKinetic from 'ol/Kinetic';
+import OlControlScaleLine from 'ol/control/ScaleLine';
+import { altKeyOnly } from 'ol/events/condition';
+import OlInteractionPinchRotate from 'ol/interaction/PinchRotate';
+import OlInteractionDragRotate from 'ol/interaction/DragRotate';
+import OlInteractionDoubleClickZoom from 'ol/interaction/DoubleClickZoom';
+import OlInteractionPinchZoom from 'ol/interaction/PinchZoom';
+import OlInteractionDragPan from 'ol/interaction/DragPan';
+import OlInteractionMouseWheelZoom from 'ol/interaction/MouseWheelZoom';
+import OlInteractionDragZoom from 'ol/interaction/DragZoom';
+import * as olProj from 'ol/proj';
+import util from '../../../util/util';
+import {
+ refreshRotation,
+ updateRenderedState,
+ updateMapUI,
+} from '../../../modules/map/actions';
+import { saveRotation } from '../../../map/util';
+import {
+ MAP_DISABLE_CLICK_ZOOM,
+ MAP_ENABLE_CLICK_ZOOM,
+ MAP_DRAG,
+ MAP_MOVE_START,
+ MAP_ZOOMING,
+} from '../../../util/constants';
+import { startLoading, stopLoading, MAP_LOADING } from '../../../modules/loading/actions';
+import { granuleFootprint } from '../../../map/granule/util';
+
+const { events } = util;
+
+const CreateMap = (props) => {
+ const {
+ config,
+ isMapSet,
+ preloadForCompareMode,
+ setGranuleFootprints,
+ setMap,
+ setUI,
+ startLoading,
+ stopLoading,
+ ui,
+ updateExtent,
+ updateMapUI,
+ updateRenderedState,
+ updateRotation,
+ } = props;
+
+ const { projections } = config;
+ let granuleFootprintsObj = {};
+ const animationDuration = 250;
+ const doubleClickZoom = new OlInteractionDoubleClickZoom({
+ duration: animationDuration,
+ });
+
+ useEffect(() => {
+ if (isMapSet) return;
+ setMap(true);
+ const uiCopy = ui;
+ lodashForOwn(projections, (proj) => {
+ const map = mapCreation(proj, uiCopy);
+ uiCopy.proj[proj.id] = map;
+ });
+ setGranuleFootprints(granuleFootprintsObj);
+ setUI(uiCopy);
+ });
+
+ /**
+ * Create map object
+ *
+ * @method createMap
+ * @static
+ *
+ * @param {object} proj - Projection properties
+ * @param {object} dateSelected
+ *
+ * @returns {object} OpenLayers Map Object
+ */
+ const mapCreation = (proj, uiCopy) => {
+ const mapContainerEl = document.getElementById('wv-map');
+ const mapEl = document.createElement('div');
+ const id = `wv-map-${proj.id}`;
+
+ mapEl.setAttribute('id', id);
+ mapEl.setAttribute('data-proj', proj.id);
+ mapEl.classList.add('wv-map');
+ mapEl.style.display = 'none';
+ mapContainerEl.insertAdjacentElement('afterbegin', mapEl);
+
+ const scaleMetric = new OlControlScaleLine({
+ className: 'wv-map-scale-metric',
+ units: 'metric',
+ });
+ const scaleImperial = new OlControlScaleLine({
+ className: 'wv-map-scale-imperial',
+ units: 'imperial',
+ });
+ const rotateInteraction = new OlInteractionDragRotate({
+ condition: altKeyOnly,
+ duration: animationDuration,
+ });
+ const mobileRotation = new OlInteractionPinchRotate({
+ duration: animationDuration,
+ });
+ const map = new OlMap({
+ view: new OlView({
+ maxResolution: proj.resolutions[0],
+ projection: olProj.get(proj.crs),
+ center: proj.startCenter,
+ zoom: proj.startZoom,
+ maxZoom: proj.numZoomLevels,
+ enableRotation: true,
+ extent: proj.id === 'geographic' ? [-250, -90, 250, 90] : proj.maxExtent,
+ constrainOnlyCenter: true,
+ }),
+ target: id,
+ renderer: ['canvas'],
+ logo: false,
+ controls: [scaleMetric, scaleImperial],
+ interactions: [
+ doubleClickZoom,
+ new OlInteractionDragPan({
+ kinetic: new OlKinetic(-0.005, 0.05, 100),
+ }),
+ new OlInteractionPinchZoom({
+ duration: animationDuration,
+ }),
+ new OlInteractionMouseWheelZoom({
+ duration: animationDuration,
+ }),
+ new OlInteractionDragZoom({
+ duration: animationDuration,
+ }),
+ ],
+ loadTilesWhileAnimating: true,
+ loadTilesWhileInteracting: true,
+ maxTilesLoading: 32,
+ });
+ map.wv = {
+ scaleMetric,
+ scaleImperial,
+ };
+ map.proj = proj.id;
+
+ map.getView().on('change:resolution', () => {
+ events.trigger(MAP_MOVE_START);
+ });
+
+ // This component is inside the map viewport container. Allowing
+ // mouse move events to bubble up displays map coordinates--let those
+ // be blank when over a component.
+ document.querySelector('.wv-map-scale-metric').addEventListener('mousemove', (e) => e.stopPropagation());
+ document.querySelector('.wv-map-scale-imperial').addEventListener('mousemove', (e) => e.stopPropagation());
+
+ // Allow rotation by dragging for polar projections
+ if (proj.id !== 'geographic' && proj.id !== 'webmerc') {
+ map.addInteraction(rotateInteraction);
+ map.addInteraction(mobileRotation);
+ }
+
+ const onRotate = () => {
+ const radians = map.getView().getRotation();
+ updateRotation(radians);
+ const PI_OVER_180 = Math.PI / 180;
+ const currentDeg = radians * PI_OVER_180;
+ saveRotation(currentDeg, map.getView());
+ updateExtent();
+ };
+
+ // Set event listeners for changes on the map view (when rotated, zoomed, panned)
+ const debouncedUpdateExtent = lodashDebounce(updateExtent, 300);
+ const debouncedOnRotate = lodashDebounce(onRotate, 300);
+
+ map.getView().on('change:center', debouncedUpdateExtent);
+ map.getView().on('change:resolution', debouncedUpdateExtent);
+ map.getView().on('change:rotation', debouncedOnRotate);
+
+ map.on('pointerdrag', () => {
+ uiCopy.mapIsbeingDragged = true;
+ events.trigger(MAP_DRAG);
+ });
+ map.getView().on('propertychange', (e) => {
+ switch (e.key) {
+ case 'resolution':
+ uiCopy.mapIsbeingZoomed = true;
+ events.trigger(MAP_ZOOMING);
+ break;
+ default:
+ break;
+ }
+ });
+ map.on('moveend', (e) => {
+ setTimeout(() => {
+ uiCopy.mapIsbeingDragged = false;
+ uiCopy.mapIsbeingZoomed = false;
+ }, 200);
+ });
+ const onRenderComplete = () => {
+ updateRenderedState();
+ updateMapUI(uiCopy, uiCopy.selected.getView().getRotation());
+ setTimeout(preloadForCompareMode, 250);
+
+ map.un('rendercomplete', onRenderComplete);
+ };
+
+ map.on('loadstart', () => {
+ startLoading(MAP_LOADING);
+ });
+ map.on('loadend', () => {
+ stopLoading(MAP_LOADING);
+ });
+ map.on('rendercomplete', onRenderComplete);
+
+ granuleFootprintsObj = { ...granuleFootprintsObj, [proj.crs]: granuleFootprint(map) };
+
+ window.addEventListener('resize', () => {
+ map.getView().changed();
+ });
+
+ events.on(MAP_DISABLE_CLICK_ZOOM, () => {
+ doubleClickZoom.setActive(false);
+ });
+ events.on(MAP_ENABLE_CLICK_ZOOM, () => {
+ setTimeout(() => {
+ doubleClickZoom.setActive(true);
+ }, 100);
+ });
+ return map;
+ };
+
+ return null;
+};
+
+const mapDispatchToProps = (dispatch) => ({
+ updateRotation: (rotation) => {
+ dispatch(refreshRotation(rotation));
+ },
+ updateRenderedState: () => {
+ dispatch(updateRenderedState());
+ },
+ updateMapUI: (ui, rotation) => {
+ dispatch(updateMapUI(ui, rotation));
+ },
+ startLoading: (key) => {
+ dispatch(startLoading(key));
+ },
+ stopLoading: (key) => {
+ dispatch(stopLoading(key));
+ },
+});
+
+export default connect(
+ null,
+ mapDispatchToProps,
+)(CreateMap);
+
+CreateMap.propTypes = {
+ config: PropTypes.object,
+ isMapSet: PropTypes.bool,
+ preloadForCompareMode: PropTypes.func,
+ setGranuleFootprints: PropTypes.func,
+ setMap: PropTypes.func,
+ setUI: PropTypes.func,
+ startLoading: PropTypes.func,
+ stopLoading: PropTypes.func,
+ ui: PropTypes.object,
+ updateExtent: PropTypes.func,
+ updateMapUI: PropTypes.func,
+ updateRenderedState: PropTypes.func,
+ updateRotation: PropTypes.func,
+};
diff --git a/web/js/mapUI/components/granule-hover/granuleHover.js b/web/js/mapUI/components/granule-hover/granuleHover.js
new file mode 100644
index 0000000000..8c8a033d2d
--- /dev/null
+++ b/web/js/mapUI/components/granule-hover/granuleHover.js
@@ -0,0 +1,56 @@
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { getActiveGranuleFootPrints } from '../../../modules/layers/selectors';
+import { GRANULE_HOVERED, GRANULE_HOVER_UPDATE } from '../../../util/constants';
+import util from '../../../util/util';
+
+const { events } = util;
+
+const GranuleHover = (props) => {
+ const {
+ granuleFootprints,
+ state,
+ ui,
+ } = props;
+
+ const onGranuleHover = (platform, date, update) => {
+ const proj = ui.selected.getView().getProjection().getCode();
+ if (!granuleFootprints[proj]) return;
+ let geometry;
+ if (platform && date) {
+ geometry = getActiveGranuleFootPrints(state)[date];
+ }
+ granuleFootprints[proj].addFootprint(geometry, date);
+ };
+
+ const onGranuleHoverUpdate = (platform, date) => {
+ const proj = ui.selected.getView().getProjection().getCode();
+ if (!granuleFootprints[proj]) return;
+ let geometry;
+ if (platform && date) {
+ geometry = getActiveGranuleFootPrints(state)[date];
+ }
+ if (!geometry) return;
+ granuleFootprints[proj].updateFootprint(geometry, date);
+ };
+
+ events.on(GRANULE_HOVERED, onGranuleHover);
+ events.on(GRANULE_HOVER_UPDATE, onGranuleHoverUpdate);
+
+ return null;
+};
+
+const mapStateToProps = (state) => ({
+ state,
+});
+
+export default connect(
+ mapStateToProps,
+)(GranuleHover);
+
+GranuleHover.propTypes = {
+ granuleFootprints: PropTypes.object,
+ setGranuleFootprints: PropTypes.func,
+ state: PropTypes.object,
+ ui: PropTypes.object,
+};
diff --git a/web/js/mapUI/components/layers/addLayer.js b/web/js/mapUI/components/layers/addLayer.js
new file mode 100644
index 0000000000..8e6d1a4a48
--- /dev/null
+++ b/web/js/mapUI/components/layers/addLayer.js
@@ -0,0 +1,115 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import {
+ cloneDeep as lodashCloneDeep,
+ findIndex as lodashFindIndex,
+ find as lodashFind,
+} from 'lodash';
+import { getActiveLayers } from '../../../modules/layers/selectors';
+import * as layerConstants from '../../../modules/layers/constants';
+import { clearPreload } from '../../../modules/date/actions';
+
+const AddLayer = (props) => {
+ const {
+ action,
+ activeLayersState,
+ activeString,
+ compareMapUi,
+ mode,
+ preloadNextTiles,
+ selected,
+ updateLayerVisibilities,
+ ui,
+ } = props;
+
+ useEffect(() => {
+ if (action.type === layerConstants.ADD_LAYER) {
+ const def = lodashFind(action.layers, { id: action.id });
+ if (def.type === 'granule') {
+ return granuleLayerAdd(def);
+ }
+ clearPreload();
+ addLayer(def);
+ }
+ }, [action]);
+
+ const granuleLayerAdd = (def) => {
+ ui.processingPromise = new Promise((resolve) => {
+ resolve(addLayer(def));
+ });
+ };
+
+ /**
+ * Initiates the adding of a layer
+ * @param {object} def - layer Specs
+ * @returns {void}
+ */
+ const addLayer = async function(def, layerDate, activeLayersParam) {
+ const { createLayer } = ui;
+ const date = layerDate || selected;
+ const activeLayers = activeLayersParam || activeLayersState;
+ const reverseLayers = lodashCloneDeep(activeLayers).reverse();
+ const index = lodashFindIndex(reverseLayers, { id: def.id });
+ const mapLayers = ui.selected.getLayers().getArray();
+ const firstLayer = mapLayers[0];
+
+ if (firstLayer && firstLayer.get('group') && firstLayer.get('granule') !== true) {
+ const activelayer = firstLayer.get('group') === activeString
+ ? firstLayer
+ : mapLayers[1];
+ const options = {
+ date,
+ group: activeString,
+ };
+ const newLayer = await createLayer(def, options);
+ activelayer.getLayers().insertAt(index, newLayer);
+ compareMapUi.create(ui.selected, mode);
+ } else {
+ const newLayer = await createLayer(def);
+ ui.selected.getLayers().insertAt(index, newLayer);
+ }
+ updateLayerVisibilities();
+ preloadNextTiles();
+ };
+ return null;
+};
+
+const mapStateToProps = (state) => {
+ const { compare, date } = state;
+ const { activeString, mode } = compare;
+ const { selected } = date;
+ const activeLayersState = getActiveLayers(state);
+ return {
+ activeLayersState,
+ activeString,
+ mode,
+ selected,
+ };
+};
+
+const mapDispatchToProps = (dispatch) => ({
+ clearPreload: () => {
+ dispatch(clearPreload());
+ },
+});
+
+export default React.memo(
+ connect(
+ mapStateToProps,
+ mapDispatchToProps,
+ )(AddLayer),
+);
+
+AddLayer.propTypes = {
+ activeLayersState: PropTypes.array,
+ activeString: PropTypes.string,
+ action: PropTypes.object,
+ clearPreload: PropTypes.func,
+ compareMapUi: PropTypes.object,
+ mode: PropTypes.string,
+ preloadNextTiles: PropTypes.func,
+ selected: PropTypes.object,
+ updateLayerVisibilities: PropTypes.func,
+ ui: PropTypes.object,
+};
diff --git a/web/js/mapUI/components/layers/removeLayer.js b/web/js/mapUI/components/layers/removeLayer.js
new file mode 100644
index 0000000000..6f31d3718b
--- /dev/null
+++ b/web/js/mapUI/components/layers/removeLayer.js
@@ -0,0 +1,54 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+const RemoveLayer = (props) => {
+ const {
+ action,
+ compare,
+ findLayer,
+ ui,
+ updateLayerVisibilities,
+ } = props;
+
+ useEffect(() => {
+ if (action.layersToRemove) {
+ removeLayer(action.layersToRemove);
+ }
+ }, [action]);
+
+ const removeLayer = (layersToRemove) => {
+ layersToRemove.forEach((def) => {
+ const layer = findLayer(def, compare.activeString);
+ if (compare && compare.active) {
+ if (ui.selected) ui.selected.getLayers().remove(layer);
+ } else {
+ ui.selected.removeLayer(layer);
+ }
+ });
+ updateLayerVisibilities();
+ };
+
+ return null;
+};
+
+const mapStateToProps = (state) => {
+ const { compare } = state;
+
+ return {
+ compare,
+ };
+};
+export default React.memo(
+ connect(
+ mapStateToProps,
+ )(RemoveLayer),
+);
+
+RemoveLayer.propTypes = {
+ action: PropTypes.any,
+ compare: PropTypes.object,
+ findLayer: PropTypes.func,
+ ui: PropTypes.object,
+ updateLayerVisibilities: PropTypes.func,
+};
diff --git a/web/js/mapUI/components/markers/markers.js b/web/js/mapUI/components/markers/markers.js
new file mode 100644
index 0000000000..8108470d10
--- /dev/null
+++ b/web/js/mapUI/components/markers/markers.js
@@ -0,0 +1,203 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { reverseGeocode } from '../../../modules/location-search/util-api';
+import { getNormalizedCoordinate } from '../../../components/location-search/util';
+import { animateCoordinates, areCoordinatesWithinExtent, getCoordinatesMarker } from '../../../modules/location-search/util';
+import { setGeocodeResults, removeMarker } from '../../../modules/location-search/actions';
+import { getActiveLayers, getMaxZoomLevelLayerCollection } from '../../../modules/layers/selectors';
+
+const Markers = (props) => {
+ const {
+ action,
+ activeLayers,
+ config,
+ coordinates,
+ isMobileDevice,
+ selectedMap,
+ selectedMapMarkers,
+ proj,
+ removeMarker,
+ setGeocodeResults,
+ ui,
+ } = props;
+
+ useEffect(() => {
+ switch (action.type) {
+ case 'LOCATION_SEARCH/REMOVE_MARKER': {
+ return removeCoordinatesMarker(action.coordinates);
+ }
+ case 'LOCATION_SEARCH/SET_MARKER': {
+ if (action.flyToExistingMarker) {
+ return flyToMarker(action.coordinates);
+ }
+ return addMarkerAndUpdateStore(true, action.reverseGeocodeResults, action.isCoordinatesSearchActive, action.coordinates);
+ }
+ case 'LOCATION_SEARCH/TOGGLE_DIALOG_VISIBLE': {
+ return addMarkerAndUpdateStore(false);
+ }
+ default:
+ break;
+ }
+ }, [action]);
+
+ /**
+ * Remove coordinates marker from all projections
+ *
+ * @method removeCoordinatesMarker
+ * @static
+ *
+ * @param {Object} coordinatesObject - set of coordinates for marker
+ *
+ * @returns {void}
+ */
+ const removeCoordinatesMarker = (coordinatesObject) => {
+ selectedMapMarkers.forEach((marker) => {
+ if (marker.id === coordinatesObject.id) {
+ marker.setMap(null);
+ selectedMap.removeOverlay(marker);
+ }
+ });
+ };
+
+ /**
+ * Remove all coordinates markers
+ *
+ * @method removeAllCoordinatesMarkers
+ * @static
+ *
+ * @returns {void}
+ */
+ const removeAllCoordinatesMarkers = () => {
+ ui.markers.forEach((marker) => {
+ marker.setMap(null);
+ ui.selected.removeOverlay(marker);
+ });
+ };
+
+ /**
+ * Handle reverse geocode and add map marker with results
+ *
+ * @method handleActiveMapMarker
+ * @static
+ *
+ * @returns {void}
+ */
+ const handleActiveMapMarker = () => {
+ removeAllCoordinatesMarkers();
+ if (coordinates && coordinates.length > 0) {
+ coordinates.forEach((coordinatesObject) => {
+ const { longitude, latitude } = coordinatesObject;
+ const coord = [longitude, latitude];
+ if (!areCoordinatesWithinExtent(proj, coord)) return;
+ reverseGeocode(getNormalizedCoordinate(coord), config).then((results) => {
+ addMarkerAndUpdateStore(true, results, null, coordinatesObject);
+ });
+ });
+ }
+ };
+
+ const flyToMarker = (coordinatesObject) => {
+ const { sources } = config;
+ const { longitude, latitude } = coordinatesObject;
+ const latestCoordinates = coordinatesObject && [longitude, latitude];
+ const zoom = selectedMap.getView().getZoom();
+ const maxZoom = getMaxZoomLevelLayerCollection(activeLayers, zoom, proj.id, sources);
+ animateCoordinates(selectedMap, proj, latestCoordinates, maxZoom);
+ };
+
+ /**
+ * Add map coordinate marker and update store
+ *
+ * @method addMarkerAndUpdateStore
+ * @static
+ *
+ * @param {Boolean} showDialog
+ * @param {Object} geocodeResults
+ * @param {Boolean} shouldFlyToCoordinates - if location search via input
+ * @param {Object} coordinatesObject - set of coordinates for marker
+ * @returns {void}
+ */
+ const addMarkerAndUpdateStore = (showDialog, geocodeResults, shouldFlyToCoordinates, coordinatesObject) => {
+ const results = geocodeResults;
+ if (!results) return;
+ const remove = () => removeMarker(coordinatesObject);
+ const marker = getCoordinatesMarker(
+ proj,
+ coordinatesObject,
+ results,
+ remove,
+ isMobileDevice,
+ showDialog,
+ );
+
+ if (!marker) {
+ return false;
+ }
+
+ ui.markers.push(marker);
+ ui.selected.addOverlay(marker);
+ ui.selected.renderSync();
+
+ if (shouldFlyToCoordinates) {
+ flyToMarker(coordinatesObject);
+ }
+
+ setGeocodeResults(geocodeResults);
+ };
+
+ useEffect(() => {
+ handleActiveMapMarker();
+ }, [ui]);
+
+ return null;
+};
+
+const mapStateToProps = (state) => {
+ const {
+ locationSearch, proj, screenSize, map,
+ } = state;
+ const { coordinates } = locationSearch;
+ const { isMobileDevice } = screenSize;
+ const activeLayers = getActiveLayers(state).filter(({ projections }) => projections[proj.id]);
+ const selectedMap = map.ui.selected;
+ const selectedMapMarkers = map.ui.markers;
+ return {
+ activeLayers,
+ coordinates,
+ isMobileDevice,
+ selectedMap,
+ selectedMapMarkers,
+ proj,
+ };
+};
+
+const mapDispatchToProps = (dispatch) => ({
+ setGeocodeResults: (value) => {
+ dispatch(setGeocodeResults(value));
+ },
+ removeMarker: (value) => {
+ dispatch(removeMarker(value));
+ },
+});
+
+export default React.memo(
+ connect(
+ mapStateToProps,
+ mapDispatchToProps,
+ )(Markers),
+);
+
+Markers.propTypes = {
+ action: PropTypes.object,
+ config: PropTypes.object,
+ coordinates: PropTypes.array,
+ isMobileDevice: PropTypes.bool,
+ proj: PropTypes.object,
+ removeMarker: PropTypes.func,
+ setGeocodeResults: PropTypes.func,
+ state: PropTypes.object,
+ ui: PropTypes.object,
+};
+
+
diff --git a/web/js/mapUI/components/mouse-move-events/mouseMoveEvents.js b/web/js/mapUI/components/mouse-move-events/mouseMoveEvents.js
new file mode 100644
index 0000000000..d8d865a30e
--- /dev/null
+++ b/web/js/mapUI/components/mouse-move-events/mouseMoveEvents.js
@@ -0,0 +1,96 @@
+import { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import {
+ throttle as lodashThrottle,
+} from 'lodash';
+import util from '../../../util/util';
+import {
+ MAP_MOUSE_MOVE,
+ MAP_MOUSE_OUT,
+} from '../../../util/constants';
+
+const { events } = util;
+
+const MouseMoveEvents = (props) => {
+ const {
+ isCoordinateSearchActive,
+ isEventsTabActive,
+ isMobile,
+ isMeasureActive,
+ isMapAnimating,
+ sidebarActiveTab,
+ map,
+ ui,
+ compareMapUi,
+ } = props;
+
+ const [mouseMove, setMouseMove] = useState({});
+
+ const throttledOnMouseMove = lodashThrottle(({ pixel }) => {
+ if (!map.ui.selected) return;
+
+ const coords = ui.selected.getCoordinateFromPixel(pixel);
+
+ if (map.ui.selected.proj !== ui.selected.proj) return;
+ if (ui.mapIsbeingZoomed) return;
+ if (ui.mapIsbeingDragged) return;
+ if (compareMapUi && compareMapUi.dragging) return;
+ if (isMobile) return;
+ if (isMeasureActive) return;
+ if (isCoordinateSearchActive) return;
+ if (!coords) return;
+ if (isEventsTabActive || isMapAnimating || sidebarActiveTab === 'download') return;
+ ui.runningdata.newPoint(pixel, ui.selected);
+ }, 300);
+
+ useEffect(() => {
+ throttledOnMouseMove(mouseMove);
+ }, [mouseMove]);
+
+ events.on(MAP_MOUSE_MOVE, setMouseMove);
+ events.on(MAP_MOUSE_OUT, (e) => {
+ throttledOnMouseMove.cancel();
+ ui.runningdata.clearAll();
+ });
+
+ return null;
+};
+
+const mapStateToProps = (state) => {
+ const {
+ events, locationSearch, sidebar, animation, measure, screenSize, map,
+ } = state;
+ const { isCoordinateSearchActive } = locationSearch;
+ const isEventsTabActive = typeof events !== 'undefined' && events.active;
+ const isMobile = screenSize.isMobileDevice;
+ const isMeasureActive = measure.isActive;
+ const isMapAnimating = animation.isPlaying;
+ const sidebarActiveTab = sidebar.activeTab;
+
+ return {
+ isCoordinateSearchActive,
+ isEventsTabActive,
+ isMobile,
+ isMeasureActive,
+ isMapAnimating,
+ sidebarActiveTab,
+ map,
+ };
+};
+
+export default connect(
+ mapStateToProps,
+)(MouseMoveEvents);
+
+MouseMoveEvents.propTypes = {
+ compareMapUi: PropTypes.object,
+ isCoordinateSearchActive: PropTypes.bool,
+ isEventsTabActive: PropTypes.bool,
+ isMapAnimating: PropTypes.bool,
+ isMeasureActive: PropTypes.bool,
+ isMobile: PropTypes.bool,
+ sidebarActiveTab: PropTypes.string,
+ map: PropTypes.object,
+ ui: PropTypes.object,
+};
diff --git a/web/js/mapUI/components/update-date/updateDate.js b/web/js/mapUI/components/update-date/updateDate.js
new file mode 100644
index 0000000000..122e7f27f0
--- /dev/null
+++ b/web/js/mapUI/components/update-date/updateDate.js
@@ -0,0 +1,184 @@
+import { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import {
+ findIndex as lodashFindIndex,
+ get as lodashGet,
+} from 'lodash';
+import {
+ getActiveLayers,
+ getAllActiveLayers,
+ getActiveLayerGroup,
+ getGranuleCount,
+} from '../../../modules/layers/selectors';
+import { setStyleFunction } from '../../../modules/vector-styles/selectors';
+import { getSelectedDate } from '../../../modules/date/selectors';
+import * as dateConstants from '../../../modules/date/constants';
+import * as layerConstants from '../../../modules/layers/constants';
+
+const UpdateDate = (props) => {
+ const {
+ action,
+ activeLayers,
+ activeString,
+ compareMapUi,
+ config,
+ dateCompareState,
+ getGranuleOptions,
+ granuleState,
+ isCompareActive,
+ layerState,
+ preloadNextTiles,
+ state,
+ ui,
+ updateLayerVisibilities,
+ vectorStyleState,
+ } = props;
+
+ useEffect(() => {
+ actionSwitch();
+ }, [action]);
+
+ const actionSwitch = () => {
+ if (action.type === dateConstants.SELECT_DATE) {
+ if (ui.processingPromise) {
+ return new Promise((resolve) => {
+ resolve(ui.processingPromise);
+ }).then(() => {
+ ui.processingPromise = null;
+ return updateDate(action.outOfStep);
+ });
+ }
+ return updateDate(action.outOfStep);
+ } if (action.type === layerConstants.TOGGLE_LAYER_VISIBILITY || action.type === layerConstants.TOGGLE_OVERLAY_GROUP_VISIBILITY) {
+ return updateDate();
+ }
+ };
+
+ function findLayerIndex({ id }) {
+ const layerGroup = getActiveLayerGroup(layerState);
+ const layers = layerGroup.getLayers().getArray();
+ return lodashFindIndex(layers, {
+ wv: { id },
+ });
+ }
+
+ function updateVectorStyles (def) {
+ const { vectorStyles } = config;
+ const layerName = def.layer || def.id;
+ let vectorStyleId;
+
+ vectorStyleId = def.vectorStyle.id;
+ if (activeLayers) {
+ activeLayers.forEach((layer) => {
+ if (layer.id === layerName && layer.custom) {
+ vectorStyleId = layer.custom;
+ }
+ });
+ }
+ setStyleFunction(def, vectorStyleId, vectorStyles, null, vectorStyleState);
+ }
+
+ async function updateCompareLayer (def, index, layerCollection) {
+ const { createLayer } = ui;
+ const options = {
+ group: activeString,
+ date: getSelectedDate(dateCompareState),
+ ...getGranuleOptions(granuleState, def, activeString),
+ };
+ const updatedLayer = await createLayer(def, options);
+ layerCollection.setAt(index, updatedLayer);
+ compareMapUi.update(activeString);
+ }
+
+ async function updateDate(outOfStepChange) {
+ const { createLayer } = ui;
+
+ const layerGroup = getActiveLayerGroup(layerState);
+ const mapLayerCollection = layerGroup.getLayers();
+ const layers = mapLayerCollection.getArray();
+ const activeLayers = getAllActiveLayers(state);
+
+ const visibleLayers = activeLayers.filter(
+ ({ id }) => layers
+ .map(({ wv }) => lodashGet(wv, 'def.id'))
+ .includes(id),
+ ).filter(({ visible }) => visible);
+
+ const layerPromises = visibleLayers.map(async (def) => {
+ const { id, type } = def;
+ const temporalLayer = ['subdaily', 'daily', 'monthly', 'yearly']
+ .includes(def.period);
+ const index = findLayerIndex(def);
+ const hasVectorStyles = config.vectorStyles && lodashGet(def, 'vectorStyle.id');
+ if (isCompareActive && layers.length) {
+ await updateCompareLayer(def, index, mapLayerCollection);
+ } else if (temporalLayer) {
+ if (index !== undefined && index !== -1) {
+ const layerValue = layers[index];
+ const layerOptions = type === 'granule'
+ ? { granuleCount: getGranuleCount(granuleState, id) }
+ : { previousLayer: layerValue ? layerValue.wv : null };
+ const updatedLayer = await createLayer(def, layerOptions);
+ mapLayerCollection.setAt(index, updatedLayer);
+ }
+ }
+ if (hasVectorStyles && temporalLayer) {
+ updateVectorStyles(def);
+ }
+ });
+ await Promise.all(layerPromises);
+ updateLayerVisibilities();
+ if (!outOfStepChange) {
+ preloadNextTiles();
+ }
+ }
+
+ return null;
+};
+
+const mapStateToProps = (state) => {
+ const {
+ compare, date, layers, proj, vectorStyles, config, map,
+ } = state;
+ const dateCompareState = { date, compare };
+ const { activeString } = compare;
+ const activeLayers = getActiveLayers(state);
+ const isCompareActive = compare.active;
+ const granuleState = { compare, layers };
+ const layerState = { compare, map };
+ const vectorStyleState = { proj, vectorStyles, config };
+
+ return {
+ activeLayers,
+ activeString,
+ dateCompareState,
+ granuleState,
+ isCompareActive,
+ layerState,
+ state,
+ vectorStyleState,
+ };
+};
+
+export default connect(
+ mapStateToProps,
+)(UpdateDate);
+
+UpdateDate.propTypes = {
+ action: PropTypes.object,
+ activeLayers: PropTypes.array,
+ activeString: PropTypes.string,
+ compareMapUi: PropTypes.object,
+ config: PropTypes.object,
+ dateCompareState: PropTypes.object,
+ getGranuleOptions: PropTypes.func,
+ granuleState: PropTypes.object,
+ isComparActive: PropTypes.bool,
+ layerState: PropTypes.object,
+ preloadNextTiles: PropTypes.func,
+ state: PropTypes.object,
+ ui: PropTypes.object,
+ updateLayerVisibilities: PropTypes.func,
+ vectorStyleState: PropTypes.object,
+};
diff --git a/web/js/mapUI/components/update-opacity/updateOpacity.js b/web/js/mapUI/components/update-opacity/updateOpacity.js
new file mode 100644
index 0000000000..02fa80e121
--- /dev/null
+++ b/web/js/mapUI/components/update-opacity/updateOpacity.js
@@ -0,0 +1,109 @@
+import { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import {
+ each as lodashEach,
+ find as lodashFind,
+} from 'lodash';
+import { getActiveLayers } from '../../../modules/layers/selectors';
+import * as layerConstants from '../../../modules/layers/constants';
+
+const UpdateOpacity = (props) => {
+ const {
+ action,
+ activeLayers,
+ activeString,
+ compare,
+ findLayer,
+ isCompareActive,
+ ui,
+ updateLayerVisibilities,
+ } = props;
+
+ useEffect(() => {
+ if (action.type === layerConstants.UPDATE_OPACITY) {
+ updateOpacity(action);
+ }
+ }, [action]);
+
+ const updateGranuleLayerOpacity = (def, activeString, opacity, compare) => {
+ const { id } = def;
+ const layers = ui.selected.getLayers().getArray();
+ lodashEach(Object.keys(layers), (index) => {
+ const layer = layers[index];
+ if (layer.className_ === 'ol-layer') {
+ if (compare && isCompareActive) {
+ const layerGroup = layer.getLayers().getArray();
+ lodashEach(Object.keys(layerGroup), (groupIndex) => {
+ const compareLayerGroup = layerGroup[groupIndex];
+ if (compareLayerGroup.wv.id === id) {
+ const tileLayer = compareLayerGroup.getLayers().getArray();
+
+ // inner first granule group tile layer
+ const firstTileLayer = tileLayer[0];
+ if (firstTileLayer.wv.id === id) {
+ if (firstTileLayer.wv.group === activeString) {
+ compareLayerGroup.setOpacity(opacity);
+ }
+ }
+ }
+ });
+ } else if (layer.wv.id === id) {
+ if (layer.wv.group === activeString) {
+ layer.setOpacity(opacity);
+ }
+ }
+ }
+ });
+ };
+
+ /**
+ * Sets new opacity to layer
+ * @param {object} def - layer Specs
+ * @param {number} value - number value
+ * @returns {void}
+ */
+ const updateOpacity = (action) => {
+ const { id, opacity } = action;
+ const def = lodashFind(activeLayers, { id });
+ if (def.type === 'granule') {
+ updateGranuleLayerOpacity(def, activeString, opacity, compare);
+ } else {
+ const layerGroup = findLayer(def, activeString);
+ layerGroup.getLayersArray().forEach((l) => {
+ l.setOpacity(opacity);
+ });
+ }
+ updateLayerVisibilities();
+ };
+ return null;
+};
+
+const mapStateToProps = (state) => {
+ const { compare } = state;
+ const isCompareActive = compare.active;
+ const activeString = compare.isCompareA ? 'active' : 'activeB';
+ const activeLayers = getActiveLayers(state);
+
+ return {
+ activeLayers,
+ activeString,
+ compare,
+ isCompareActive,
+ };
+};
+
+export default connect(
+ mapStateToProps,
+)(UpdateOpacity);
+
+UpdateOpacity.propTypes = {
+ action: PropTypes.object,
+ activeLayers: PropTypes.array,
+ activeString: PropTypes.string,
+ compare: PropTypes.object,
+ findLayers: PropTypes.func,
+ isCompareActive: PropTypes.bool,
+ ui: PropTypes.object,
+ updateLayerVisibilities: PropTypes.func,
+};
diff --git a/web/js/mapUI/components/update-projection/updateProjection.js b/web/js/mapUI/components/update-projection/updateProjection.js
new file mode 100644
index 0000000000..9be8f61fe4
--- /dev/null
+++ b/web/js/mapUI/components/update-projection/updateProjection.js
@@ -0,0 +1,405 @@
+import { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import OlLayerGroup from 'ol/layer/Group';
+import {
+ each as lodashEach,
+ get as lodashGet,
+} from 'lodash';
+import {
+ fitToLeadingExtent,
+ updateMapUI,
+} from '../../../modules/map/actions';
+import { getLeadingExtent } from '../../../modules/map/util';
+import {
+ getLayers,
+ getActiveLayers,
+} from '../../../modules/layers/selectors';
+import { getSelectedDate } from '../../../modules/date/selectors';
+import util from '../../../util/util';
+import { fly } from '../../../map/util';
+import * as layerConstants from '../../../modules/layers/constants';
+import * as compareConstants from '../../../modules/compare/constants';
+import * as paletteConstants from '../../../modules/palettes/constants';
+import * as vectorStyleConstants from '../../../modules/vector-styles/constants';
+import { LOCATION_POP_ACTION } from '../../../redux-location-state-customs';
+import { EXIT_ANIMATION, STOP_ANIMATION } from '../../../modules/animation/constants';
+import { SET_SCREEN_INFO } from '../../../modules/screen-size/constants';
+
+const UpdateProjection = (props) => {
+ const {
+ action,
+ activeLayers,
+ compare,
+ compareMapUi,
+ compareMode,
+ config,
+ dateCompareState,
+ fitToLeadingExtent,
+ getGranuleOptions,
+ isMobile,
+ layerState,
+ map,
+ models,
+ preloadForCompareMode,
+ proj,
+ projectionTrigger,
+ updateExtent,
+ updateLayerVisibilities,
+ updateMapUI,
+ ui,
+ } = props;
+
+ useEffect(() => {
+ actionSwitch();
+ }, [action]);
+
+ useEffect(() => {
+ if (projectionTrigger === 1) {
+ updateProjection(true);
+ } else if (projectionTrigger > 1) {
+ updateProjection();
+ }
+ }, [projectionTrigger]);
+
+ const actionSwitch = () => {
+ switch (action.type) {
+ case STOP_ANIMATION:
+ case EXIT_ANIMATION:
+ return onStopAnimation();
+ case LOCATION_POP_ACTION: {
+ const newState = util.fromQueryString(action.payload.search);
+ const extent = lodashGet(map, 'extent');
+ const rotate = lodashGet(map, 'rotation') || 0;
+ setTimeout(() => {
+ updateProjection();
+ if (newState.v && !newState.e && extent) {
+ flyToNewExtent(extent, rotate);
+ }
+ }, 200); return;
+ }
+ case layerConstants.UPDATE_GRANULE_LAYER_OPTIONS: {
+ const granuleOptions = {
+ id: action.id,
+ reset: null,
+ };
+ return reloadLayers(granuleOptions);
+ }
+ case layerConstants.RESET_GRANULE_LAYER_OPTIONS: {
+ const granuleOptions = {
+ id: action.id,
+ reset: action.id,
+ };
+ return reloadLayers(granuleOptions);
+ }
+ case compareConstants.CHANGE_STATE:
+ if (compareMode === 'spy') {
+ return reloadLayers();
+ }
+ return;
+ case layerConstants.REORDER_LAYERS:
+ case layerConstants.REORDER_OVERLAY_GROUPS:
+ case compareConstants.TOGGLE_ON_OFF:
+ case compareConstants.CHANGE_MODE:
+ reloadLayers();
+ preloadForCompareMode();
+ return;
+ case layerConstants.TOGGLE_OVERLAY_GROUPS:
+ return reloadLayers();
+ case paletteConstants.SET_THRESHOLD_RANGE_AND_SQUASH:
+ case paletteConstants.SET_CUSTOM:
+ case paletteConstants.SET_DISABLED_CLASSIFICATION:
+ case paletteConstants.CLEAR_CUSTOM:
+ case layerConstants.ADD_LAYERS_FOR_EVENT:
+ return setTimeout(reloadLayers, 100);
+ case vectorStyleConstants.SET_FILTER_RANGE:
+ case vectorStyleConstants.SET_VECTORSTYLE:
+ case vectorStyleConstants.CLEAR_VECTORSTYLE:
+ case SET_SCREEN_INFO:
+ return onResize();
+ default:
+ break;
+ }
+ };
+
+ const flyToNewExtent = function(extent, rotation) {
+ const coordinateX = extent[0] + (extent[2] - extent[0]) / 2;
+ const coordinateY = extent[1] + (extent[3] - extent[1]) / 2;
+ const coordinates = [coordinateX, coordinateY];
+ const resolution = ui.selected.getView().getResolutionForExtent(extent);
+ const zoom = ui.selected.getView().getZoomForResolution(resolution);
+ // Animate to extent, zoom & rotate:
+ // Don't animate when an event is selected (Event selection already animates)
+ return fly(ui.selected, proj, coordinates, zoom, rotation);
+ };
+
+ /**
+ * Create a Layergroup given the date and layerGroups when compare mode is activated and
+ * the group similar layers option is selected.
+ *
+ * @method getCompareLayerGroup
+ * @static
+ *
+ * @param {string} compareActiveString
+ * @param {string} compareDateString
+ * @param {object} state object representing the layers, compare, & proj properties from global state
+ * @param {object} granuleOptions object representing selected granule layer options
+ */
+ async function getCompareLayerGroup([compareActiveString, compareDateString], state, granuleOptions) {
+ const { createLayer } = ui;
+ const compareSideLayers = getActiveLayers(state, compareActiveString);
+ const layers = getLayers(state, { reverse: true }, compareSideLayers)
+ .map(async (def) => {
+ const options = {
+ ...getGranuleOptions(state, def, compareActiveString, granuleOptions),
+ date: getSelectedDate(dateCompareState, compareDateString),
+ group: compareActiveString,
+ };
+ return createLayer(def, options);
+ });
+ const compareLayerGroup = await Promise.all(layers);
+
+ return new OlLayerGroup({
+ layers: compareLayerGroup,
+ date: getSelectedDate(dateCompareState, compareDateString),
+ group: compareActiveString,
+ });
+ }
+
+ /**
+ * Remove Layers from map
+ *
+ * @method clearLayers
+ * @static
+ *
+ * @returns {void}
+ */
+ const clearLayers = function() {
+ const activeLayersUI = ui.selected
+ .getLayers()
+ .getArray()
+ .slice(0);
+ lodashEach(activeLayersUI, (mapLayer) => {
+ ui.selected.removeLayer(mapLayer);
+ });
+ ui.cache.clear();
+ };
+
+ /**
+ * @method reloadLayers
+ *
+ * @param {Object} granuleOptions (optional: only used for granule layers)
+ * @param {Boolean} granuleDates - array of granule dates
+ * @param {Boolean} id - layer id
+ * @returns {void}
+ */
+ async function reloadLayers(granuleOptions) {
+ const mapUI = ui.selected;
+ const { createLayer } = ui;
+
+ if (!config.features.compare || !compare.active) {
+ const compareMapDestroyed = !compare.active && compareMapUi.active;
+ if (compareMapDestroyed) {
+ compareMapUi.destroy();
+ }
+ clearLayers();
+ const defs = getLayers(layerState, { reverse: true });
+ const layerPromises = defs.map((def) => {
+ const options = getGranuleOptions(layerState, def, compare.activeString, granuleOptions);
+ return createLayer(def, options);
+ });
+ const createdLayers = await Promise.all(layerPromises);
+ lodashEach(createdLayers, (l) => { mapUI.addLayer(l); });
+ } else {
+ const stateArray = [['active', 'selected'], ['activeB', 'selectedB']];
+ if (compare && !compare.isCompareA && compare.mode === 'spy') {
+ stateArray.reverse(); // Set Layer order based on active A|B group
+ }
+ clearLayers();
+ const stateArrayGroups = stateArray.map(async (arr) => getCompareLayerGroup(arr, layerState, granuleOptions));
+ const compareLayerGroups = await Promise.all(stateArrayGroups);
+ compareLayerGroups.forEach((layerGroup) => mapUI.addLayer(layerGroup));
+ compareMapUi.create(mapUI, compare.mode);
+ }
+ updateLayerVisibilities();
+ }
+
+ const onStopAnimation = function() {
+ const needsRefresh = activeLayers.some(({ type }) => type === 'granule' || type === 'vector');
+ if (needsRefresh) {
+ // The SELECT_DATE and STOP_ANIMATION actions happen back to back and both
+ // try to modify map layers asynchronously so we need to set a timeout to allow
+ // the updateDate() function to complete before trying to call reloadLayers() here
+ setTimeout(reloadLayers, 100);
+ }
+ };
+
+ /**
+ * When page is resized set for mobile or desktop
+ *
+ * @method onResize
+ * @static
+ *
+ * @returns {void}
+ */
+ function onResize() {
+ const mapUI = ui.selected;
+ if (isMobile) {
+ mapUI.removeControl(mapUI.wv.scaleImperial);
+ mapUI.removeControl(mapUI.wv.scaleMetric);
+ } else {
+ mapUI.addControl(mapUI.wv.scaleImperial);
+ mapUI.addControl(mapUI.wv.scaleMetric);
+ }
+ }
+
+ /**
+ * Show Map
+ *
+ * @method showMap
+ * @static
+ *
+ * @param {object} map - Openlayers Map obj
+ *
+ * @returns {void}
+ */
+ function showMap(map) {
+ document.getElementById(`${map.getTarget()}`).style.display = 'block';
+ }
+
+ /**
+ * Hide Map
+ *
+ * @method hideMap
+ * @static
+ *
+ * @param {object} map - Openlayers Map obj
+ *
+ * @returns {void}
+ */
+ function hideMap(map) {
+ document.getElementById(`${map.getTarget()}`).style.display = 'none';
+ }
+
+
+ const updateProjection = (start) => {
+ if (ui.selected) {
+ // Keep track of center point on projection switch
+ ui.selected.previousCenter = ui.selected.center;
+ hideMap(ui.selected);
+ }
+ ui.selected = ui.proj[proj.id];
+ const map = ui.selected;
+
+ const isProjectionRotatable = proj.id !== 'geographic' && proj.id !== 'webmerc';
+ const currentRotation = isProjectionRotatable ? map.getView().getRotation() : 0;
+ const rotationStart = isProjectionRotatable ? models.map.rotation : 0;
+ const rotation = start ? rotationStart : currentRotation;
+
+ updateMapUI(ui, rotation);
+
+ reloadLayers();
+
+ // If the browser was resized, the inactive map was not notified of
+ // the event. Force the update no matter what and reposition the center
+ // using the previous value.
+ showMap(map);
+
+ map.updateSize();
+
+ if (ui.selected.previousCenter) {
+ ui.selected.setCenter(ui.selected.previousCenter);
+ }
+
+ if (start) {
+ const projId = proj.selected.id;
+ let extent = null;
+ let callback = null;
+ if (models.map.extent) {
+ extent = models.map.extent;
+ } else if (!models.map.extent && projId === 'geographic') {
+ extent = getLeadingExtent(config.pageLoadTime);
+ callback = () => {
+ const view = map.getView();
+ const extent = view.calculateExtent(map.getSize());
+ fitToLeadingExtent(extent);
+ };
+ }
+ if (projId !== 'geographic') {
+ callback = () => {
+ const view = map.getView();
+ view.setRotation(rotationStart);
+ };
+ }
+ if (extent) {
+ map.getView().fit(extent, {
+ constrainResolution: false,
+ callback,
+ });
+ } else if (rotationStart && projId !== 'geographic') {
+ callback();
+ }
+ }
+ updateExtent();
+ onResize();
+ };
+
+ return null;
+};
+
+const mapStateToProps = (state) => {
+ const {
+ proj, map, screenSize, layers, compare, date,
+ } = state;
+ const layerState = { layers, compare, proj };
+ const isMobile = screenSize.isMobileDevice;
+ const dateCompareState = { date, compare };
+ const activeLayers = getActiveLayers(state);
+ const compareMode = compare.mode;
+ return {
+ activeLayers,
+ compare,
+ compareMode,
+ dateCompareState,
+ isMobile,
+ layerState,
+ proj,
+ map,
+ };
+};
+
+const mapDispatchToProps = (dispatch) => ({
+ fitToLeadingExtent: (extent) => {
+ dispatch(fitToLeadingExtent(extent));
+ },
+ updateMapUI: (ui, rotation) => {
+ dispatch(updateMapUI(ui, rotation));
+ },
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(UpdateProjection);
+
+UpdateProjection.propTypes = {
+ action: PropTypes.object,
+ activeLayers: PropTypes.array,
+ compare: PropTypes.object,
+ compareMapUi: PropTypes.object,
+ config: PropTypes.object,
+ dateCompareState: PropTypes.object,
+ fitToLeadingExtent: PropTypes.func,
+ getGranuleOptions: PropTypes.func,
+ isMobile: PropTypes.bool,
+ layerState: PropTypes.object,
+ map: PropTypes.object,
+ models: PropTypes.object,
+ preloadForCompareMode: PropTypes.func,
+ proj: PropTypes.object,
+ projectionTrigger: PropTypes.number,
+ ui: PropTypes.object,
+ updateExtent: PropTypes.func,
+ updateLayerVisibilities: PropTypes.func,
+ updateMapUi: PropTypes.func,
+};
diff --git a/web/js/mapUI/mapUI.js b/web/js/mapUI/mapUI.js
new file mode 100644
index 0000000000..097b95aa58
--- /dev/null
+++ b/web/js/mapUI/mapUI.js
@@ -0,0 +1,490 @@
+import React, { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+/* eslint-disable no-multi-assign */
+/* eslint-disable no-shadow */
+/* eslint-disable no-param-reassign */
+/* eslint-disable no-nested-ternary */
+import { each as lodashEach, find as lodashFind } from 'lodash';
+import AddLayer from './components/layers/addLayer';
+import RemoveLayer from './components/layers/removeLayer';
+import CreateMap from './components/create-map/createMap';
+import GranuleHover from './components/granule-hover/granuleHover';
+import Markers from './components/markers/markers';
+import UpdateDate from './components/update-date/updateDate';
+import UpdateOpacity from './components/update-opacity/updateOpacity';
+import UpdateProjection from './components/update-projection/updateProjection';
+import MouseMoveEvents from './components/mouse-move-events/mouseMoveEvents';
+import BufferQuickAnimate from './components/buffer-quick-animate/bufferQuickAnimate';
+import { LOCATION_POP_ACTION } from '../redux-location-state-customs';
+import { CHANGE_PROJECTION } from '../modules/projection/constants';
+import { SET_SCREEN_INFO } from '../modules/screen-size/constants';
+import {
+ REMOVE_MARKER,
+ SET_MARKER,
+ TOGGLE_DIALOG_VISIBLE,
+} from '../modules/location-search/constants';
+import * as dateConstants from '../modules/date/constants';
+import util from '../util/util';
+import * as layerConstants from '../modules/layers/constants';
+import * as compareConstants from '../modules/compare/constants';
+import * as paletteConstants from '../modules/palettes/constants';
+import * as vectorStyleConstants from '../modules/vector-styles/constants';
+import {
+ getActiveLayers,
+ isRenderable as isRenderableLayer,
+ getGranuleLayer,
+} from '../modules/layers/selectors';
+import { getSelectedDate } from '../modules/date/selectors';
+import { getNextDateTime } from '../modules/date/util';
+import { EXIT_ANIMATION, STOP_ANIMATION } from '../modules/animation/constants';
+import { REFRESH_ROTATE, CLEAR_ROTATE } from '../modules/map/constants';
+import { promiseImageryForTime } from '../modules/map/util';
+import { updateVectorSelection } from '../modules/vector-styles/util';
+import { REDUX_ACTION_DISPATCHED } from '../util/constants';
+import { updateMapExtent } from '../modules/map/actions';
+import { clearPreload, setPreload } from '../modules/date/actions';
+
+const { events } = util;
+
+const MapUI = (props) => {
+ const {
+ activeLayers,
+ activeLayersState,
+ activeString,
+ arrowDown,
+ clearPreload,
+ compare,
+ compareMapUi,
+ config,
+ dateCompareState,
+ embed,
+ lastArrowDirection,
+ layerQueue,
+ lastPreloadDate,
+ layers,
+ models,
+ palettes,
+ preloaded,
+ proj,
+ renderableLayersState,
+ selectedDate,
+ selectedDateB,
+ setPreload,
+ setUI,
+ ui,
+ updateMapExtent,
+ vectorStyles,
+ vectorStylesState,
+ } = props;
+
+ const [isMapSet, setMap] = useState(false);
+ const [projectionTrigger, setProjectionTrigger] = useState(0);
+ const [projectionAction, setProjectionAction] = useState({});
+ const [addLayerAction, setAddLayerAction] = useState({});
+ const [removeLayersAction, setRemoveLayersAction] = useState({});
+ const [dateAction, setDateAction] = useState({});
+ const [opacityAction, setOpacityAction] = useState({});
+ const [markerAction, setMarkerAction] = useState({});
+ const [granuleFootprints, setGranuleFootprints] = useState({});
+ const [quickAnimateAction, setQuickAnimateAction] = useState({});
+ const [vectorActions, setVectorActions] = useState({});
+ const [preloadAction, setPreloadAction] = useState({});
+
+ const subscribeToStore = function(action) {
+ switch (action.type) {
+ case CHANGE_PROJECTION: {
+ return setProjectionTrigger((projectionTrigger) => projectionTrigger + 1);
+ }
+ case layerConstants.ADD_LAYER:
+ return setAddLayerAction(action);
+ case STOP_ANIMATION:
+ case EXIT_ANIMATION:
+ case LOCATION_POP_ACTION:
+ case layerConstants.UPDATE_GRANULE_LAYER_OPTIONS:
+ case layerConstants.RESET_GRANULE_LAYER_OPTIONS:
+ case compareConstants.CHANGE_STATE:
+ case layerConstants.REORDER_LAYERS:
+ case layerConstants.REORDER_OVERLAY_GROUPS:
+ case compareConstants.TOGGLE_ON_OFF:
+ case compareConstants.CHANGE_MODE:
+ case layerConstants.TOGGLE_OVERLAY_GROUPS:
+ case paletteConstants.SET_THRESHOLD_RANGE_AND_SQUASH:
+ case paletteConstants.SET_CUSTOM:
+ case paletteConstants.SET_DISABLED_CLASSIFICATION:
+ case paletteConstants.CLEAR_CUSTOM:
+ case layerConstants.ADD_LAYERS_FOR_EVENT:
+ case vectorStyleConstants.SET_FILTER_RANGE:
+ case vectorStyleConstants.SET_VECTORSTYLE:
+ case vectorStyleConstants.CLEAR_VECTORSTYLE:
+ case SET_SCREEN_INFO:
+ return setProjectionAction(action);
+ case layerConstants.REMOVE_GROUP:
+ case layerConstants.REMOVE_LAYER:
+ return setRemoveLayersAction(action);
+ case dateConstants.SELECT_DATE:
+ case layerConstants.TOGGLE_LAYER_VISIBILITY:
+ case layerConstants.TOGGLE_OVERLAY_GROUP_VISIBILITY:
+ return setDateAction(action);
+ case layerConstants.UPDATE_OPACITY:
+ return setOpacityAction(action);
+ case REMOVE_MARKER:
+ case SET_MARKER:
+ case TOGGLE_DIALOG_VISIBLE:
+ return setMarkerAction(action);
+ case CLEAR_ROTATE: {
+ ui.selected.getView().animate({
+ duration: 500,
+ rotation: 0,
+ });
+ return;
+ }
+ case REFRESH_ROTATE: {
+ ui.selected.getView().animate({
+ rotation: action.rotation,
+ duration: 500,
+ });
+ return;
+ }
+ case vectorStyleConstants.SET_SELECTED_VECTORS:
+ return setVectorActions(action);
+ case dateConstants.CHANGE_CUSTOM_INTERVAL:
+ case dateConstants.CHANGE_INTERVAL:
+ return setPreloadAction(action);
+ case dateConstants.ARROW_DOWN:
+ setQuickAnimateAction(action);
+ break;
+ default:
+ break;
+ }
+ };
+
+ events.on(REDUX_ACTION_DISPATCHED, subscribeToStore);
+ window.addEventListener('orientationchange', () => {
+ setTimeout(() => { setProjectionTrigger((projectionTrigger) => projectionTrigger + 1); }, 200);
+ });
+
+ // Initial hook that initiates the map after it has been created in CreateMap.js
+ useEffect(() => {
+ if (document.getElementById('app')) {
+ setProjectionTrigger(1);
+ }
+ }, [ui]);
+
+ useEffect(() => {
+ if (vectorActions.type === vectorStyleConstants.SET_SELECTED_VECTORS) {
+ updateVectorSelections();
+ }
+ }, [vectorActions]);
+
+ useEffect(() => {
+ if (preloadAction.type === dateConstants.CHANGE_INTERVAL) {
+ preloadNextTiles();
+ }
+ }, [preloadAction]);
+
+ const updateVectorSelections = () => {
+ const type = 'selection';
+ const newSelection = vectorActions.payload;
+ updateVectorSelection(
+ vectorActions.payload,
+ ui.selectedVectors,
+ activeLayers,
+ type,
+ vectorStylesState,
+ );
+ ui.selectedVectors = newSelection;
+ };
+
+ const updateExtent = () => {
+ const map = ui.selected;
+ const view = map.getView();
+ const extent = view.calculateExtent();
+ updateMapExtent(extent);
+ if (map.isRendered()) {
+ clearPreload();
+ }
+ };
+
+ const updateLayerVisibilities = () => {
+ const layerGroup = ui.selected.getLayers();
+
+ const setRenderable = (layer, parentCompareGroup) => {
+ const { id, group } = layer.wv;
+ const dateGroup = layer.get('date') || group === 'active' ? 'selected' : 'selectedB';
+ const date = getSelectedDate(dateCompareState, dateGroup);
+ const layers = getActiveLayers(activeLayersState, parentCompareGroup || group);
+ const renderable = isRenderableLayer(id, layers, date, null, renderableLayersState);
+ layer.setVisible(renderable);
+ };
+
+ layerGroup.forEach((layer) => {
+ const compareActiveString = layer.get('group');
+ const granule = layer.get('granuleGroup');
+
+ // Not in A|B
+ if (layer.wv && !granule) {
+ setRenderable(layer);
+
+ // If in A|B layer-group will have a 'group' string
+ } else if (compareActiveString || granule) {
+ const compareGrouplayers = layer.getLayers().getArray();
+
+ compareGrouplayers.forEach((subLayer) => {
+ if (!subLayer.wv) {
+ return;
+ }
+ // TileLayers within granule LayerGroup
+ if (subLayer.get('granuleGroup')) {
+ const granuleLayers = subLayer.getLayers().getArray();
+ granuleLayers.forEach((l) => setRenderable(l));
+ subLayer.setVisible(true);
+ }
+ setRenderable(subLayer, compareActiveString);
+ });
+
+ layer.setVisible(true);
+ }
+ });
+ };
+
+ const findLayer = (def, activeCompareState) => {
+ const layers = ui.selected.getLayers().getArray();
+ let layer = lodashFind(layers, {
+ wv: {
+ id: def.id,
+ },
+ });
+
+ if (!layer && layers.length && (layers[0].get('group') || layers[0].get('granuleGroup'))) {
+ let olGroupLayer;
+ const layerKey = `${def.id}-${activeCompareState}`;
+ lodashEach(layers, (layerGroup) => {
+ if (layerGroup.get('layerId') === layerKey || layerGroup.get('group') === activeCompareState) {
+ olGroupLayer = layerGroup;
+ }
+ });
+ const subGroup = olGroupLayer.getLayers().getArray();
+ layer = lodashFind(subGroup, {
+ wv: {
+ id: def.id,
+ },
+ });
+ }
+ return layer;
+ };
+
+ /**
+ * Get granule options for layerBuilding
+ * @param {object} state
+ * @param {Object} def
+ * @param {String} layerGroupStr
+ * @param {Object} options
+ * @returns {Object}
+ */
+ const getGranuleOptions = (state, { id, count, type }, activeString, options) => {
+ if (type !== 'granule') return {};
+ const reset = options && options.reset === id;
+
+ const granuleState = getGranuleLayer(state, id, activeString);
+ let granuleDates;
+ let granuleCount;
+ let geometry;
+ if (granuleState) {
+ granuleDates = !reset ? granuleState.dates : false;
+ granuleCount = granuleState.count;
+ geometry = granuleState.geometry;
+ }
+ return {
+ granuleDates,
+ granuleCount: granuleCount || count,
+ geometry,
+ };
+ };
+
+ function preloadForCompareMode() {
+ preloadNextTiles(selectedDate, 'active');
+ if (compare.active) {
+ preloadNextTiles(selectedDateB, 'activeB');
+ }
+ }
+
+ async function preloadNextTiles(date, compareString) {
+ const map = { ui };
+ const state = {
+ proj, embed, layers, palettes, vectorStyles, compare, map,
+ };
+ const useActiveString = compareString || activeString;
+ const useDate = date || (preloaded ? lastPreloadDate : getSelectedDate(dateCompareState));
+ const nextDate = getNextDateTime(dateCompareState, 1, useDate);
+ const prevDate = getNextDateTime(dateCompareState, -1, useDate);
+ const subsequentDate = lastArrowDirection === 'right' ? nextDate : prevDate;
+ if (preloaded && lastArrowDirection) {
+ setPreload(true, subsequentDate);
+ layerQueue.add(() => promiseImageryForTime(state, subsequentDate, useActiveString));
+ return;
+ }
+ layerQueue.add(() => promiseImageryForTime(state, nextDate, useActiveString));
+ layerQueue.add(() => promiseImageryForTime(state, prevDate, useActiveString));
+ if (!date && !arrowDown) {
+ preloadNextTiles(subsequentDate, useActiveString);
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+const mapStateToProps = (state) => {
+ const {
+ compare, config, date, embed, layers, map, palettes, proj, vectorStyles,
+ } = state;
+ const {
+ arrowDown, lastArrowDirection, lastPreloadDate, preloaded, selected, selectedB,
+ } = date;
+
+ const vectorStylesState = {
+ config, map, proj, vectorStyles,
+ };
+ const renderableLayersState = {
+ date, compare, config, proj,
+ };
+ const dateCompareState = { date, compare };
+ const activeLayersState = { embed, compare, layers };
+ const activeLayers = getActiveLayers(state);
+ const selectedDate = selected;
+ const selectedDateB = selectedB;
+ const { activeString } = compare;
+ const useDate = selectedDate || (preloaded ? lastPreloadDate : getSelectedDate(state));
+ const nextDate = getNextDateTime(state, 1, useDate);
+ const prevDate = getNextDateTime(state, -1, useDate);
+
+ return {
+ activeLayers,
+ activeLayersState,
+ activeString,
+ arrowDown,
+ compare,
+ dateCompareState,
+ embed,
+ lastArrowDirection,
+ lastPreloadDate,
+ layers,
+ nextDate,
+ palettes,
+ preloaded,
+ prevDate,
+ proj,
+ renderableLayersState,
+ selectedDate,
+ selectedDateB,
+ vectorStyles,
+ vectorStylesState,
+ };
+};
+
+const mapDispatchToProps = (dispatch) => ({
+ clearPreload: () => {
+ dispatch(clearPreload());
+ },
+ updateMapExtent: (extent) => {
+ dispatch(updateMapExtent(extent));
+ },
+ setPreload: (preloaded, lastPreloadDate) => {
+ dispatch(setPreload(preloaded, lastPreloadDate));
+ },
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(MapUI);
+
+MapUI.propTypes = {
+ activeLayers: PropTypes.array,
+ activeLayersState: PropTypes.object,
+ activeString: PropTypes.string,
+ arrowDown: PropTypes.string,
+ clearPreload: PropTypes.func,
+ compare: PropTypes.object,
+ compareMapUi: PropTypes.object,
+ config: PropTypes.object,
+ dateCompareState: PropTypes.object,
+ embed: PropTypes.object,
+ lastArrowDirection: PropTypes.string,
+ layerQueue: PropTypes.object,
+ layers: PropTypes.object,
+ lastPreloadDate: PropTypes.object,
+ models: PropTypes.object,
+ palettes: PropTypes.object,
+ preloaded: PropTypes.bool,
+ proj: PropTypes.object,
+ renderableLayersState: PropTypes.object,
+ selectedDate: PropTypes.object,
+ selectedDateB: PropTypes.object,
+ setPreload: PropTypes.func,
+ setUI: PropTypes.func,
+ ui: PropTypes.object,
+ updateMapExtent: PropTypes.func,
+ vectorStyles: PropTypes.object,
+ vectorStylesState: PropTypes.object,
+};
diff --git a/web/js/modules/animation/util.js b/web/js/modules/animation/util.js
index 1cffca337f..99646f0f59 100644
--- a/web/js/modules/animation/util.js
+++ b/web/js/modules/animation/util.js
@@ -36,6 +36,16 @@ export function snapToIntervalDelta(currDate, startDate, endDate, interval, delt
return currentDate || startDate;
}
+/**
+ * Calculate the required number of steps (frames) required for the animation
+ * @param {Date} start | The date of the first frame of animation
+ * @param {Date} end | The date of the last frame of animation
+ * @param {String} interval | The animation step value (Year/Month/Day/Custom) separating frames
+ * @param {Number} delta | Rate of change between states; defaults to 1 second
+ * @param {Number} maxToCheck | The limit on the total number of frames to be used
+ *
+ * @return {Number} | The total number of frames required
+ */
export function getNumberOfSteps(start, end, interval, delta = 1, maxToCheck) {
let i = 1;
let currentDate = start;
diff --git a/web/js/modules/layers/actions.js b/web/js/modules/layers/actions.js
index 82743003da..5ebf628da1 100644
--- a/web/js/modules/layers/actions.js
+++ b/web/js/modules/layers/actions.js
@@ -31,6 +31,8 @@ import {
UPDATE_GRANULE_LAYER_GEOMETRY,
RESET_GRANULE_LAYER_OPTIONS,
CHANGE_GRANULE_SATELLITE_INSTRUMENT_GROUP,
+ UPDATE_LAYER_COLLECTION,
+ UPDATE_LAYER_DATE_COLLECTION,
} from './constants';
import { updateRecentLayers } from '../product-picker/util';
import { getOverlayGroups, getLayersFromGroups } from './util';
@@ -391,3 +393,24 @@ export function changeGranuleSatelliteInstrumentGroup(id, granulePlatform) {
});
};
}
+
+export function updateLayerCollection(id) {
+ return (dispatch, getState) => {
+ const { layers } = getState();
+
+ const collections = layers.collections[id];
+ if (!collections) {
+ dispatch({
+ type: UPDATE_LAYER_COLLECTION,
+ id,
+ });
+ }
+ };
+}
+
+export function updateLayerDateCollection(layerInfo) {
+ return {
+ type: UPDATE_LAYER_DATE_COLLECTION,
+ ...layerInfo,
+ };
+}
diff --git a/web/js/modules/layers/constants.js b/web/js/modules/layers/constants.js
index ccd5be6c2a..c8caff9032 100644
--- a/web/js/modules/layers/constants.js
+++ b/web/js/modules/layers/constants.js
@@ -16,7 +16,8 @@ export const UPDATE_GRANULE_LAYER_OPTIONS = 'LAYERS/UPDATE_GRANULE_LAYER_OPTIONS
export const UPDATE_GRANULE_LAYER_GEOMETRY = 'LAYERS/UPDATE_GRANULE_LAYER_GEOMETRY';
export const RESET_GRANULE_LAYER_OPTIONS = 'LAYERS/RESET_GRANULE_LAYER_OPTIONS';
export const CHANGE_GRANULE_SATELLITE_INSTRUMENT_GROUP = 'LAYERS/CHANGE_GRANULE_SATELLITE_INSTRUMENT_GROUP';
-
+export const UPDATE_LAYER_COLLECTION = 'LAYERS/UPDATE_LAYER_COLLECTION';
+export const UPDATE_LAYER_DATE_COLLECTION = 'LAYERS/UPDATE_LAYER_DATE_COLLECTION';
export const DEFAULT_NUM_GRANULES = 10;
export const MIN_GRANULES = 1;
export const MAX_GRANULES = 30;
diff --git a/web/js/modules/layers/reducers.js b/web/js/modules/layers/reducers.js
index 2dd03ea5a0..7e0bc83ac5 100644
--- a/web/js/modules/layers/reducers.js
+++ b/web/js/modules/layers/reducers.js
@@ -20,6 +20,8 @@ import {
CHANGE_GRANULE_SATELLITE_INSTRUMENT_GROUP,
REORDER_OVERLAY_GROUPS,
REMOVE_GROUP,
+ UPDATE_LAYER_COLLECTION,
+ UPDATE_LAYER_DATE_COLLECTION,
} from './constants';
import {
SET_CUSTOM as SET_CUSTOM_PALETTE,
@@ -51,6 +53,7 @@ const groupState = {
export const initialState = {
active: { ...groupState },
activeB: { ...groupState },
+ collections: {},
layerConfig: {},
startingLayers: [],
granuleFootprints: {},
@@ -339,6 +342,34 @@ export function layerReducer(state = initialState, action) {
},
});
+ case UPDATE_LAYER_COLLECTION:
+ return update(state, {
+ collections: {
+ $merge: {
+ [action.id]: {
+ dates: [],
+ },
+ },
+ },
+
+ });
+
+ case UPDATE_LAYER_DATE_COLLECTION:
+ return update(state, {
+ collections: {
+ [action.id]: {
+ dates: {
+ $push: [{
+ version: action.collection.version,
+ type: action.collection.type,
+ date: action.date,
+ }],
+ },
+ },
+ },
+
+ });
+
default:
return state;
}
diff --git a/web/js/modules/layers/selectors.js b/web/js/modules/layers/selectors.js
index ea526739a7..d035628df5 100644
--- a/web/js/modules/layers/selectors.js
+++ b/web/js/modules/layers/selectors.js
@@ -25,6 +25,16 @@ export const getStartingLayers = createSelector([getConfig], (config) => resetLa
export const isGroupingEnabled = ({ compare, layers }) => layers[compare.activeString].groupOverlays;
+export const getCollections = (layers, date, layer) => {
+ if (!layers.collections[layer.id]) return;
+ const dateCollection = layers.collections[layer.id].dates;
+ for (let i = 0; i < dateCollection.length; i += 1) {
+ if (dateCollection[i].date === date) {
+ return dateCollection[i];
+ }
+ }
+};
+
/**
* Return a list of layers for the currently active compare state
* regardless of projection
diff --git a/web/js/modules/layers/util.js b/web/js/modules/layers/util.js
index 67df2b263c..44ff6184cf 100644
--- a/web/js/modules/layers/util.js
+++ b/web/js/modules/layers/util.js
@@ -8,7 +8,6 @@ import {
isEqual as lodashIsEqual,
} from 'lodash';
import moment from 'moment';
-
import googleTagManager from 'googleTagManager';
import update from 'immutability-helper';
import {
@@ -1276,18 +1275,10 @@ export function mapLocationToLayerState(
});
}
- // TODO how do we properly combine initial state with location state
- newStateFromLocation.layers.active = {
- ...newStateFromLocation.layers.active,
- granuleLayers: {},
- granulePlatform: '',
- };
-
- newStateFromLocation.layers.activeB = {
- ...newStateFromLocation.layers.activeB,
- granuleLayers: {},
- granulePlatform: '',
- };
+ newStateFromLocation.layers = update(state.layers, {
+ active: { $merge: newStateFromLocation.layers.active },
+ activeB: { $merge: newStateFromLocation.layers.activeB },
+ });
return newStateFromLocation;
}
diff --git a/web/js/modules/layers/util.test.js b/web/js/modules/layers/util.test.js
index 9d99fa6657..ea59ff0e6c 100644
--- a/web/js/modules/layers/util.test.js
+++ b/web/js/modules/layers/util.test.js
@@ -17,6 +17,9 @@ let defaultStateFromLocation = {
active: {
layers: [],
},
+ activeB: {
+ layers: [],
+ },
},
};
const globalState = fixtures.getState();
@@ -107,6 +110,9 @@ describe('permalink 1.0', () => {
active: {
layers: [],
},
+ activeB: {
+ layers: [],
+ },
},
};
});
@@ -136,6 +142,9 @@ describe('permalink 1.1', () => {
active: {
layers: [],
},
+ activeB: {
+ layers: [],
+ },
},
};
});
@@ -245,6 +254,9 @@ describe('Date range building', () => {
active: {
layers: [],
},
+ activeB: {
+ layers: [],
+ },
},
};
});
diff --git a/web/js/modules/map/actions.js b/web/js/modules/map/actions.js
index 458ec3c527..d428a0c8d1 100644
--- a/web/js/modules/map/actions.js
+++ b/web/js/modules/map/actions.js
@@ -1,5 +1,11 @@
import {
- CLEAR_ROTATE, CHANGE_CURSOR, REFRESH_ROTATE,
+ CLEAR_ROTATE,
+ CHANGE_CURSOR,
+ REFRESH_ROTATE,
+ UPDATE_MAP_EXTENT,
+ RENDERED,
+ UPDATE_MAP_UI,
+ FITTED_TO_LEADING_EXTENT,
} from './constants';
export function clearRotate() {
@@ -20,3 +26,31 @@ export function changeCursor(bool) {
bool,
};
}
+
+export function updateMapExtent(extent) {
+ return {
+ type: UPDATE_MAP_EXTENT,
+ extent,
+ };
+}
+
+export function updateRenderedState() {
+ return {
+ type: RENDERED,
+ };
+}
+
+export function updateMapUI(ui, rotation) {
+ return {
+ type: UPDATE_MAP_UI,
+ ui,
+ rotation,
+ };
+}
+
+export function fitToLeadingExtent(extent) {
+ return {
+ type: FITTED_TO_LEADING_EXTENT,
+ extent,
+ };
+}
diff --git a/web/js/modules/map/util.js b/web/js/modules/map/util.js
index 97f492649e..b5162facf5 100644
--- a/web/js/modules/map/util.js
+++ b/web/js/modules/map/util.js
@@ -284,6 +284,7 @@ function promiseLayerGroup(layerGroup, map) {
*/
export async function promiseImageryForTime(state, date, activeString) {
const { map } = state;
+ if (!map.ui.proj) return;
const {
cache, selected, createLayer, layerKey,
} = map.ui;
diff --git a/web/scss/components/tooltip.scss b/web/scss/components/tooltip.scss
index 696d12756f..9f170aba34 100644
--- a/web/scss/components/tooltip.scss
+++ b/web/scss/components/tooltip.scss
@@ -205,6 +205,26 @@
}
}
+/* bootstrap overrides */
+#center-align-tooltip {
+ padding: 7px !important;
+ border-radius: 5px !important;
+}
+
+#coordinate-setting-tooltip {
+ width: 80%;
+ position: relative;
+ right: -40px;
+ top: 10px;
+}
+
+#temperature-setting-tooltip {
+ width: 70%;
+ padding: 4px;
+ position: relative;
+ top: 10px;
+}
+
@media (max-width: $mobile-max-width) {
.tooltip-custom-black {
&.tooltip-coordinates-container {
diff --git a/web/scss/features/layers.scss b/web/scss/features/layers.scss
index 1f96c84a2f..ea06aab95b 100644
--- a/web/scss/features/layers.scss
+++ b/web/scss/features/layers.scss
@@ -1002,6 +1002,16 @@
}
}
+.collection-title {
+ margin-right: 4px;
+}
+
+.instrument-collection {
+ margin-top: 3px;
+ display: flex;
+ justify-content: space-between;
+}
+
/** React-Swipe-To-Delete Overrides */
.swipe-to-delete .js-content:first-child {
position: relative !important;