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 = "▼"; 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.