PageSourceSearch

https://www.drought.gov/modules/custom/general_page_js_css/js/map-helpers/ol-maps.js?tlrx84

js drought.gov collected 2026-09-24 08:36:13 UTC 57,500 bytes, 1,642 lines download raw bytes

1/**
2 * @file
3 *
4 * This file handles top level functions required for the site wide openlayers maps.
5 */
6/**
7 * @typedef {{}} MapConfig
8 * @property {ol.layer.Vector} conus - Pointer to transparent conus boundary used for fitting map
9 * @property {ol.layer.Vector|ol.layer.Tile} dataLayer - Pointer to primary data layer
10 * @property {{}} basemaps - Object of keyed tiles/vector tiles/vectors used as basemaps
11 * @property {ol.layer.Group} dataLayerGroup - Pointer to parent group for data layer. Allows layer to be hot swapped
12 *   without shuffling other layers
13 * @property {ol.layer.Vector|undefined} states - Shows state lines & is interactive when needed
14 * @property {ol.layer.Group} usdmUnder - Group where the USDM / data underlayers are rendered
15 * @property {ol.layer.Vector} highlight - Empty pre-styled layer where the highlighted features are cloned to
16 * @property {ol.layer.Group} basemapGroup - Group where the basemaps are rendered
17 * @property {ol.layer.Group} labelsGroup - Group for the ESRI Vector Tile labels
18 * @property {ol.layer.Vector|undefined} watersheds - Shows watershed lines & is interactive when needed
19 * @property {[]} layers - Array of all Layers and Groups to be drawn
20 * @property {ol.layer.Group} allCounties - Renders every county border on screenshots and NWS maps
21 * @property {ol.layer.Vector|undefined} counties - Individual state county lines
22 * @property {ol.layer.Group} usdmOver - Group where the USDM / data over-layers are rendered
23 * @property {[ol.layer.Vector]} highlightableLayers - Array of pointers to layers which can be highlighted on mouseover
24 * @property {ol.layer.Vector|undefined} tribal - Boundaries of all Tribal Nations.
25 * @property {ol.layer.Group} aoiLines - Custom area of interest lines. Generally added by class.
26 */
27/**
28 * @typedef {ol.Map} DMap - Map Object used across site.
29 * @extends MapConfig
30 */
31(function (
32  $,
33  drupalSettings,
34  mapsUtil,
35  overlay,
36  ol,
37  olClick,
38  olCounties,
39  olExtents,
40  olFixedLayers,
41  olLayers,
42  olStyles,
43  tableReader,
44  aoiOverride
45) {
46  "use strict";
47
48  (function main() {
49    _exportConfig();
50  })();
51
52  /**
53   * Draws an OL map from scratch, sets all needed properties on it.
54   * @param {Element} wrapper - Wrapper for a panel, keyed by .interactive-map-panel.
55   * @param {Metadata} metadata - Metadata for a map.
56   * @return {{}} - Extended OL map object.
57   */
58  function drawInitialOpenLayers(wrapper, metadata) {
59    metadata.abv = mapsUtil.getLayerAbv(metadata);
60    const mapElem = mapsUtil.findMap(wrapper);
61    mapElem.setAttribute("tabIndex", "0");
62    mapElem.style.minHeight = "300px";
63    if (metadata["minimum-map-height"]) {
64      mapElem.style.minHeight = metadata["minimum-map-height"].toString().replace("px", "").trim() + "px";
65    }
66    mapElem.style.overflow = "hidden";
67    const layers = buildBaseLayers(mapElem, metadata);
68    const interactions = ol.interaction.defaults.defaults();
69    interactions.set("onFocusOnly", true);
70    const fullScreen = new ol.control.FullScreen();
71    const controls =
72      window.hasOwnProperty("checkMobile") && window.checkMobile()
73      ? ol.control.defaults.defaults().extend([fullScreen])
74      : ol.control.defaults.defaults();
75    const map = new ol.Map({
76      target: mapElem,
77      layers: layers.layers,
78      interactions,
79      view: new ol.View(getOpenLayersViewOptions(metadata)),
80      controls,
81    });
82    // TODO: bind the config to a property and extract it from there rather than the piecemeal way here
83    map.dataLayer = layers.dataLayer;
84    map.dataLayerGroup = layers.dataLayerGroup;
85    map.highlightLayer = layers.highlight;
86    map.maskReceivingLayer = layers.maskReceivingLayer;
87    map.abv = metadata.abv;
88    map.conus = layers.conus;
89    map.allCounties = layers.allCounties;
90    map.states = layers.states;
91    map.tribal = layers.tribal;
92    map.tribalPage = layers.tribalPage;
93    map.highlightableLayers = layers.highlightableLayers;
94    map.basemaps = layers.basemaps;
95    map.basemapGroup = layers.basemapGroup;
96    map.labelsGroup = layers.labelsGroup;
97    map.initialFitDone = metadata.initialFitDone;
98    map.mainFeatureOutlined = false;
99    map.usdmUnder = layers.usdmUnder;
100    map.usdmOver = layers.usdmOver;
101    map.watersheds = layers.watersheds;
102    map.watershedName = layers.watershedName;
103    map.counties = layers.counties;
104    map.aoiLines = layers.aoiLines;
105    map.metadata = metadata;
106
107    _tryPrecomputedFit(map);
108    _setupInitialZoomListeners(map);
109    _handleClickableLayers(map);
110
111    if (!map.mapElem) {
112      map.mapElem = mapElem;
113    }
114
115    if (!drupalSettings.state_name && !drupalSettings.fipsCode && !drupalSettings.watershed_name) {
116      addOLJumpButtons(map, wrapper);
117      filterOLJumpButtons(map, wrapper, metadata);
118    }
119
120    if (window.hasOwnProperty("checkMobile") && window.checkMobile()) {
121      addOLLegendButton(map, wrapper);
122      map.fullScreen = fullScreen;
123    }
124
125    if (!wrapper.parentElement.classList.contains("map-screenshot-map-wrappers")) {
126      tableReader.addTableReader(map, wrapper, metadata);
127    }
128
129    map.on("singleclick", olClick.OLMapSingleClick);
130    map.on("pointermove", olClick.handlePointerMove);
131    map.on("moveend", olMapMoveEnd);
132    map.on("rendercomplete", olMapRenderComplete);
133    map.getViewport().addEventListener("mouseout", function (evt) {
134      removeAllHighlights(map);
135    });
136    map.pointerMoveEventSet = true;
137
138    resizeOLMap.call(map);
139    createInitialOLPopup(map, mapsUtil.findMap(wrapper));
140    olCounties.addCountyDropdownToMap(mapElem, map.abv);
141    $(mapElem).data("map", map);
142
143    OLApplyFilter(map, metadata);
144    mapElem.parentNode.dispatchEvent(new Event("mapRendered"));
145    document.addEventListener("scroll", handleDocumentScroll.bind(window, map));
146    return map;
147  }
148
149  /**
150   * Enables MouseWheelZoom interactions for a map.
151   * @param {ol.interaction} interaction - An interaction for the map. Looped over all possible interactions.
152   */
153  function enableMouseWheelZoom(interaction) {
154    if (interaction instanceof ol.interaction.MouseWheelZoom) {
155      interaction.setActive(true);
156    }
157  }
158
159  /**
160   * Disables MouseWheelZoom interactions for a map.
161   * @param {ol.interaction} interaction - An interaction for the map. Looped over all possible interactions.
162   */
163  function disableMouseWheelZoom(interaction) {
164    if (interaction instanceof ol.interaction.MouseWheelZoom) {
165      interaction.setActive(false);
166    }
167  }
168
169  /**
170   * Disables mouse wheel zooms for maps for a short window of time when the page is scrolled.
171   *
172   * Users had issues with the map stealing scroll intent when they hovered over them.
173   * @param {*} map - OpenLayers map object defined in drawInitialOpenLayers
174   */
175  function handleDocumentScroll(map) {
176    clearTimeout(map.get("scrollTimeout"));
177    map.getInteractions().forEach(disableMouseWheelZoom, this);
178    map.set(
179      "scrollTimeout",
180      setTimeout(function () {
181        map.getInteractions().forEach(enableMouseWheelZoom, this);
182      }, 400)
183    );
184  }
185
186  /**
187   * Sets listeners on the appropriate vector layers for when they first load to
188   * zoom to their geometry.
189   * @param {{}} map - OL Map object.
190   */
191  function _setupInitialZoomListeners(map) {
192    if (map.watershedName && map.watersheds) {
193      map.watersheds.getSource().on("featuresloadend", function () {
194        _handleMapInitialFit(map);
195      });
196    } else if (map.abv) {
197      if (aoiOverride.mapOverridesAoi(map.metadata) || aoiOverride.urlOverridesAoi()) {
198        map.aoiLines.getSource().on("featuresloadend", function () {
199          _handleMapInitialFit(map);
200        });
201      }
202      if (map.abv === "us" || map.abv === "tribal" || map.abv === "nws") {
203        map.conus.getSource().on("featuresloadend", function () {
204          _handleMapInitialFit(map);
205        });
206      } else if (map.abv !== "international") {
207        map.states.getSource().on("featuresloadend", function () {
208          _handleMapInitialFit(map);
209        });
210      }
211    }
212  }
213
214  /**
215   * Toggles the display of jump to region buttons for a map.
216   * @param {{}} map - OL Map object.
217   * @param {Element} wrapper - Wrapper for a panel, keyed by .interactive-map-panel.
218   * @param {Metadata} metadata - Metadata for a map.
219   */
220  function filterOLJumpButtons(map, wrapper, metadata) {
221    const filters = metadata.missing;
222    const buttons = wrapper.querySelectorAll(".ol-jump-button");
223    let i, l, button;
224    for (i = 0, l = buttons.length; i < l; i++) {
225      button = buttons[i];
226      if (filters.indexOf(button.getAttribute("data-key")) === -1) {
227        button.style.removeProperty("display");
228      } else {
229        button.style.display = "none";
230      }
231    }
232    if (
233      map.abv === "international" ||
234      (filters.indexOf("alaska") !== -1 && filters.indexOf("hawaii") !== -1 && filters.indexOf("puertorico") !== -1)
235    ) {
236      for (i = 0, l = buttons.length; i < l; i++) {
237        buttons[i].style.display = "none";
238      }
239    }
240  }
241
242  /**
243   * Draws the main data layer for a map.
244   * @param {{}} map - OL Map object.
245   * @param {Metadata} metadata - Metadata for a map.
246   * @param {Element} mapElem - Result of mapsUtil.findMap, keyed by .interactive-map.
247   */
248  function drawOLDataLayer(map, metadata, mapElem) {
249    const newDataLayer = makeOLDataLayer(mapElem, metadata);
250    newDataLayer.setOpacity(metadata.opacity);
251    const watershedLayers = getWatershedLayers(metadata);
252
253    map.dataLayerGroup.getLayers().clear();
254    map.dataLayerGroup.setLayers(createDataLayerCollection(metadata, newDataLayer, watershedLayers));
255    map.dataLayer = newDataLayer;
256    _handleClickableLayers(map);
257  }
258
259  /**
260   * Gets the layers needed for a watershed if the page renders them.
261   * @param {Metadata} metadata - Metadata for a map.
262   * @return {{}|undefined} - Result of olFixedLayers.watersheds.
263   */
264  function getWatershedLayers(metadata) {
265    if (drupalSettings.drawWatershedOutlines) {
266      return olFixedLayers.watersheds(drupalSettings.watershed_name, metadata.projection);
267    }
268    return undefined;
269  }
270
271  function addVectorTileLayers(map) {
272    olLayers.redrawOlBasemaps(map, map.metadata.basemap);
273    drawOLDataLayer(map, map.metadata, map.mapElem);
274  }
275
276  function removeVectorTileLayers(map) {
277    map.basemapGroup.getLayers().clear();
278    map.labelsGroup.getLayers().clear();
279    map.dataLayerGroup.getLayers().clear();
280  }
281
282  /**
283   * Adds layers to the clickableLayer array
284   * @param {ol.Map} map - Individual OpenLayers map.
285   * @private
286   */
287  function _handleClickableLayers(map) {
288    map.clickableLayers = [];
289    if (drupalSettings.drawCountyOutlines) {
290      map.clickableLayers.push(map.counties);
291    }
292    if (drupalSettings.drawWatershedOutlines && drupalSettings.watershedClickable !== false) {
293      map.clickableLayers.push(map.watersheds);
294    }
295    if (olClick.checkStatesClickable()) {
296      map.clickableLayers.push(map.states);
297    }
298    if (map.abv === "tribal") {
299      map.clickableLayers.push(map.tribalPage);
300    }
301
302    // If switching to a tile layer, openlayers throws an error because getUrl is not a function for tile layers.
303    // - DS 7/29/21
304    if (typeof map.dataLayer.getSource().getUrl === "function") {
305      const url = map.dataLayer.getSource().getUrl();
306      if (olClick.checkDataLayerIsClickable(url)) {
307        map.clickableLayers.push(map.dataLayer);
308      }
309    }
310  }
311
312  /**
313   * Gets the initial view options for an OpenLayers map.
314   *
315   * @param {Metadata} metadata - Metadata for current map.
316   * @return {{}} - https://openlayers.org/en/latest/apidoc/module-ol_View-View.html
317   */
318  function getOpenLayersViewOptions(metadata) {
319    const proj = getProjectionMachineName(metadata.projection);
320    const abv = metadata.abv;
321
322    // The CMORPH dataset is more limited, so we are starting from a slightly more zoomed in extent
323    const CMORPH_SEARCH_STRING = "noaa-cmorph/cmorph_spi_gamma_90_day_latest";
324    const CMORPH_ZOOM = 2.75;
325
326    const options = {
327      projection: proj,
328      center: getOpenLayersCenterPoint(proj, abv),
329      zoom:
330        abv === "international" && metadata.layer.indexOf(CMORPH_SEARCH_STRING) !== -1
331          ? CMORPH_ZOOM
332          : getOpenLayersZoom(abv),
333      minZoom: abv === "international" ? 1 : 3,
334    };
335
336    if (metadata.layer.indexOf("sector/energy") !== -1) {
337      options.maxZoom = 6;
338    }
339
340    return options;
341  }
342
343  /**
344   * Determines the initial center point of the map.
345   * @param {string} proj - EPSG:3857 or EPSG:5070
346   * @param {string} abv - Type of map, only relevant if "international".
347   * @return {[number, number]} - Long/Lat coordinate pair.
348   */
349  function getOpenLayersCenterPoint(proj, abv) {
350    const latitude = parseFloat(drupalSettings.latitude) || 39.8283;
351    const longitude = parseFloat(drupalSettings.longitude) || -98.5795;
352    let lonlat;
353
354    if (abv === "international") {
355      lonlat = [-609651.4994666161, 3477747.7415025267];
356    } else if (proj === "EPSG:3857") {
357      lonlat = ol.proj.fromLonLat([longitude, latitude]);
358      // } else if (proj === "EPSG:5070") {
359      //   lonlat = ol.proj.fromLonLat([longitude, latitude], proj);
360    } else {
361      lonlat = [-49867.43369692238, 1720238.2767194216];
362    }
363
364    return lonlat;
365  }
366
367  /**
368   * Determines the initial zoom for a map before fitting.
369   * @param {string} abv - Type of map. Custom values for "international", "us", "tribal".
370   * @return {number} - Initial zoom for map. 1 for international, 3.89773 for conus, 7 otherwise.
371   */
372  function getOpenLayersZoom(abv) {
373    if (abv === "international") {
374      return 1;
375    }
376    if (abv === "us" || abv === "tribal") {
377      return 3.89773;
378    }
379    if (abv === "nws") {
380      return 4.357637643151119;
381    }
382    return 7;
383  }
384
385  /**
386   * Translates Drupal's representation of a map projection to openlayers' representation of a map projection.
387   * @param {string} proj - epsg3857 (web mercator) or epsg5070 (albers).
388   * @return {string} - EPSG:3857 or EPSG:5070.
389   */
390  function getProjectionMachineName(proj) {
391    if (proj === "epsg3857") {
392      return "EPSG:3857";
393    }
394    return "EPSG:5070";
395  }
396
397  /**
398   *
399   */
400  function olMapRenderComplete() {
401    const map = this;
402    map.clickableLayers.forEach(olClick.makeVectorLayerInteractive);
403    OLApplyMask(map);
404    outlineMainFeature(map);
405    resizeOLMap.call(map);
406    if (!map.initialFitDone) {
407      _handleMapInitialFit(map);
408    }
409    if (!map.mapMarkerRendered) {
410      if (window.drupalSettings.drawLocationMarker) {
411        const point = ol.proj.fromLonLat([window.drupalSettings.longitude, window.drupalSettings.latitude]);
412        _addMapMarker(map, point);
413        map.mapMarkerRendered = true;
414      } else if (window.drupalSettings.drawCountyMarker) {
415        const point = mapsUtil.getCenterOfExtent(
416          getLayerFeature(map.counties, "FIPS", window.drupalSettings.fipsCode).getGeometry().getExtent()
417        );
418        _addMapMarker(map, point);
419        map.mapMarkerRendered = true;
420      }
421    }
422  }
423
424  /**
425   * Handles the parameters needed for a maps initial fit. Dynamically finds the
426   * center and zoom by calling the recenterOLMap function then sets relevant
427   * variables.
428   * @param {ol.Map<Object>} map - OL Map object.
429   */
430  function _handleMapInitialFit(map) {
431    recenterOLMap(map);
432    OLApplyFilter(map, map.metadata);
433    map.currentZoom = map.getView().getZoom();
434    map.set("initialCenter", map.getView().getCenter());
435    map.set("initialZoom", map.getView().getZoom());
436    map.initialFitDone = true;
437  }
438
439  /**
440   * Updates the style of the primary feature of a map to have thick lines.
441   * @param {ol.Map<Object>} map - OL Map object.
442   */
443  function outlineMainFeature(map) {
444    if (drupalSettings.highlightMainFeature && map.mainFeatureOutlined === false) {
445      let url;
446      if (drupalSettings.watershed_file_name) {
447        url = window.location.origin + "/modules/custom/nidis_maps/watershed/" + drupalSettings.watershed_file_name;
448      }
449
450      if (url) {
451        // TODO: make this a cached style
452        const outlineStyle = new ol.style.Style({
453          stroke: new ol.style.Stroke({
454            color: "#000",
455            width: 2,
456          }),
457        });
458        const layer = olLayers.makeCachedVectorLayer(url, outlineStyle);
459        map.addLayer(layer);
460        map.mainFeatureOutlined = true;
461      }
462    }
463  }
464
465  /**
466   * Dispatches the handlers which need to happen when a map is zoomed or
467   * panned.
468   */
469  function olMapMoveEnd() {
470    const map = this;
471    _olMapHandleHighlightInteractions(map);
472    checkTileErrors(map);
473  }
474
475  /**
476   * Disables highlights when the map is zoomed too far in, re-enables them when
477   * the map is zoomed out.
478   *
479   * @param {Object} map
480   *   OpenLayers map object.
481   * @private
482   */
483  function _olMapHandleHighlightInteractions(map) {
484    const newZoom = map.getView().getZoom();
485    if (map.currentZoom !== newZoom) {
486      map.currentZoom = newZoom;
487      const zoomBreakpoint = _getZoomBreakpoint();
488      if (newZoom > zoomBreakpoint) {
489        map.un("pointermove", olClick.handlePointerMove);
490        removeAllHighlights(map);
491        map.pointerMoveEventSet = false;
492      } else if (!map.pointerMoveEventSet) {
493        map.on("pointermove", olClick.handlePointerMove);
494        map.pointerMoveEventSet = true;
495      }
496    }
497  }
498
499  /**
500   * Determines the maximum zoom level for a map.
501   * @return {number} - Max zoom level for a map.
502   */
503  function _getZoomBreakpoint() {
504    const smallStates = ["NJ", "CT", "MD", "MA", "VT", "NH", "RI", "DE", "HI", "PR", "GU"];
505    if (drupalSettings.watershed_name || drupalSettings.drawStateOutlines) {
506      if (drupalSettings.abv_state) {
507        if (smallStates.includes(drupalSettings.abv_state.toUpperCase())) {
508          return 10;
509        }
510      }
511      return 8;
512    }
513    if (drupalSettings.fipsCode) {
514      return 10;
515    }
516    return 12;
517  }
518
519  /**
520   * Draws a marker on a map, either where a user clicks or in the center of a region.
521   * @param {ol.Map<Object>} map - OL Map object.
522   * @param {[]} point - Array of two or four numbers, if it's a point or an extent.
523   */
524  function _addMapMarker(map, point) {
525    let xy = point;
526    if (point.length === 4) {
527      xy = mapsUtil.getCenterOfExtent(point);
528    }
529    const iconLayer = new ol.layer.Vector({
530      source: new ol.source.Vector(),
531      style: new ol.style.Style({
532        image: new ol.style.Icon({
533          anchor: [0.5, 46],
534          anchorXUnits: "fraction",
535          anchorYUnits: "pixels",
536          src: window.location.origin + "/modules/custom/nidis_maps/icons/map-marker.png",
537        }),
538      }),
539      zIndex: 999,
540    });
541    if (xy) {
542      iconLayer.getSource().addFeature(
543        new ol.Feature({
544          name: window.drupalSettings.full_county,
545          geometry: new ol.geom.Point(xy),
546        })
547      );
548      map.addLayer(iconLayer);
549    }
550  }
551
552  /**
553   * Removes highlighted features from a map.
554   * @param {ol.Map<Object>} map - OL Map object.
555   */
556  function removeAllHighlights(map) {
557    if (map.highlight) {
558      map.highlightLayer.getSource().removeFeature(map.highlight);
559      map.highlight = false;
560    }
561  }
562
563  /**
564   * Setup all the base layers for a given map.
565   *
566   * @param {Element} mapElem - HTML Element the OpenLayers map is in.
567   * @param {Metadata} metadata - Pre-parsed metadata object for the current map.
568   * @return {MapConfig} - Various config options and layers needed for the map.
569   */
570  function buildBaseLayers(mapElem, metadata) {
571    const styles = olStyles.getStyle(null);
572
573    const layers = [];
574    const highlightableLayers = [];
575    const watershedLayers = getWatershedLayers(metadata);
576    let stateLines, watershedLines, tribalPage;
577
578    if (window.drupalSettings.watershed_name && metadata.basemap.includes("labels_only")) {
579      metadata.basemap = "watershed_labels_only";
580    }
581
582    const basemaps = olFixedLayers.basemaps();
583
584    const basemapGroup = createOLGroup([basemaps[metadata.basemap]]);
585    const usdmUnder = createOLGroup();
586    const dataLayerGroup = createOLGroup();
587    const usdmOver = createOLGroup();
588    const allCountyLines = createOLGroup();
589    const tribalLines = createOLGroup();
590    const labelsGroup = createOLGroup();
591    const aoiOverrideGroup = createOLGroup();
592
593    layers.push(basemapGroup);
594
595    setupUSDMGroup(metadata, usdmUnder, layers, "below", 0.8);
596
597    const dataLayer = makeOLDataLayer(mapElem, metadata);
598    dataLayer.setOpacity(metadata.opacity);
599    dataLayerGroup.setLayers(createDataLayerCollection(metadata, dataLayer, watershedLayers));
600    layers.push(dataLayerGroup);
601    if (typeof dataLayer.getSource().getUrl === "function") {
602      const url = dataLayer.getSource().getUrl();
603      if (olClick.checkDataLayerIsClickable(url)) {
604        highlightableLayers.push(dataLayer);
605      }
606    }
607
608    setupUSDMGroup(metadata, usdmOver, layers, "top", 0.5);
609
610    // For the Screenshot widget we need to be able to see all c
610ounty lines for
611    // the country. This draws them but hides them in all other cases.
612    // NWS requested county lines on their maps so this overrides it.
613    // TODO: Develop more granular/classed based way to force county line display.
614    if (
615      (mapElem.closest(".map-screenshot-overlay") && $(window).width() >= 1024) ||
616      aoiOverride.mapOverridesAoi(metadata)
617    ) {
618      allCountyLines.setLayers(new ol.Collection([olFixedLayers.allCounties()]));
619    }
620    allCountyLines.setOpacity(aoiOverride.mapOverridesAoi(metadata) ? 1 : 0);
621    layers.push(allCountyLines);
622
623    // For the Screenshot widget we need to be able to see tribal lines for the country.
624    // This creates a group for them since they require a custom style built in screenshot.js.
625    tribalLines.setOpacity(0);
626    layers.push(tribalLines);
627
628    if (!drupalSettings.dontDrawStateOutlines || mapElem.closest(".map-screenshot-overlay")) {
629      const stateStyle = metadata.classes.indexOf("state-lines-3") !== -1 ? olStyles.statesThickLines() : null;
630      stateLines = olFixedLayers.states(metadata.projection, stateStyle);
631      layers.push(stateLines);
632    }
633    if (olClick.checkStatesClickable()) {
634      highlightableLayers.push(stateLines);
635    }
636
637    const counties = getAndHandleStateCountyLines(metadata, layers, highlightableLayers);
638
639    if (drupalSettings.drawWatershedOutlines) {
640      watershedLines = watershedLayers.lines;
641      if (drupalSettings.watershedClickable !== false) {
642        highlightableLayers.push(watershedLines);
643      }
644      layers.push(watershedLines);
645      layers.push(watershedLayers.outline);
646      if (watershedLayers.rivers) {
647        basemaps.watershed_labels_only_layer = olLayers.makeVectorTileLayer(
648          "/modules/custom/nidis_maps/vector_styles/watershed-labels-only-rivers-override.json"
649        );
650      }
651    }
652
653    // This layer is here to let the map be centered on it
654    const conus = olFixedLayers.conus(metadata.projection);
655    layers.push(conus);
656
657    const highlight = olLayers.makeVectorLayer(new ol.source.Vector(), styles.styleHighlight);
658    layers.push(highlight);
659
660    if (mapsUtil.getMapType(metadata) === "international") {
661      layers.push(olFixedLayers.international());
662    }
663
664    if (metadata.abv === "tribal" && !mapElem.closest(".map-screenshot-overlay")) {
665      tribalPage = olFixedLayers.tribal(null);
666      layers.push(tribalPage);
667    }
668
669    if (metadata.basemap.indexOf("labels_only") !== -1) {
670      labelsGroup.getLayers().push(basemaps[metadata.basemap + "_layer"]);
671    }
672    if (metadata.basemap === "vectorlabels") {
673      labelsGroup.getLayers().push(basemaps.vectorlabels_labels);
674    }
675    layers.push(labelsGroup);
676
677    let aoiLines;
678    if (aoiOverride.mapOverridesAoi(metadata)) {
679      if (aoiOverride.getAoiOverrideType(metadata) === "dews") {
680        aoiLines = olFixedLayers.dews(metadata.projection);
681        aoiOverrideGroup.getLayers().push(aoiLines);
682      }
683    }
684    if (aoiOverride.urlOverridesAoi()) {
685      aoiLines = olFixedLayers.dews(metadata.projection);
686      aoiLines.setStyle(olStyles.dewsOverrideStyle(aoiOverride.getUrlAoi()));
687      aoiOverrideGroup.getLayers().push(aoiLines);
688    }
689    if (metadata.classes.indexOf("c--map-boundary--wfo") !== -1) {
690      aoiOverrideGroup.getLayers().push(olFixedLayers.wfo(metadata.projection));
691    }
692    if (metadata.classes.indexOf("c--map-boundary--umrb") !== -1) {
693      aoiLines = olFixedLayers.umrb(metadata.projection);
694      aoiOverrideGroup.getLayers().push(aoiLines);
695    }
696    layers.push(aoiOverrideGroup);
697
698    const config = {
699      layers,
700      dataLayer,
701      highlight,
702      dataLayerGroup,
703      conus,
704      watersheds: watershedLines,
705      states: stateLines,
706      counties,
707      allCounties: allCountyLines,
708      tribal: tribalLines,
709      tribalPage,
710      highlightableLayers,
711      basemaps,
712      basemapGroup,
713      labelsGroup,
714      usdmUnder,
715      usdmOver,
716      aoiLines,
717    };
718
719    if (window.drupalSettings.drawStateMask) {
720      config.maskReceivingLayer = stateLines;
721    } else if (window.drupalSettings.drawWatershedOutlines) {
722      config.maskReceivingLayer = watershedLines;
723      config.watershedName = window.drupalSettings.watershed_name;
724      if (config.watershedName === "mississippi") {
725        config.maskReceivingLayer = watershedLayers.outline;
726      }
727      if (config.watershedName === "Colorado River Basin") {
728        config.watershedName = "Colorado";
729      }
730    }
731    if (metadata.classes.indexOf("c--map-boundary--umrb") !== -1) {
732      config.maskReceivingLayer = aoiLines;
733    }
734
735    return config;
736  }
737
738  /**
739   * Creates the collection of layers to be used in the main data layer group.
740   *
741   * We want the maps which render rivers to have them drawn below point layers but above polygonal layers.
742   * @param {Metadata} metadata - metadata for the current layer.
743   * @param {ol.Layer} dataLayer - the main layer for a map.
744   * @param {{}|undefined} watershedLayers - Result of olFixedLayers.watersheds or undefined.
745   * @return {ol.Collection} - A collection of the layers to be inserted into the dataLayerGroup group.
746   */
747  function createDataLayerCollection(metadata, dataLayer, watershedLayers) {
748    let dataLayerArray;
749    if (
750      watershedLayers &&
751      (watershedLayers.rivers || drupalSettings.rivers_override) &&
752      (olLayers.checkPointLayer(dataLayer) || metadata.classes.includes("rivers-under"))
753    ) {
754      dataLayerArray = [watershedLayers.rivers, dataLayer];
755      if (metadata.basemap.includes("labels_only")) {
756        dataLayerArray.splice(0, 0, olFixedLayers.watershedRiversVector());
757      }
758    } else if (watershedLayers && watershedLayers.rivers) {
759      dataLayerArray = [dataLayer, watershedLayers.rivers];
760      if (metadata.basemap.includes("labels_only")) {
761        dataLayerArray.splice(1, 0, olFixedLayers.watershedRiversVector());
762      }
763    } else {
764      dataLayerArray = [dataLayer];
765    }
766
767    return new ol.Collection(dataLayerArray);
768  }
769
770  /**
771   * Sets up the USDM layer group layers and opacity.
772   * @param {Metadata} metadata - Metadata for a map.
773   * @param {ol.layer.Group} group - usdmUnder or usdmOver.
774   * @param {array} layers - Array of the layers & layer groups which will be rendered on the map.
775   * @param {string} condition - "below" or "top".
776   * @param {number} opacity - Opacity of the layer group
777   */
778  function setupUSDMGroup(metadata, group, layers, condition, opacity) {
779    group.setOpacity(opacity);
780    if (metadata.usdm === condition) {
781      group.getLayers().push(olFixedLayers.usdm());
782    }
783    layers.push(group);
784  }
785
786  /**
787   * Gets the layer for an individual states counties, if a user is on a state page.
788   * @param {Metadata} metadata - Metadata for a map.
789   * @param {array} layers - Array of the layers & layer groups which will be rendered on the map.
790   * @param {array} highlightableLayers - Array of layers which can be highlighted 
790when a user mouses over them.
791   * @return {ol.layer.Vector|undefined} - Vector layer of counties, if it exists.
792   */
793  function getAndHandleStateCountyLines(metadata, layers, highlightableLayers) {
794    if (!drupalSettings.drawCountyOutlines) {
795      return undefined;
796    }
797    const counties = olFixedLayers.stateCounties(metadata.abv, metadata.projection);
798    highlightableLayers.push(counties);
799    layers.push(counties);
800    if (aoiOverride.urlOverridesAoi()) {
801      counties.setStyle(olStyles.getStyle("styleTransparent"));
802    }
803    return counties;
804  }
805
806  /**
807   * Creates an openlayers group.
808   * https://openlayers.org/en/latest/apidoc/module-ol_layer_Group-LayerGroup.html
809   * @param {undefined|array} layers - optional array of layers to be added to the layer group. Otherwise needs
810   *   to be set via group.setLayers(new ol.Collection(layerArray))
811   * @return {ol.layer.Group} - The layer Group.
812   */
813  function createOLGroup(layers) {
814    const options = {};
815    if (layers) {
816      options.layers = layers;
817    }
818    return new ol.layer.Group(options);
819  }
820
821  /**
822   * Adds a button to display legend popups on fullscreen mode.
823   * @param {ol.Map} map - The OpenLayers map object
824   * @param {HTMLDivElement} wrapper - The map's container
825   */
826  function addOLLegendButton(map, wrapper) {
827    const legendContainer = document.createElement("div");
828    legendContainer.className = "ol-legend-buttons ol-unselectable";
829    legendContainer.style.pointerEvents = "auto";
830    const expandLegend = document.createElement("button");
831
832    expandLegend.classList.add("ol-legend-button");
833    expandLegend.title = "Select Locations";
834    const expandLegendSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
835    expandLegendSVG.setAttribute("height", "24");
836    expandLegendSVG.setAttribute("width", "24");
837    const expandLegendPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
838    expandLegendPath.setAttribute(
839      "d",
840      // eslint-disable-next-line max-len
841      "M11 7h2v2h-2V7zm0 4h2v6h-2v-6zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"
842    );
843    expandLegendSVG.setAttribute("viewbox", "0 0 24 24");
844    expandLegendSVG.appendChild(expandLegendPath);
845    expandLegend.appendChild(expandLegendSVG);
846    legendContainer.appendChild(expandLegend);
847
848    wrapper.querySelector(".ol-overlaycontainer-stopevent").append(legendContainer);
849    expandLegend.addEventListener("click", handleExpandLegend.bind(map));
850  }
851
852  /**
853   * Binds legend popup events when the legend is opened in fullscreen.
854   * @param {Event} e - Original click event. Not used.
855   */
856  function handleExpandLegend(e) {
857    // This is bound to the OL map object.
858    const map = this;
859    olClick.handleLegendPopup(map);
860  }
861
862  /**
863   * Creates the base jump buttons for interactive maps.
864   * @param {ol.Map<Object>} map - OL Map object.
865   * @param {*} wrapper - Wrapper for a panel, keyed by .interactive-map-panel.
866   */
867  function addOLJumpButtons(map, wrapper) {
868    const optionsContainer = document.createElement("div");
869    optionsContainer.className = "ol-extra-buttons ol-unselectable";
870    optionsContainer.style.pointerEvents = "auto";
871    let i, l, option, button;
872
873    const options = [
874      {
875        label: "CONUS",
876        title: "Continental United States",
877        key: "conus",
878        rotation: "0",
879        abv: "us",
880      },
881      {
882        label: "Alaska",
883        key: "alaska",
884        rotation: "-0.5",
885        abv: "AK",
886      },
887      {
888        label: "Hawaiʻi",
889        key: "hawaii",
890        rotation: "-0.785",
891        abv: "HI",
892      },
893      {
894        label: "PR/USVI",
895        title: "Puerto Rico & United States Virgin Islands",
896        key: "puertorico",
897        rotation: "0.262",
898        abv: "PRVIBTN",
899      },
900    ];
901    if (map.abv === "nws") {
902      options.push({
903        label: "USAPI",
904        key: "usapi",
905        rotation: "-1.05",
906        abv: "USAPI",
907      });
908    }
909
910    const expandOptions = document.createElement("button");
911    expandOptions.classList.add("ol-jump-options");
912    expandOptions.title = "Select Locations";
913    const expandOptionsSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
914    expandOptionsSVG.setAttribute("height", "24");
915    expandOptionsSVG.setAttribute("width", "24");
916    const expandOptionsPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
917    expandOptionsPath.setAttribute(
918      "d",
919      // eslint-disable-next-line max-len
920      "m20.5 3-.16.03L15 5.1 9 3 3.36 4.9c-.21.07-.36.25-.36.48V20.5c0 .28.22.5.5.5l.16-.03L9 18.9l6 2.1 5.64-1.9c.21-.07.36-.25.36-.48V3.5c0-.28-.22-.5-.5-.5zM15 19l-6-2.11V5l6 2.11V19z"
921    );
922    expandOptionsSVG.setAttribute("viewbox", "0 0 24 24");
923    expandOptionsSVG.appendChild(expandOptionsPath);
924    expandOptions.appendChild(expandOptionsSVG);
925    optionsContainer.appendChild(expandOptions);
926
927    const closeOptions = document.createElement("button");
928    closeOptions.title = "Hide Locations";
929    closeOptions.innerHTML = "&#9660";
930    closeOptions.classList.add("ol-jump-close");
931
932    for (i = 0, l = options.length; i < l; i++) {
933      option = options[i];
934      button = document.createElement("button");
935      button.textContent = option.label;
936      button.setAttribute("data-key", option.key);
937      button.setAttribute("data-rotation", option.rotation);
938      button.setAttribute("data-abv", option.abv);
939      if (option.title) {
940        button.setAttribute("title", option.title);
941      }
942      button.classList.add("ol-jump-button");
943      optionsContainer.append(button);
944    }
945
946    optionsContainer.appendChild(closeOptions);
947
948    wrapper.querySelector(".ol-overlaycontainer-stopevent").append(optionsContainer);
949    wrapper.addEventListener("click", dispatchJumpEvent.bind(map));
950    expandOptions.addEventListener("click", expandJumpOptions);
951    closeOptions.addEventListener("click", hideJumpOptions);
952  }
953
954  /**
955   * Displays the jump buttons if hidden.
956   */
957  function expandJumpOptions() {
958    const jumpBtns = this.parentElement.querySelectorAll(".ol-jump-button");
959    const jumpClose = this.parentElement.querySelector(".ol-jump-close");
960    jumpBtns.forEach((btn) => {
961      btn.style.display = "inline";
962    });
963    jumpClose.style.display = "inline";
964    this.style.display = "none";
965  }
966
967  /**
968   * Hides the jump buttons.
969   */
970  function hideJumpOptions() {
971    const jumpBtns = this.parentElement.querySelectorAll(".ol-jump-button");
972    const jumpOptions = this.parentElement.querySelector(".ol-jump-options");
973    jumpBtns.forEach((btn) => {
974      btn.style.display = "";
975    });
976    jumpOptions.style.display = "";
977    this.style.display = "none";
978  }
979
980  /**
981   * Handles a user clicking on a state jump button.
982   *
983   * The `this` keyword is bound to the customized OL map object in _setupInitialZoomListeners.
984   * @param {MouseEvent} e - A click on the custom OL buttons.
985   */
986  function dispatchJumpEvent(e) {
987    const button = e.target;
988    if (!button.classList.contains("ol-jump-button")) {
989      return;
990    }
991    const map = this;
992
993    OLRotateMap(map, map.metadata.projection !== "epsg3857" ? parseFloat(button.getAttribute("data-rotation")) : 0);
994    map.abv = button.getAttribute("data-abv");
995    map.metadata.abv = button.getAttribute("data-abv");
996    recenterOLMap(map);
997  }
998
999  /**
1000   * Rotates an openlayers map.
1001   * @param {ol.Map<Object>} map - OL Map object.
1002   * @param {number} rotation - radians of rotation.
1003   */
1004  function OLRotateMap(map, rotation) {
1005    map.getView().setRotation(rotation);
1006  }
1007
1008  /**
1009   *
1010   */
1011  function resizeOLMap() {
1012    const map = this;
1013    const requestAnimationFrame =
1014      window.requestAnimationFrame ||
1015      window.mozRequestAnimationFrame ||
1016      window.webkitRequestAnimationFrame ||
1017      window.msRequestAnimationFrame;
1018    const cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame;
1019    if (map.animId) {
1020      cancelAnimationFrame(map.animId);
1021      map.animId = undefined;
1022    }
1023    if (!map.testHeight || map.testHeight !== getNewMapHeight(mapsUtil.findMap(map.getOverlayContainer()))) {
1024      map.animId = requestAnimationFrame(updateMapSize.bind(map));
1025    }
1026  }
1027
1028  /**
1029   *
1030   * @param {*} map - OL Map object.
1031   */
1032  function recenterOLMap(map) {
1033    const precomputedFitOccurred = _tryPrecomputedFit(map);
1034    if (
1035      precomputedFitOccurred &&
1036      (aoiOverride.mapOverridesAoi(map.metadata) === false ||
1037        map.metadata.classes.indexOf("c--map-boundary--umrb") !== -1)
1038    ) {
1039      return;
1040    }
1041    const feature = _getMapMainFeature(map);
1042    if (feature) {
1043      fitMap(map, feature);
1044    }
1045  }
1046
1047  /**
1048   * Determines the main layer for a map, used for masking / fitting / cropping.
1049   *
1050   * With cropping there are instances where we want an event to fire only when
1051   * the features have loaded. This function allows us to determine the layer and
1052   * check for features, which we can then pull via the _getMapMainFeature
1053   * function.
1054   * @param {ol.Map<Object>} map - OL Map object.
1055   * @return {ol.layer.Vector<Object>|undefined} - Primary layer for the map.
1056   */
1057  function _getMapMainLayer(map) {
1058    let layer;
1059    if (map.watershedName) {
1060      layer = map.watersheds;
1061    } else if (map.abv) {
1062      if (aoiOverride.mapOverridesAoi(map.metadata)) {
1063        layer = map.aoiLines;
1064      } else if (map.abv === "us" || map.abv === "tribal" || map.abv === "nws") {
1065        layer = map.conus;
1066      } else if (map.abv !== "international") {
1067        layer = map.states;
1068      }
1069    }
1070    if (map.metadata.classes.indexOf("c--map-boundary--umrb") !== -1) {
1071      layer = map.aoiLines;
1072    }
1073    return layer;
1074  }
1075
1076  /**
1077   * Pulls out the main feature for a map, used for fitting and masking.
1078   * @param {ol.Map<Object>} map - OL Map object.
1079   * @return {ol.Feature<Object>|undefined} - Primary feature of the main vector layer.
1080   */
1081  function _getMapMainFeature(map) {
1082    let feature;
1083    if (aoiOverride.urlOverridesAoi()) {
1084      feature = aoiOverride.getAoiFeature(map);
1085    } else if (map.watershedName) {
1086      if (map.abv === "AK") {
1087        feature = getLayerFeature(map.watersheds, "Name", "Alaska Region");
1088      } else if (map.abv === "HI") {
1089        feature = getLayerFeature(map.watersheds, "Name", "Hawaii Region");
1090      } else if (map.abv === "PR") {
1091        feature = getLayerFeature(map.watersheds, "Name", "Caribbean Region");
1092      } else if (map.watershedName === "mississippi") {
1093        feature = olFixedLayers
1094          .watersheds(drupalSettings.watershed_name, map.metadata.projection)
1095          .outline.getSource()
1096          .getFeatures()[0];
1097      } else {
1098        feature = getLayerFeature(map.watersheds, "Name", map.watershedName);
1099      }
1100    } else if (map.abv) {
1101      if (aoiOverride.mapOverridesAoi(map.metadata)) {
1102        feature = aoiOverride.getAoiFeature(map);
1103      } else if (map.abv === "us" || map.abv === "tribal" || map.abv === "nws") {
1104        feature = map.conus.getSource().getFeatures()[0];
1105      } else if (map.abv !== "international") {
1106        feature = getLayerFeature(map.states, "STATE", map.abv);
1107      }
1108    }
1109    if (map.metadata.classes.indexOf("c--map-boundary--umrb") !== -1) {
1110      feature = map.aoiLines.getSource().getFeatures()[0];
1111    }
1112    return feature;
1113  }
1114
1115  /**
1116   * Fixes the sizes of openlayers maps with text next to them.
1117   */
1118  function updateMapSize() {
1119    const map = this;
1120    const mapElement = mapsUtil.findMap(map.mapElem);
1121    if (mapElement.closest(".map-screenshot-overlay")) {
1122      return;
1122 // Do not modify heights of the screenshot maps.
1123    }
1124    map.testHeight = getNewMapHeight(mapElement);
1125    mapElement.style.height = map.testHeight;
1126    map.updateSize();
1127    const cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame;
1128    cancelAnimationFrame(map.animId);
1129  }
1130
1131  /**
1132   * Determines the height of the map.
1133   * @param {Element} elem - Main map element. map.mapElem.
1134   * @return {string} - Pixel height for map. Formatted as [NUMBER]px
1135   */
1136  function getNewMapHeight(elem) {
1137    const windowWidth = $(window).width();
1138    const tabWrapper = mapsUtil.findWrapper(elem);
1139    let height;
1140    // have to check for this elements existence due to differences in the screenshot and primary
1141    // data panel's existence.
1142    if (!tabWrapper) {
1143      return "300px";
1144    }
1145    const minimumMapHeight = $(elem).data("map").metadata["minimum-map-height"];
1146    if (windowWidth >= 64 * parseFloat(getComputedStyle(document.querySelector("body")).fontSize) && minimumMapHeight) {
1147      height = minimumMapHeight.toString() + "px";
1148    } else {
1149      height = "300px";
1150    }
1151    return height;
1152  }
1153
1154  /**
1155   *
1156   * @param {ol.Map<Object>} map - OL Map object.
1157   */
1158  function OLApplyMask(map) {
1159    if (drupalSettings.forceRemoveMask) {
1160      if (map.mask) {
1161        map.maskReceivingLayer.removeFilter(map.mask);
1162        map.mask = null;
1163      }
1164      return;
1165    }
1166    if (aoiOverride.urlOverridesAoi()) {
1167      return;
1168    }
1169    const feature = _getMapMainFeature(map);
1170    if (map.mask) {
1171      if (map.mask.get("feature") === feature) {
1172        // Do not remove and re-draw the mask if the feature you're masking is the same
1173        return;
1174      }
1175      map.maskReceivingLayer.removeFilter(map.mask);
1176    }
1177    if (
1178      drupalSettings.drawStateMask ||
1179      drupalSettings.drawWatershedMask ||
1180      map.metadata.classes.indexOf("c--map-boundary--umrb") !== -1
1181    ) {
1182      const mask = new ol.filter.Mask({
1183        feature,
1184        inner: false,
1185        fill: new ol.style.Fill({ color: [0, 0, 0, 0.5] }),
1186      });
1187      map.maskReceivingLayer.addFilter(mask);
1188      map.mask = mask;
1189    }
1190  }
1191
1192  /**
1193   * Applies data filter to vector layers.
1194   *
1195   * TODO: Figure out how to make this work for tile layers too.
1196   * @param {ol.Map<Object>} map - OL Map object.
1197   * @param {Metadata} metadata - Metadata for the current map.
1198   */
1199  function OLApplyFilter(map, metadata) {
1200    if (
1201      metadata &&
1202      (metadata.layer.indexOf("json") === -1 || metadata.classes.toLowerCase().indexOf("filter-data") === -1)
1203    ) {
1204      return;
1205    }
1206    const filterLayer = _getMapMainLayer(map);
1207    if (filterLayer.getSource().getFeatures().length === 0) {
1208      filterLayer.on("addfeature", function () {
1209        _OLApplyFilterVector(map);
1210      });
1211    } else {
1212      _OLApplyFilterVector(map);
1213    }
1214  }
1215
1216  /**
1217   * Actually creates the crop object and applies it to the layer.
1218   * @param {ol.Map<Object>} map - OL Map object.
1219   */
1220  function _OLApplyFilterVector(map) {
1221    const crop = new ol.filter.Crop({
1222      feature: _getMapMainFeature(map),
1223      wrapX: true,
1224      inner: false,
1225      active: true,
1226    });
1227    map.dataLayer.addFilter(crop);
1228  }
1229
1230  /**
1231   * Builds the initial overlay elements for use as a map popup.
1232   * @param {ol.Map<Object>} map - The OL Map object
1233   * @param {Element} mapElem - The map placeholder
1234   */
1235  function createInitialOLPopup(map, mapElem) {
1236    map.popupContainer = document.createElement("div");
1237    map.popupContainer.className = "ol-popup";
1238
1239    const content = document.createElement("div");
1240    content.className = "popup-content";
1241
1242    const closer = document.createElement("a");
1243    closer.className = "ol-popup-closer";
1244    closer.href = "#";
1245    closer.innerHTML = '<span class="usa-sr-only">Close</span>';
1246    closer.title = "Close";
1247
1248    map.popupContainer.appendChild(closer);
1249    map.popupContainer.appendChild(content);
1250    mapElem.appendChild(map.popupContainer);
1251    const autoPan = !((window.hasOwnProperty("checkMobile") && window.checkMobile()) || window.innerWidth <= 600);
1252
1253    map.popupOverlay = new ol.Overlay({
1254      element: map.popupContainer,
1255      autoPan,
1256      autoPanAnimation: {
1257        duration: 250,
1258      },
1259    });
1260
1261    closer.onclick = function (e) {
1262      e.preventDefault();
1263      map.popupOverlay.setPosition(undefined);
1264    };
1265
1266    if (map.fullScreen) {
1267      map.fullScreen.addEventListener("leavefullscreen", () => {
1268        map.popupOverlay.setPosition(undefined);
1269      });
1270    }
1271
1272    map.addOverlay(map.popupOverlay);
1273  }
1274
1275  /**
1276   * Creates an OpenLayers Layer for the primary data of the map.
1277   *
1278   * @param {Element} mapElem - Wrapper element for the map.
1279   * @param {Metadata} metadata - metadata for the map.
1280   * @return {ol.layer.Vector|ol.layer.Tile} - openlayers layer for the maps main data layer.
1281   */
1282  function makeOLDataLayer(mapElem, metadata) {
1283    overlay.clearOverlaySpinner(mapElem);
1284    overlay.addSpinnerOverlay(mapElem, metadata);
1285    const url = metadata.layer;
1286
1287    let layer;
1288    const suffix = mapsUtil.getExtension(url);
1289    if (suffix === "json" || suffix === "geojson" || suffix === "kml") {
1290      layer = olLayers.makeDataLayerVector(url, mapElem, metadata);
1291      _checkSourceNoData(layer.getSource(), mapElem, metadata);
1292    } else {
1293      layer = olLayers.makeTileLayer(url, "");
1294
1295      /**
1296       * Custom loader function: If a retryCode is returned from the fetch, retry once.
1297       * Mark failed tiles with the .empty property and manually set the state to done and src to empty.
1298       *
1299       * @param {object} tile - The tile to load
1300       * @param {string} src - The URL for the tile
1301       */
1302      const retryCodes = [408, 429, 500, 502, 503, 504];
1303      const retries = {};
1304      layer.getSource().setTileLoadFunction(function (tile, src) {
1305        const image = tile.getImage();
1306        fetch(src)
1307          .then((response) => {
1308            if (retryCodes.includes(response.status)) {
1309              retries[src] = (retries[src] || 0) + 1;
1310              if (retries[src] <= 2) {
1311                setTimeout(() => tile.load(), retries[src] * 500);
1312              }
1313              return Promise.reject(response.status);
1314            }
1315            if (response.status === 404) {
1316              return Promise.reject();
1317            }
1318            return response.blob();
1319          })
1320          .then((blob) => {
1321            const imageUrl = URL.createObjectURL(blob);
1322            image.src = imageUrl;
1323            setTimeout(() => URL.revokeObjectURL(imageUrl), 5000);
1324          })
1325          .catch((error) => {
1326            if (error !== 408) {
1327              // option to replace with a fallback image here;
1328              image.src = "";
1329              tile.empty = true;
1330              tile.setState(2);
1331            }
1332          });
1333      });
1334
1335      // uses Sets to prevent duplicates and simplify removal
1336      layer.emptyTileQueue = new Set();
1337      layer.fullTileQueue = new Set();
1338      layer.getSource().on("tileloadend", function (e) {
1339        if (!e.tile.empty) {
1340          layer.fullTileQueue.add(e.tile.ol_uid);
1341        } else {
1342          layer.emptyTileQueue.add(e.tile.ol_uid);
1343        }
1344        overlay.clearOverlaySpinnerLoaded(mapElem, metadata);
1345      });
1346    }
1347    if (!mapElem.closest(".map-screenshot-overlay")) {
1348      mapsUtil.updateLastUpdated(metadata);
1349    }
1350    return layer;
1351  }
1352
1353  /**
1354   * Handles the over/underlay drawing on all maps.
1355   *
1356   * snotel key is there for the UMRB project.
1357   * @param {DMap} map - Individual map.
1358   * @param {Metadata} metadata - Metadata for map.
1359   */
1360  function handleUnderlay(map, metadata) {
1361    map.usdmUnder.getLayers().clear();
1362    map.usdmOver.getLayers().clear();
1363    if (!metadata.usdm || typeof metadata.usdm !== "string") {
1364      return;
1365    }
1366    if (metadata.usdm === "below") {
1367      map.usdmUnder.getLayers().push(olFixedLayers.usdm());
1368    }
1369    if (metadata.usdm === "top" || metadata.usdm.indexOf("above") !== -1) {
1370      map.usdmOver.getLayers().push(olFixedLayers.usdm());
1371    }
1372    if (metadata.usdm === "snodasswe") {
1373      map.usdmUnder.getLayers().push(olFixedLayers.snodasswe());
1374    }
1375    if (metadata.usdm === "snodassnowdepth") {
1376      map.usdmUnder.getLayers().push(olFixedLayers.snodassnowdepth());
1377    }
1378  }
1379
1380  /**
1381   * Builds a message box to attach to the map
1382   * @return {HTMLDivElement} - The fully built message box
1383   */
1384  function _buildTileErrorMessage() {
1385    const div = document.createElement("div");
1386    div.className = "tile-error-message";
1387    const p = document.createElement("p");
1388    p.textContent = "Data may not be available for this entire area.";
1389    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1390    const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1391    path.setAttribute(
1392      "d",
1393      "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
1394    );
1395    const button = document.createElement("button");
1396    svg.appendChild(path);
1397    button.appendChild(svg);
1398    div.append(p, button);
1399
1400    button.addEventListener("click", () => {
1401      removeTileErrorMessage(div);
1402    });
1403
1404    return div;
1405  }
1406
1407  /**
1408   * Removes the tile error message div
1409   * @param {HTMLDivElement} el - Optional error message div if readily available
1410   */
1411  function removeTileErrorMessage(el = false) {
1412    const div = el || document.querySelector(".map-screenshot-map-conu
1412s .tile-error-message");
1413    if (div) {
1414      div.remove();
1415    }
1416  }
1417
1418  /**
1419   * Get a count of the max number of map tiles used in the current view
1420   * @param {object} map - The OL map object
1421   * @param {object} tileGrid - The OL tile grid object
1422   * @return {number} - The number of tiles in the current view
1423   */
1424  function _getTileCount(map, tileGrid) {
1425    const view = map.getView();
1426    const extent = view.calculateExtent(map.getSize());
1427    const zoom = Math.ceil(view.getZoom());
1428    let count = 0;
1429    tileGrid.forEachTileCoord(extent, zoom, () => {
1430      count += 1;
1431    });
1432    return count;
1433  }
1434
1435  /**
1436   * Assesses the data layer for missing tiles and determines if the error message should be presented.
1437   * @param {object} map - The OL map object
1438   */
1439  function checkTileErrors(map) {
1440    const layer = map.dataLayer;
1441    const source = layer.getSource();
1442    if (source instanceof ol.source.XYZ) {
1443      const container = map.getTarget().parentElement;
1444      const count = _getTileCount(map, source.getTileGrid());
1445      if (layer.emptyTileQueue.size > count / 2 + layer.fullTileQueue.size) {
1446        // out of bounds
1447        if (
1448          container.classList.contains("map-screenshot-map-conus") &&
1449          container.querySelector(".tile-error-message") === null
1450        ) {
1451          container.appendChild(_buildTileErrorMessage());
1452        }
1453      }
1454      layer.emptyTileQueue.clear();
1455      layer.fullTileQueue.clear();
1456    }
1457  }
1458
1459  /**
1460   * Checks the vector source to see if no data exists and an error message must be displayed.
1461   * @param {ol.source.Vector} source - Vector Data Layer.
1462   * @param {Element} mapElem - wrapper for the map.
1463   * @param {Metadata} metadata - Metadata for the current map.
1464   * @private
1465   */
1466  function _checkSourceNoData(source, mapElem, metadata) {
1467    if (source.get("showNoDataOverlay") === true) {
1468      overlay.addFileNoDataOverlay(mapElem, metadata);
1469    } else if (source.get("showNoDataOverlay") === undefined) {
1470      source.once("featuresloadend", function () {
1471        overlay.handleOLSourceNoData(source, mapElem, metadata);
1472      });
1473    }
1474  }
1475
1476  /**
1477   * Finds a feature from an openlayers Layer
1478   * @param {ol.layer.Vector<Object>} layer - A vector layer.
1479   * @param {string} key - Feature key, what is checked.
1480   * @param {string} val - Feature value, what is checked against.
1481   * @return {ol.Feature<Object>|undefined} - Feature with the key/value pair.
1482   */
1483  function getLayerFeature(layer, key, val) {
1484    const layerFeatures = layer.getSource().getFeatures();
1485    for (let i = 0; i < layerFeatures.length; i++) {
1486      if (layerFeatures[i].getProperties()[key] === val) {
1487        return layerFeatures[i];
1488      }
1489    }
1490  }
1491
1492  /**
1493   * Fits a map to an extent.
1494   *
1495   * Originally all maps dynamically determined the fit of maps. However for performance reasons and to facilitate the
1496   * SNOTEL map only being in the western U.S. & Alaska pre-fit extents were generated.
1497   * @param {Object} map - OpenLayers map object.
1498   * @param {[number, number, number, number]} extent - Extent to fit the map to.
1499   * @param {number|null} scale - See padExtent scale. Null if default behavior is desired.
1500   */
1501  function fitMapToExtent(map, extent, scale) {
1502    if (typeof scale === "number") {
1503      map.getView().fit(padExtent(extent, scale), {
1504        size: map.getSize(),
1505      });
1506    } else {
1507      map.getView().fit(padExtent(extent), {
1508        size: map.getSize(),
1509      });
1510    }
1511  }
1512
1513  /**
1514   * Positions a map to the extent of a feature.
1515   *
1516   * @param {Object} map
1517   *   OpenLayers map object.
1518   * @param {Object} feature
1519   *   OpenLayers feature to find the relevant extent in.
1520   */
1521  function fitMap(map, feature) {
1522    let extent = feature.getGeometry().getExtent();
1523    const properties = feature.getProperties();
1524    if (properties.Name && map.watershedName && properties.Name === "Alaska Region") {
1525      // Alaska Watershed Region doesn't play nice with openlayers so we have to set it manually.
1526      extent = [-21006118, 6604159, -14076031, 11652672];
1527    }
1528    if (window.drupalSettings.drawCountyMarker || window.drupalSettings.drawLocationMarker) {
1529      extent = getLayerFeature(map.counties, "FIPS", window.drupalSettings.fipsCode).getGeometry().getExtent();
1530      // let extraExtent = getLayerFeature(map.states, "STATE", map.abv).getGeometry().getExtent();
1531      fitMapToExtent(map, extent, 0.5);
1532    } else if (properties.STATE === "USAPI" && map.metadata.projection === "epsg5070") {
1533      // USAPI extent need to be shrunk to hide distortions of surrounding areas
1534      fitMapToExtent(map, extent, -0.16);
1535    } else {
1536      fitMapToExtent(map, extent, null);
1537    }
1538  }
1539
1540  /**
1541   * If the map can be fit to one of the precomputed maps it is. Returns status code for recenterOLMap.
1542   * @param {{}} map - The OpenLayers map.
1543   * @return {boolean} - true if the map was fit, false if not.
1544   * @private
1545   */
1546  function _tryPrecomputedFit(map) {
1547    const metadata = map.metadata;
1548    let mapType = mapsUtil.getMapType(metadata);
1549    if (map.mapElem && map.mapElem.closest(".map-screenshot-map-alaska")) {
1550      // The Alaska cutout for SWE maps was being fit to the western US since the conus view needs to be set there. Due
1551      // to what information is stored in the metadata and what can be passed this extra check needed to be added here.
1552      // drought.gov/topics/snow-drought
1553      // eslint-disable-next-line max-len
1554      // https://storage.googleapis.com/noaa-nidis-drought-gov-data/current-conditions/test/NRCS-SNOTEL-huc6/nrcs_huc_swe.geojson
1555      mapType = "other";
1556    }
1557
1558    if (aoiOverride.urlOverridesAoi()) {
1559      mapType = aoiOverride.getUrlAoi();
1560    } else if (["conus", "SNOTEL"].indexOf(mapType) !== -1) {
1561      // no-op
1562    } else if (mapType === "international" && olExtents.getExtent(metadata.projection, mapType)) {
1563      // no-op
1564    } else if (mapType === "state" && olExtents.getExtent(metadata.projection, map.abv)) {
1565      mapType = map.abv;
1566    } else if (
1567      aoiOverride.mapOverridesAoi(metadata) &&
1568      olExtents.getExtent(metadata.projection, aoiOverride.getAoiAbv
1568(metadata))
1569    ) {
1570      mapType = aoiOverride.getAoiAbv(metadata);
1571    } else if (mapType === "watershed") {
1572      mapType = map.watershedName;
1573    }
1574    const extent = olExtents.getExtent(metadata.projection, mapType);
1575    if (extent) {
1576      fitMapToExtent(map, extent, null);
1577      const rotation = olExtents.getRotation(metadata.projection, mapType);
1578      if (rotation) {
1579        OLRotateMap(map, rotation);
1580      }
1581      return true;
1582    }
1583    return false;
1584  }
1585
1586  /**
1587   * Proportionally pads an extent based on its size
1588   * @param {[number]} extent - original extent of four numbers.
1589   * @param {number} scale - proportion of original size to scale by. should be a value with a range from (0,1]
1590   * @param {boolean} extraPadding - Add fixed extra padding to the new extent.
1591   * @param {[number]} extraExtent - An extra extent for use when calculating the extra padding.
1592   * @return {[number]} - original extent with scaled additional padding on the sides.
1593   */
1594  function padExtent(extent, scale = 0.02, extraPadding = false, extraExtent = undefined) {
1595    let longDiff = Math.abs(Math.abs(extent[0]) - Math.abs(extent[2])) * scale,
1596      latDiff = Math.abs(Math.abs(extent[1]) - Math.abs(extent[3])) * scale;
1597    if (extraPadding && extraExtent) {
1598      const extraExtentLongDiff = Math.abs(Math.abs(extraExtent[0]) - Math.abs(extraExtent[2]));
1599      if (extraExtentLongDiff > 200000) {
1600        longDiff += 200000;
1601        latDiff += 200000;
1602      }
1603    }
1604    return [extent[0] - longDiff, extent[1] - latDiff, extent[2] + longDiff, extent[3] + latDiff];
1605  }
1606
1607  /**
1608   * Exports functions to the global scope.
1609   */
1610  function _exportConfig() {
1611    window.olMaps = {
1612      addVectorTileLayers,
1613      drawInitialOpenLayers,
1614      filterOLJumpButtons,
1615      drawOLDataLayer,
1616      handleUnderlay,
1617      OLApplyFilter,
1618      OLRotateMap,
1619      fitMapToExtent,
1620      checkTileErrors,
1621      removeTileErrorMessage,
1622      removeVectorTileLayers,
1623      recenterOLMap,
1624      fitMap,
1625      getOpenLayersViewOptions,
1626    };
1627  }
1628})(
1629  window.jQuery,
1630  window.drupalSettings,
1631  window.mapsUtil,
1632  window.overlay,
1633  window.ol,
1634  window.olClick,
1635  window.olCounties,
1636  window.olExtents,
1637  window.olFixedLayers,
1638  window.olLayers,
1639  window.olStyles,
1640  window.tableReader,
1641  window.aoiOverride
1642);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.