diff --git a/.github/workflows/secret-check.yml b/.github/workflows/secret-check.yml index 858522b996..8c6e449901 100644 --- a/.github/workflows/secret-check.yml +++ b/.github/workflows/secret-check.yml @@ -12,9 +12,9 @@ jobs: ref: ${{ github.ref_name }} fetch-depth: 0 - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@main + uses: trufflesecurity/trufflehog@v3.23.0 with: path: ./ base: main head: ${{ github.ref_name }} - extra_args: --debug --only-verified \ No newline at end of file + extra_args: --debug --only-verified diff --git a/config/default/common/config/metadata/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.md b/config/default/common/config/metadata/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.md new file mode 100644 index 0000000000..d78ee7cb75 --- /dev/null +++ b/config/default/common/config/metadata/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.md @@ -0,0 +1,27 @@ +Granule Corrected Reflectance imagery displays each 6-minute granule of imagery in near real-time. It is available on a 15 day rolling window basis. Each granule shows imagery as it was captured by the VIIRS instrument in that 6-minute time period and reveals areas that may have otherwise been obscured by the mosaicking of subsequent overpasses. + +For a daily overview, please refer to the associated Corrected Reflectance Mosaic/Composite image. + +*** + +False Color: Red = M11, Green = I2, Blue = I1 + +This combination is most useful for distinguishing burn scars from naturally low vegetation or bare soil and enhancing floods. + +This combination can also be used to distinguish snow and ice from clouds. Snow and ice are very reflective in the visible part of the spectrum (Band I1), and absorbent in Bands I2 (near infrared) and M11 (short-wave infrared, or SWIR). Thick ice and snow appear vivid sky blue, while small ice crystals in high-level clouds will also appear blueish, and water clouds will appear white. + +The Visible Infrared Imaging Radiometer Suite (VIIRS) Corrected Reflectance imagery is available only as near real-time imagery. The VIIRS instrument is aboard the joint NASA/NOAA NOAA-20 (JPSS-1) satellite. The imagery can be visualized in Worldview and the Global Imagery Browse Services (GIBS).The sensor resolution is 750 m and 375 m (M Bands are 750 m, I Bands are 375 m), imagery resolution is 250 m, and the temporal resolution is daily. + +#### Vegetation and bare ground +Vegetation is very reflective in the near infrared (Band I2), and absorbent in Band I1 and Band M11. Assigning that band to green means even the smallest hint of vegetation will appear bright green in the image. Naturally bare soil, like a desert, is reflective in all bands used in this image, but more so in the SWIR (Band M11, red) and so soils will often have a pinkish tinge. + +#### Burned areas +Burned areas or fire-affected areas are characterized by deposits of charcoal and ash, removal of vegetation and/or the alteration of vegetation structure. When bare soil becomes exposed, the brightness in Band I1 may increase, but that may be offset by the presence of black carbon residue; the near infrared (Band I2) will become darker, and Band M11 becomes more reflective. When assigned to red in the image, Band M11 will show burn scars as deep or bright red, depending on the type of vegetation burned, the amount of residue, or the completeness of the burn. + +#### Water +Liquid water on the ground appears very dark since it absorbs in the red and the SWIR. Sediments in water appear dark blue. Ice and snow appear as bright turquoise. Clouds comprised of small water droplets scatter light equally in both the visible and the SWIR and will appear white. These clouds are usually lower to the ground and warmer. High and cold clouds are comprised of ice crystals and will appear turquoise. + +Note: The Corrected Reflectance and the Thermal Band I5 imagery from NOAA-20/VIIRS will occasionally show a checkered pattern, especially over the respective polar areas. This is due to overlapping and superimposition of observations from multiple orbits with widely different cloud/snow coverages. The checkered pattern may also arise from the mixture of partial day and night observations. Though all necessary steps have been taken to mitigate this effect, users may still notice this to some extent over the polar areas, depending on the season. + +References: VJ103MOD_NRT [doi:10.5067/VIIRS/VJ103MOD_NRT.021](https://doi.org/10.5067/VIIRS/VJ103MOD_NRT.021); VJ103IMG_NRT [doi:10.5067/VIIRS/VJ103IMG_NRT.021](https://doi.org/10.5067/VIIRS/VJ103IMG_NRT.021); +VJ102MOD_NRT [doi:10.5067/VIIRS/VJ102MOD_NRT.021](https://doi.org/10.5067/VIIRS/VJ102MOD_NRT.021); VJ102IMG_NRT [doi:10.5067/VIIRS/VJ102IMG_NRT.021](https://doi.org/10.5067/VIIRS/VJ102IMG_NRT.021) diff --git a/config/default/common/config/metadata/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.md b/config/default/common/config/metadata/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.md new file mode 100644 index 0000000000..dd531d1b0c --- /dev/null +++ b/config/default/common/config/metadata/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.md @@ -0,0 +1,25 @@ +Granule Corrected Reflectance imagery displays each 6-minute granule of imagery in near real-time. It is available on a 15 day rolling window basis. Each granule shows imagery as it was captured by the VIIRS instrument in that 6-minute time period and reveals areas that may have otherwise been obscured by the mosaicking of subsequent overpasses. + +For a daily overview, please refer to the associated Corrected Reflectance Mosaic/Composite image. + +*** + +False Color: Red = M3, Green = I3, Blue = M11 + +This combination is used to map snow and ice. Snow and ice are very reflective in the visible part of the spectrum (Band M3), and very absorbent in Bands I3 and M11 (short-wave infrared, or SWIR). This band combination is good for distinguishing liquid water from frozen water, for example, clouds over snow, ice cloud versus water cloud; or floods from dense vegetation. + +The Visible Infrared Imaging Radiometer Suite (VIIRS) Corrected Reflectance imagery is available only as near real-time imagery. The VIIRS instrument is aboard the joint NASA/NOAA NOAA-20 (JPSS-1) satellite. The imagery can be visualized in Worldview and the Global Imagery Browse Services (GIBS). The sensor resolution is 750 m and 375 m (M Bands are 750 m, I Bands are 375 m), imagery resolution is 250 m, and the temporal resolution is daily. + +#### Snow and Ice +Since the only visible light used in these images (Band M3) is assigned to red, snow and ice appear bright red. The more ice, the stronger the absorption in the SWIR bands, and the more red the color. Thick ice and snow appear vivid red (or dark pink), while small ice crystals in high-level clouds will appear pinkish. + +#### Vegetation +Vegetation will appear green in this band combination, as vegetation is absorbent in Bands M3 and M11, but reflective in Band I3. Bare soil and deserts will appear bright cyan in the image since it much more reflective in Band I3 and M11 than Band M3. + +#### Water +Liquid water on the ground will appear very dark since it absorbs in the red and the SWIR, but small liquid water drops in clouds scatter light equally in both the visible and the SWIR, and will therefore appear white. Sediments in water appear dark red. + +Note: The Corrected Reflectance and the Thermal Band I5 imagery from NOAA-20/VIIRS will occasionally show a checkered pattern, especially over the respective polar areas. This is due to overlapping and superimposition of observations from multiple orbits with widely different cloud/snow coverages. The checkered pattern may also arise from the mixture of partial day and night observations. Though all necessary steps have been taken to mitigate this effect, users may still notice this to some extent over the polar areas, depending on the season. + +References: VJ103MOD_NRT [doi:10.5067/VIIRS/VJ103MOD_NRT.021](https://doi.org/10.5067/VIIRS/VJ103MOD_NRT.021); VJ103IMG_NRT [doi:10.5067/VIIRS/VJ103IMG_NRT.021](https://doi.org/10.5067/VIIRS/VJ103IMG_NRT.021); +VJ102MOD_NRT [doi:10.5067/VIIRS/VJ102MOD_NRT.021](https://doi.org/10.5067/VIIRS/VJ102MOD_NRT.021); VJ102IMG_NRT [doi:10.5067/VIIRS/VJ102IMG_NRT.021](https://doi.org/10.5067/VIIRS/VJ102IMG_NRT.021) diff --git a/config/default/common/config/metadata/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.md b/config/default/common/config/metadata/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.md new file mode 100644 index 0000000000..0439d22839 --- /dev/null +++ b/config/default/common/config/metadata/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.md @@ -0,0 +1,16 @@ +Granule Corrected Reflectance imagery displays each 6-minute granule of imagery in near real-time. It is available on a 15 day rolling window basis. Each granule shows imagery as it was captured by the VIIRS instrument in that 6-minute time period and reveals areas that may have otherwise been obscured by the mosaicking of subsequent overpasses. + +For a daily overview, please refer to the associated Corrected Reflectance Mosaic/Composite image. + +*** + +True Color: Red = Band I1, Green = Band M4, Blue = Band M3 + +These images are called true-color or natural color because this combination of wavelengths is similar to what the human eye would see. The images are natural-looking images of land surface, oceanic and atmospheric features. + +The Visible Infrared Imaging Radiometer Suite (VIIRS) Corrected Reflectance imagery is available only as near real-time imagery. The VIIRS instrument is aboard the joint NASA/NOAA NOAA-20 (JPSS-1) satellite. The imagery can be visualized in Worldview and the Global Imagery Browse Services (GIBS). The sensor resolution is 750 m and 375 m (M Bands are 750 m, I Bands are 375 m), imagery resolution is 250 m, and the temporal resolution is daily. + +Note: The Corrected Reflectance and the Thermal Band I5 imagery from NOAA-20/VIIRS will occasionally show a checkered pattern, especially over the respective polar areas. This is due to overlapping and superimposition of observations from multiple orbits with widely different cloud/snow coverages. The checkered pattern may also arise from the mixture of partial day and night observations. Though all necessary steps have been taken to mitigate this effect, users may still notice this to some extent over the polar areas, depending on the season. + +References: VJ103MOD_NRT [doi:10.5067/VIIRS/VJ103MOD_NRT.021](https://doi.org/10.5067/VIIRS/VJ103MOD_NRT.021); VJ103IMG_NRT [doi:10.5067/VIIRS/VJ103IMG_NRT.021](https://doi.org/10.5067/VIIRS/VJ103IMG_NRT.021); +VJ102MOD_NRT [doi:10.5067/VIIRS/VJ102MOD_NRT.021](https://doi.org/10.5067/VIIRS/VJ102MOD_NRT.021); VJ102IMG_NRT [doi:10.5067/VIIRS/VJ102IMG_NRT.021](https://doi.org/10.5067/VIIRS/VJ102IMG_NRT.021) \ No newline at end of file diff --git a/config/default/common/config/metadata/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.md b/config/default/common/config/metadata/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.md new file mode 100644 index 0000000000..cb5962ea43 --- /dev/null +++ b/config/default/common/config/metadata/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.md @@ -0,0 +1,26 @@ +Granule Corrected Reflectance imagery displays each 6-minute granule of imagery in near real-time. It is available on a 15 day rolling window basis. Each granule shows imagery as it was captured by the VIIRS instrument in that 6-minute time period and reveals areas that may have otherwise been obscured by the mosaicking of subsequent overpasses. + +For a daily overview, please refer to the associated Corrected Reflectance Mosaic/Composite image. + +*** + +False Color: Red = M11, Green = I2, Blue = I1 + +This combination is most useful for distinguishing burn scars from naturally low vegetation or bare soil and enhancing floods. + +This combination can also be used to distinguish snow and ice from clouds. Snow and ice are very reflective in the visible part of the spectrum (Band I1), and absorbent in Bands I2 (near infrared) and M11 (short-wave infrared, or SWIR). Thick ice and snow appear vivid sky blue, while small ice crystals in high-level clouds will also appear blueish, and water clouds will appear white. + +The Visible Infrared Imaging Radiometer Suite (VIIRS) Corrected Reflectance imagery is available only as near real-time imagery. The VIIRS instrument in on board the joint NASA/NOAA Suomi National Polar orbiting Partnership (Suomi NPP) satellite. The imagery can be visualized in Worldview and the Global Imagery Browse Services (GIBS).The sensor resolution is 750 m and 375 m (M Bands are 750 m, I Bands are 375 m), imagery resolution is 250 m, and the temporal resolution is daily. + +#### Vegetation and bare ground +Vegetation is very reflective in the near infrared (Band I2), and absorbent in Band I1 and Band M11. Assigning that band to green means even the smallest hint of vegetation will appear bright green in the image. Naturally bare soil, like a desert, is reflective in all bands used in this image, but more so in the SWIR (Band M11, red) and so soils will often have a pinkish tinge. + +#### Burned areas +Burned areas or fire-affected areas are characterized by deposits of charcoal and ash, removal of vegetation and/or the alteration of vegetation structure. When bare soil becomes exposed, the brightness in Band I1 may increase, but that may be offset by the presence of black carbon residue; the near infrared (Band I2) will become darker, and Band M11 becomes more reflective. When assigned to red in the image, Band M11 will show burn scars as deep or bright red, depending on the type of vegetation burned, the amount of residue, or the completeness of the burn. + +#### Water +Liquid water on the ground appears very dark since it absorbs in the red and the SWIR. Sediments in water appear dark blue. Ice and snow appear as bright turquoise. +Clouds comprised of small water droplets scatter light equally in both the visible and the SWIR and will appear white. These clouds are usually lower to the ground and warmer. High and cold clouds are comprised of ice crystals and will appear turquoise. + +References: VNP03MOD_NRT [doi:10.5067/VIIRS/VNP03MOD_NRT.002](https://doi.org/10.5067/VIIRS/VNP03MOD_NRT.002); VNP03IMG_NRT [doi:10.5067/VIIRS/VNP03IMG_NRT.002](https://doi.org/10.5067/VIIRS/VNP03IMG_NRT.002); VNP02MOD_NRT [doi:10.5067/VIIRS/VNP02MOD_NRT.002](https://doi.org/10.5067/VIIRS/VNP02MOD_NRT.002); VNP02IMG_NRT [doi:10.5067/VIIRS/VNP02IMG_NRT.002](https://doi.org/10.5067/VIIRS/VNP02IMG_NRT.002) + diff --git a/config/default/common/config/metadata/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.md b/config/default/common/config/metadata/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.md new file mode 100644 index 0000000000..8c6efb76fe --- /dev/null +++ b/config/default/common/config/metadata/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.md @@ -0,0 +1,23 @@ + +Granule Corrected Reflectance imagery displays each 6-minute granule of imagery in near real-time. It is available on a 15 day rolling window basis. Each granule shows imagery as it was captured by the VIIRS instrument in that 6-minute time period and reveals areas that may have otherwise been obscured by the mosaicking of subsequent overpasses. + +For a daily overview, please refer to the associated Corrected Reflectance Mosaic/Composite image. + +*** + +False Color: Red = M3, Green = I3, Blue = M11 + +This combination is used to map snow and ice. Snow and ice are very reflective in the visible part of the spectrum (Band M3), and very absorbent in Bands I3 and M11 (short-wave infrared, or SWIR). This band combination is good for distinguishing liquid water from frozen water, for example, clouds over snow, ice cloud versus water cloud; or floods from dense vegetation. + +The Visible Infrared Imaging Radiometer Suite (VIIRS) Corrected Reflectance imagery is available only as near real-time imagery. The VIIRS instrument in on board the joint NASA/NOAA Suomi National Polar orbiting Partnership (Suomi NPP) satellite. The imagery can be visualized in Worldview and the Global Imagery Browse Services (GIBS). The sensor resolution is 750 m and 375 m (M Bands are 750 m, I Bands are 375 m), imagery resolution is 250 m, and the temporal resolution is daily. + +#### Snow and Ice +Since the only visible light used in these images (Band M3) is assigned to red, snow and ice appear bright red. The more ice, the stronger the absorption in the SWIR bands, and the more red the color. Thick ice and snow appear vivid red (or dark pink), while small ice crystals in high-level clouds will appear pinkish. + +#### Vegetation +Vegetation will appear green in this band combination, as vegetation is absorbent in Bands M3 and M11, but reflective in Band I3. Bare soil and deserts will appear bright cyan in the image since it much more reflective in Band I3 and M11 than Band M3. + +#### Water +Liquid water on the ground will appear very dark since it absorbs in the red and the SWIR, but small liquid water drops in clouds scatter light equally in both the visible and the SWIR, and will therefore appear white. Sediments in water appear dark red. + +References: VNP03MOD_NRT [doi:10.5067/VIIRS/VNP03MOD_NRT.002](https://doi.org/10.5067/VIIRS/VNP03MOD_NRT.002); VNP03IMG_NRT [doi:10.5067/VIIRS/VNP03IMG_NRT.002](https://doi.org/10.5067/VIIRS/VNP03IMG_NRT.002); VNP02MOD_NRT [doi:10.5067/VIIRS/VNP02MOD_NRT.002](https://doi.org/10.5067/VIIRS/VNP02MOD_NRT.002); VNP02IMG_NRT [doi:10.5067/VIIRS/VNP02IMG_NRT.002](https://doi.org/10.5067/VIIRS/VNP02IMG_NRT.002) \ No newline at end of file diff --git a/config/default/common/config/metadata/layers/viirs/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.md b/config/default/common/config/metadata/layers/viirs/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.md new file mode 100644 index 0000000000..72f3a587de --- /dev/null +++ b/config/default/common/config/metadata/layers/viirs/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.md @@ -0,0 +1,13 @@ +Granule Corrected Reflectance imagery displays each 6-minute granule of imagery in near real-time. It is available on a 15 day rolling window basis. Each granule shows imagery as it was captured by the VIIRS instrument in that 6-minute time period and reveals areas that may have otherwise been obscured by the mosaicking of subsequent overpasses. + +For a daily overview, please refer to the associated Corrected Reflectance Mosaic/Composite image. + +*** + +True Color: Red = Band I1, Green = Band M4, Blue = Band M3 + +These images are called true-color or natural color because this combination of wavelengths is similar to what the human eye would see. The images are natural-looking images of land surface, oceanic and atmospheric features. + +The Visible Infrared Imaging Radiometer Suite (VIIRS) Corrected Reflectance imagery is available only as near real-time imagery. The VIIRS instrument in on board the joint NASA/NOAA Suomi National Polar orbiting Partnership (Suomi NPP) satellite. The imagery can be visualized in Worldview and the Global Imagery Browse Services (GIBS). The sensor resolution is 750 m and 375 m (M Bands are 750 m, I Bands are 375 m), imagery resolution is 250 m, and the temporal resolution is daily. + +References: VNP03MOD_NRT [doi:10.5067/VIIRS/VNP03MOD_NRT.002](https://doi.org/10.5067/VIIRS/VNP03MOD_NRT.002); VNP03IMG_NRT [doi:10.5067/VIIRS/VNP03IMG_NRT.002](https://doi.org/10.5067/VIIRS/VNP03IMG_NRT.002); VNP02MOD_NRT [doi:10.5067/VIIRS/VNP02MOD_NRT.002](https://doi.org/10.5067/VIIRS/VNP02MOD_NRT.002); VNP02IMG_NRT [doi:10.5067/VIIRS/VNP02IMG_NRT.002](https://doi.org/10.5067/VIIRS/VNP02IMG_NRT.002) diff --git a/config/default/common/config/metadata/stories/hls_intro/step002.md b/config/default/common/config/metadata/stories/hls_intro/step002.md index de55e07865..989e93b2d8 100644 --- a/config/default/common/config/metadata/stories/hls_intro/step002.md +++ b/config/default/common/config/metadata/stories/hls_intro/step002.md @@ -1 +1 @@ -The Harmonized Landsat Sentinel-2 (HLS) project brings us 30 meter resolution true color surface reflectance imagery from the Operational Land Imager (OLI) instrument aboard the NASA/USGS Landsat 8 satellite, and the Multi-Spectral Instrument (MSI) aboard the European Space Agency (ESA) Sentinel 2A and Sentinel 2B satellites. \ No newline at end of file +The Harmonized Landsat Sentinel-2 (HLS) project brings us 30 meter resolution true color surface reflectance imagery from the Operational Land Imager (OLI) instrument aboard the NASA/USGS Landsat 8 and 9 satellites, and the Multi-Spectral Instrument (MSI) aboard the European Space Agency (ESA) Sentinel 2A and Sentinel 2B satellites. \ No newline at end of file diff --git a/config/default/common/config/metadata/stories/hls_intro/step003.md b/config/default/common/config/metadata/stories/hls_intro/step003.md index 8d845c37d8..2e4e2038d5 100644 --- a/config/default/common/config/metadata/stories/hls_intro/step003.md +++ b/config/default/common/config/metadata/stories/hls_intro/step003.md @@ -1 +1 @@ - The HLS imagery are approximately 8 times higher spatial resolution than the MODIS and VIIRS surface reflectance layers - watch New York City come in focus as you swipe to the left! \ No newline at end of file +The HLS imagery are approximately 8 times higher spatial resolution than the MODIS and VIIRS surface reflectance layers - watch New York City come in focus as you swipe to the left! \ No newline at end of file diff --git a/config/default/common/config/metadata/stories/hls_intro/step004.md b/config/default/common/config/metadata/stories/hls_intro/step004.md index a231f280ff..37ab5e767c 100644 --- a/config/default/common/config/metadata/stories/hls_intro/step004.md +++ b/config/default/common/config/metadata/stories/hls_intro/step004.md @@ -1,2 +1,2 @@ - To produce the Nadir Bidirectional Reflectance Distribution Function (BRDF)-Adjusted Reflectance (NBAR) imagery, the data from the two instruments aboard the three satellites are processed through a set of algorithms to make the imagery consistent and comparable across the instruments. This includes atmospheric correction, cloud and cloud-shadow masking, spatial co-registration and common gridding, illumination and view angle normalization, and spectral bandpass adjustment. Turn on and off the Landsat 8 and Sentinel 2A & 2B Reflectance layers by clicking on the eye icon to see how seamless the imagery is. + To produce the Nadir Bidirectional Reflectance Distribution Function (BRDF)-Adjusted Reflectance (NBAR) imagery, the data from the two instruments aboard the four satellites are processed through a set of algorithms to make the imagery consistent and comparable across the instruments. This includes atmospheric correction, cloud and cloud-shadow masking, spatial co-registration and common gridding, illumination and view angle normalization, and spectral bandpass adjustment. Turn on and off the Landsat 8 & 9 and Sentinel 2A & 2B Reflectance layers by clicking on the eye icon to see how seamless the imagery is. diff --git a/config/default/common/config/metadata/stories/hls_intro/step006.md b/config/default/common/config/metadata/stories/hls_intro/step006.md index 9f7b664354..8045fdd6c1 100644 --- a/config/default/common/config/metadata/stories/hls_intro/step006.md +++ b/config/default/common/config/metadata/stories/hls_intro/step006.md @@ -1 +1 @@ -Another trade-off is with how often the satellite instrument collects imagery over a particular part of the Earth. MODIS has a high temporal revisit period therefore there is near daily coverage of the Earth's surface. MSI, aboard Sentinel 2A and 2B, have a 5 day revisit period and OLI, aboard Landsat, has a 16 day revisit period. Combined together by the HLS project, this equates to near global coverage in 2 to 3 days. +Another trade-off is with how often the satellite instrument collects imagery over a particular part of the Earth. MODIS has a high temporal revisit period therefore there is near daily coverage of the Earth's surface. MSI, aboard Sentinel 2A and 2B, have a 5 day revisit period and OLI, aboard Landsat 8 and 9, have an 8 day revisit period. Combined together by the HLS project, this equates to near global coverage in 2 to 3 days. diff --git a/config/default/common/config/metadata/stories/hls_intro/step007.md b/config/default/common/config/metadata/stories/hls_intro/step007.md index c4126c6225..b092a66223 100644 --- a/config/default/common/config/metadata/stories/hls_intro/step007.md +++ b/config/default/common/config/metadata/stories/hls_intro/step007.md @@ -1 +1 @@ -These satellites have a similar overpass time as the Terra satellite (approximately 10:00 - 10:30am local time equatorial crossing), providing continuity to the Terra/MODIS surface reflectance layers. Orbit tracks are shown here from the Landsat 8 satellite in red, Sentinel 2A in magenta and Sentinel 2B in lime green. \ No newline at end of file +These satellites have a similar overpass time as the Terra satellite (approximately 10:00 - 10:30am local time equatorial crossing), providing continuity to the Terra/MODIS surface reflectance layers. Orbit tracks are shown here from the Landsat 8 satellite in red, Landsat 9 in green, Sentinel 2A in magenta, and Sentinel 2B in lime green. \ No newline at end of file diff --git a/config/default/common/config/metadata/stories/hls_intro/step017.md b/config/default/common/config/metadata/stories/hls_intro/step017.md new file mode 100644 index 0000000000..446ee6fa6d --- /dev/null +++ b/config/default/common/config/metadata/stories/hls_intro/step017.md @@ -0,0 +1 @@ +Mount Taranaki in New Zealand is a dormant stratovolcano. Visible in the imagery is a protected area, in dark green, around the summit. Egmont National Park is a mostly circular area with a radius of six miles from the summit and includes older volcanic remnants of Pouakai and Kaitake. Beyond the boundaries of the park are intensively farmed dairy pastures. \ No newline at end of file diff --git a/config/default/common/config/metadata/stories/hls_intro/step018.md b/config/default/common/config/metadata/stories/hls_intro/step018.md new file mode 100644 index 0000000000..f44f0d9350 --- /dev/null +++ b/config/default/common/config/metadata/stories/hls_intro/step018.md @@ -0,0 +1 @@ +Poniente Almeriense, in Almeria, Spain is also known as the "Mar de plástico" (Sea of Plastic) due to the vast number of greenhouses across the region. These greenhouses are made from polyethylene plastic fastened to wooden posts or metal structures, and results in the prevalent white appearance of the region. The largest municipalities in the area are El Ejido and Roquetas de Mar. Roquetas de Mar is on the eastern side of the area along the shore, and El Ejido is visible as the brown patch within the sea of plastic. \ No newline at end of file diff --git a/config/default/common/config/wv.json/layerOrder.json b/config/default/common/config/wv.json/layerOrder.json index 1e266edba6..99e2b820d7 100644 --- a/config/default/common/config/wv.json/layerOrder.json +++ b/config/default/common/config/wv.json/layerOrder.json @@ -1,5 +1,11 @@ { "layerOrder": [ + "VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule", + "VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule", + "VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule", + "VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule", + "VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule", + "VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule", "VIIRS_SNPP_CorrectedReflectance_TrueColor", "VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11", "VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1", diff --git a/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1.json b/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1.json index ec544e010b..0b6d6f7a90 100644 --- a/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1.json +++ b/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1.json @@ -6,7 +6,8 @@ "tags": "false color jpss1 7-2-1 jpss-1", "layergroup": "Corrected Reflectance", "group": "baselayers", - "wrapadjacentdays": true + "wrapadjacentdays": true, + "associatedLayers": ["VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule"] } } } \ No newline at end of file diff --git a/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.json b/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.json new file mode 100644 index 0000000000..99136e1e7f --- /dev/null +++ b/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.json @@ -0,0 +1,18 @@ +{ + "layers": { + "VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule": { + "id": "VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule", + "description": "viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule", + "tags": "subdaily", + "group": "overlays", + "layergroup": "Granules", + "type": "granule", + "period": "subdaily", + "associatedLayers": ["VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1"], + "availability": { + "rollingWindow": 30 + }, + "disableSnapshot": true + } + } +} \ No newline at end of file diff --git a/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11.json b/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11.json index 2652577814..3774faa076 100644 --- a/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11.json +++ b/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11.json @@ -6,7 +6,8 @@ "tags": "false color jpss1 3-6-7 jpss-1", "layergroup": "Corrected Reflectance", "group": "baselayers", - "wrapadjacentdays": true + "wrapadjacentdays": true, + "associatedLayers": ["VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule"] } } } \ No newline at end of file diff --git a/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.json b/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.json new file mode 100644 index 0000000000..e1a2c7e53f --- /dev/null +++ b/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.json @@ -0,0 +1,18 @@ +{ + "layers": { + "VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule": { + "id": "VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule", + "description": "viirs/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule", + "tags": "subdaily", + "group": "overlays", + "layergroup": "Granules", + "type": "granule", + "period": "subdaily", + "associatedLayers": ["VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11"], + "availability": { + "rollingWindow": 30 + }, + "disableSnapshot": true + } + } +} \ No newline at end of file diff --git a/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_TrueColor.json b/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_TrueColor.json index a22e4f5335..8c1f59be3b 100644 --- a/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_TrueColor.json +++ b/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_TrueColor.json @@ -6,7 +6,8 @@ "tags": "natural color cr jpss1 jpss-1", "layergroup": "Corrected Reflectance", "group": "baselayers", - "wrapadjacentdays": true + "wrapadjacentdays": true, + "associatedLayers": ["VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule"] } } } \ No newline at end of file diff --git a/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.json b/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.json new file mode 100644 index 0000000000..9476084bf1 --- /dev/null +++ b/config/default/common/config/wv.json/layers/viirs/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.json @@ -0,0 +1,18 @@ +{ + "layers": { + "VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule": { + "id": "VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule", + "description": "viirs/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule", + "tags": "subdaily", + "group": "overlays", + "layergroup": "Granules", + "type": "granule", + "period": "subdaily", + "associatedLayers": ["VIIRS_NOAA20_CorrectedReflectance_TrueColor"], + "availability": { + "rollingWindow": 30 + }, + "disableSnapshot": true + } + } +} \ No newline at end of file diff --git a/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1.json b/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1.json index 0e3fc06902..dbfdeb40bf 100644 --- a/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1.json +++ b/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1.json @@ -6,7 +6,8 @@ "tags": "false color s-npp snpp 7-2-1", "layergroup": "Corrected Reflectance", "group": "baselayers", - "wrapadjacentdays": true + "wrapadjacentdays": true, + "associatedLayers": ["VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule"] } } } \ No newline at end of file diff --git a/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.json b/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.json new file mode 100644 index 0000000000..e4748a97e9 --- /dev/null +++ b/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.json @@ -0,0 +1,18 @@ +{ + "layers": { + "VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule": { + "id": "VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule", + "description": "viirs/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule", + "tags": "subdaily", + "group": "overlays", + "layergroup": "Granules", + "type": "granule", + "period": "subdaily", + "associatedLayers": ["VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1"], + "availability": { + "rollingWindow": 30 + }, + "disableSnapshot": true + } + } +} \ No newline at end of file diff --git a/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11.json b/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11.json index 280db4de0b..17bf562f3e 100644 --- a/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11.json +++ b/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11.json @@ -6,7 +6,8 @@ "tags": "false color s-npp snpp 3-6-7", "layergroup": "Corrected Reflectance", "group": "baselayers", - "wrapadjacentdays": true + "wrapadjacentdays": true, + "associatedLayers": ["VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule"] } } } \ No newline at end of file diff --git a/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.json b/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.json new file mode 100644 index 0000000000..8c52ff316d --- /dev/null +++ b/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.json @@ -0,0 +1,18 @@ +{ + "layers": { + "VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule": { + "id": "VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule", + "description": "viirs/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule", + "tags": "subdaily", + "group": "overlays", + "layergroup": "Granules", + "type": "granule", + "period": "subdaily", + "associatedLayers": ["VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11"], + "availability": { + "rollingWindow": 30 + }, + "disableSnapshot": true + } + } +} \ No newline at end of file diff --git a/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_TrueColor.json b/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_TrueColor.json index 1f4a0d468f..7f729b64a6 100644 --- a/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_TrueColor.json +++ b/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_TrueColor.json @@ -7,6 +7,7 @@ "layergroup": "Corrected Reflectance", "group": "baselayers", "wrapadjacentdays": true, + "associatedLayers": ["VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule"], "projections": { "geographic": { "source": "GIBS:geographic" diff --git a/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.json b/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.json new file mode 100644 index 0000000000..59b16dc157 --- /dev/null +++ b/config/default/common/config/wv.json/layers/viirs/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.json @@ -0,0 +1,18 @@ +{ + "layers": { + "VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule": { + "id": "VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule", + "description": "viirs/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule", + "tags": "subdaily", + "group": "overlays", + "layergroup": "Granules", + "type": "granule", + "period": "subdaily", + "associatedLayers": ["VIIRS_SNPP_CorrectedReflectance_TrueColor"], + "availability": { + "rollingWindow": 30 + }, + "disableSnapshot": true + } + } +} \ No newline at end of file diff --git a/config/default/common/config/wv.json/measurements/Corrected Reflectance.json b/config/default/common/config/wv.json/measurements/Corrected Reflectance.json index 2a47be7650..c12394a960 100644 --- a/config/default/common/config/wv.json/measurements/Corrected Reflectance.json +++ b/config/default/common/config/wv.json/measurements/Corrected Reflectance.json @@ -35,9 +35,12 @@ "image": "", "settings": [ "VIIRS_SNPP_CorrectedReflectance_TrueColor", + "VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule", "VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1", + "VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule", "VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11", - "OrbitTracks_Suomi_NPP_Ascending" + "VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule", + "OrbitTracks_Suomi_NPP_Ascending" ] }, "NOAA-20/VIIRS": { @@ -47,8 +50,11 @@ "image": "", "settings": [ "VIIRS_NOAA20_CorrectedReflectance_TrueColor", + "VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule", "VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1", + "VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule", "VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11", + "VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule", "OrbitTracks_NOAA-20_Ascending" ] }, diff --git a/config/default/common/config/wv.json/stories/default/hls_intro.json b/config/default/common/config/wv.json/stories/default/hls_intro.json index b3d66ae3ca..6b9d72bb90 100644 --- a/config/default/common/config/wv.json/stories/default/hls_intro.json +++ b/config/default/common/config/wv.json/stories/default/hls_intro.json @@ -13,12 +13,12 @@ "link": "https://earthdata.nasa.gov/esds/harmonized-landsat-sentinel-2" }, { - "title": "Release of HLS Sentinel-2 Version 1.5 Provisional Data", - "link": "https://lpdaac.usgs.gov/news/release-hls-sentinel-2-provisional-data/" + "title": "HLSL30 v002 HLS Landsat Operational Land Imager Surface Reflectance and TOA Brightness Daily Global 30m", + "link": "https://lpdaac.usgs.gov/products/hlsl30v002/" }, { - "title": "Release of Provisional HLS Landsat Version 1.5 Data", - "link": "https://lpdaac.usgs.gov/news/release-provisional-hls-landsat-version-15-data/" + "title": "HLSS30 v002 HLS Sentinel-2 Multi-Spectral Instrument Surface Reflectance Daily Global 30m", + "link": "https://lpdaac.usgs.gov/products/hlss30v002/" } ], "steps": [ @@ -56,7 +56,7 @@ "element": "", "action": "" }, - "stepLink": "v=-88.87933664804595,33.79775271968792,-88.3723176050772,34.024895053672296&t=2021-01-15-T16%3A46%3A06Z&l=Reference_Labels_15m(hidden),Reference_Features_15m(hidden),Coastlines_15m(hidden),HLS_L30_Nadir_BRDF_Adjusted_Reflectance,HLS_S30_Nadir_BRDF_Adjusted_Reflectance,Land_Water_Map(opacity=0.6),VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor(hidden)" + "stepLink": "v=-88.87933664804595,33.79775271968792,-88.3723176050772,34.024895053672296&t=2022-10-17-T00%3A00%3A00Z&l=Reference_Labels_15m(hidden),Reference_Features_15m(hidden),Coastlines_15m(hidden),HLS_L30_Nadir_BRDF_Adjusted_Reflectance,HLS_S30_Nadir_BRDF_Adjusted_Reflectance,Land_Water_Map(opacity=0.6),VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor(hidden)" }, { "id": "005", @@ -65,7 +65,7 @@ "element": "", "action": "" }, - "stepLink": "v=99.04244505478943,-40.69934414672497,167.13895021893114,-10.19240494263764&t=2021-01-15-T18%3A41%3A50Z&l=OrbitTracks_Landsat-8_Descending(hidden),OrbitTracks_Sentinel-2A_Descending,OrbitTracks_Sentinel-2B_Descending,Reference_Labels_15m(hidden),Reference_Features_15m(hidden),Coastlines_15m(hidden,opacity=0.19),HLS_L30_Nadir_BRDF_Adjusted_Reflectance(hidden),HLS_S30_Nadir_BRDF_Adjusted_Reflectance,VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor(opacity=0.8)" + "stepLink": "v=99.04244505478943,-40.69934414672497,167.13895021893114,-10.19240494263764&t=2021-01-15-T18%3A41%3A50Z&l=OrbitTracks_Landsat-8_Descending(hidden),OrbitTracks_Landsat-9_Descending(hidden),OrbitTracks_Sentinel-2A_Descending,OrbitTracks_Sentinel-2B_Descending,Reference_Labels_15m(hidden),Reference_Features_15m(hidden),Coastlines_15m(hidden,opacity=0.19),HLS_L30_Nadir_BRDF_Adjusted_Reflectance(hidden),HLS_S30_Nadir_BRDF_Adjusted_Reflectance,VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor(opacity=0.8)" }, { "id": "006", @@ -74,7 +74,7 @@ "element": "", "action": "" }, - "stepLink": "v=-200.8371812680095,-86.52209371542455,192.10431801301348,89.5139950740695&t=2020-10-09-T16%3A46%3A06Z&t1=2021-01-15-T18%3A41%3A50Z&l=Reference_Labels_15m(hidden),Reference_Features_15m(hidden),Coastlines_15m(opacity=0.6),HLS_S30_Nadir_BRDF_Adjusted_Reflectance(hidden),VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor&l1=Reference_Labels_15m(hidden),Reference_Features_15m(hidden),Coastlines_15m(opacity=0.6),HLS_L30_Nadir_BRDF_Adjusted_Reflectance,HLS_S30_Nadir_BRDF_Adjusted_Reflectance,Land_Water_Map(opacity=0.6),VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor(hidden)&ca=false&cv=49" + "stepLink": "v=-200.83718126800952,-92.22346586093234,192.1043180130135,95.21536721957729&l=Reference_Labels_15m(hidden),Reference_Features_15m(hidden),Coastlines_15m(opacity=0.6),HLS_S30_Nadir_BRDF_Adjusted_Reflectance(hidden),VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor&lg=false&l1=Reference_Labels_15m(hidden),Reference_Features_15m(hidden),Coastlines_15m(opacity=0.6),HLS_S30_Nadir_BRDF_Adjusted_Reflectance,Land_Water_Map(opacity=0.6),HLS_L30_Nadir_BRDF_Adjusted_Reflectance,VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor(hidden)&lg1=false&ca=true&cv=50&t=2022-08-29-T00%3A00%3A00Z&t1=2022-08-29-T00%3A00%3A00Z" }, { "id": "007", @@ -83,7 +83,7 @@ "element": "", "action": "" }, - "stepLink": "v=-151.61006917231674,-60.89513264610752,154.61850889095217,76.29394322514025&t=2021-01-15-T18%3A41%3A50Z&l=OrbitTracks_Terra_Descending(hidden),OrbitTracks_Landsat-8_Descending,OrbitTracks_Sentinel-2A_Descending,OrbitTracks_Sentinel-2B_Descending,Reference_Labels_15m(hidden),Reference_Features_15m(hidden),Coastlines_15m(hidden,opacity=0.19),HLS_L30_Nadir_BRDF_Adjusted_Reflectance,HLS_S30_Nadir_BRDF_Adjusted_Reflectance,Land_Water_Map,VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor(hidden)" + "stepLink": "v=-137.40439624235285,-48.09304200443005,107.66084123411495,68.8066548339932&l=Reference_Labels_15m(hidden),Reference_Features_15m(hidden),Coastlines_15m(hidden,opacity=0.19),OrbitTracks_Landsat-8_Descending,OrbitTracks_Landsat-9_Descending,OrbitTracks_Sentinel-2A_Descending,OrbitTracks_Sentinel-2B_Descending,OrbitTracks_Terra_Descending(hidden),HLS_L30_Nadir_BRDF_Adjusted_Reflectance,HLS_S30_Nadir_BRDF_Adjusted_Reflectance,Land_Water_Map,VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor(hidden)&lg=true&t=2022-11-10-T00%3A00%3A00Z" }, { "id": "008", @@ -92,7 +92,7 @@ "element": "", "action": "" }, - "stepLink": "v=-200.8371812680095,-86.52209371542455,192.10431801301348,89.5139950740695&t=2021-01-15-T18%3A41%3A50Z&l=HLS_MGRS_Granule_Grid,OrbitTracks_Landsat-8_Descending(hidden),OrbitTracks_Sentinel-2B_Descending(hidden),OrbitTracks_Sentinel-2A_Descending(hidden),Reference_Labels_15m(hidden),Reference_Features_15m(hidden),Coastlines_15m(hidden,opacity=0.19),HLS_L30_Nadir_BRDF_Adjusted_Reflectance,HLS_S30_Nadir_BRDF_Adjusted_Reflectance,Land_Water_Map,VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor(hidden)&tr=hls_intro" + "stepLink": "v=-200.83718126800952,-92.22346586093234,192.1043180130135,95.21536721957729&l=HLS_MGRS_Granule_Grid,Reference_Labels_15m(hidden),Reference_Features_15m(hidden),Coastlines_15m(hidden,opacity=0.19),HLS_S30_Nadir_BRDF_Adjusted_Reflectance,Land_Water_Map,HLS_L30_Nadir_BRDF_Adjusted_Reflectance,OrbitTracks_Landsat-8_Descending(hidden),OrbitTracks_Landsat-9_Descending(hidden),OrbitTracks_Sentinel-2B_Descending(hidden),OrbitTracks_Sentinel-2A_Descending(hidden),VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor(hidden)&lg=false&t=2022-09-15-T00%3A00%3A00Z" }, { "id": "009", @@ -165,6 +165,24 @@ "action": "" }, "stepLink": "v=-112.58220286286208,35.943216192653004,-111.75633289311408,36.31320236003197&t=2021-01-15-T18%3A41%3A50Z&l=Reference_Labels_15m(hidden),Reference_Features_15m(hidden),Coastlines_15m(hidden),HLS_L30_Nadir_BRDF_Adjusted_Reflectance,VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor(hidden)" + }, + { + "id": "017", + "description": "step017.html", + "transition": { + "element": "", + "action": "" + }, + "stepLink": "v=173.38161311926515,-39.62334037304031,174.8237503761267,-38.93541984272561&l=Reference_Labels_15m(hidden),Reference_Features_15m(hidden),Coastlines_15m(hidden),HLS_L30_Nadir_BRDF_Adjusted_Reflectance,HLS_S30_Nadir_BRDF_Adjusted_Reflectance,VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor(hidden)&lg=true&t=2022-10-21-T00%3A00%3A00Z" + }, + { + "id": "018", + "description": "step018.html", + "transition": { + "element": "", + "action": "" + }, + "stepLink": "v=-3.1395203754378196,36.61528270188857,-2.4938515093780786,36.92327617937082&l=Reference_Labels_15m,Reference_Features_15m(hidden),Coastlines_15m,HLS_S30_Nadir_BRDF_Adjusted_Reflectance(hidden),HLS_L30_Nadir_BRDF_Adjusted_Reflectance,VIIRS_NOAA20_CorrectedReflectance_TrueColor(hidden),VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor(hidden)&lg=true&t=2022-11-24-T00%3A00%3A00Z" } ] } diff --git a/config/default/common/previewLayerOverrides.json b/config/default/common/previewLayerOverrides.json index 2b0c2cf450..65f6bf134f 100644 --- a/config/default/common/previewLayerOverrides.json +++ b/config/default/common/previewLayerOverrides.json @@ -77,10 +77,6 @@ "VIIRS_NOAA20_Thermal_Anomalies_375m_Day": "2020-01-29T00:00:00Z", "VIIRS_NOAA20_Thermal_Anomalies_375m_Night": "2020-01-29T00:00:00Z", - "VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule_v1_NRT": "2019-09-22T16:00:00Z", - "VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule_v1_NRT": "2019-09-22T16:00:00Z", - "VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule_v1_NRT": "2019-09-22T16:00:00Z", - "AMSRE_Columnar_Water_Vapor_Night": "2009-08-01T00:00:00Z", "AMSRE_Columnar_Water_Vapor_Day": "2009-08-01T00:00:00Z" diff --git a/e2e/features/image-download/lat-long-inputs-test.js b/e2e/features/image-download/lat-long-inputs-test.js index b43e9f910f..7d259405c8 100644 --- a/e2e/features/image-download/lat-long-inputs-test.js +++ b/e2e/features/image-download/lat-long-inputs-test.js @@ -1,57 +1,57 @@ -const { loadAndSkipTour } = require('../../reuseables/skip-tour'); -const { - openImageDownloadPanel, -} = require('../../reuseables/image-download'); +// const { loadAndSkipTour } = require('../../reuseables/skip-tour'); +// const { +// openImageDownloadPanel, +// } = require('../../reuseables/image-download'); -const TIME_LIMIT = 5000; -const input = { - maxLat: '#latlong-input-3', - maxLon: '#latlong-input-2', - minLat: '#latlong-input-1', - minLon: '#latlong-input-0', -}; +// const TIME_LIMIT = 5000; +// const input = { +// maxLat: '#latlong-input-3', +// maxLon: '#latlong-input-2', +// minLat: '#latlong-input-1', +// minLon: '#latlong-input-0', +// }; -const changeInput = (c, selector, newValue) => { - [...'123456'].forEach(() => { - c.sendKeys(selector, c.Keys.ARROW_RIGHT); - }); - [...'123456789'].forEach(() => { - c.sendKeys(selector, c.Keys.BACK_SPACE); - }); - c.sendKeys(selector, newValue); - c.sendKeys(selector, c.Keys.ENTER); -}; +// const changeInput = (c, selector, newValue) => { +// [...'123456'].forEach(() => { +// c.sendKeys(selector, c.Keys.ARROW_RIGHT); +// }); +// [...'123456789'].forEach(() => { +// c.sendKeys(selector, c.Keys.BACK_SPACE); +// }); +// c.sendKeys(selector, newValue); +// c.sendKeys(selector, c.Keys.ENTER); +// }; -module.exports = { - before(client) { - loadAndSkipTour(client, TIME_LIMIT); - }, +// module.exports = { +// before(client) { +// loadAndSkipTour(client, TIME_LIMIT); +// }, - after(client) { - client.end(); - }, +// after(client) { +// client.end(); +// }, - 'Check that image download inputs are hidden on initial load': function(c) { - openImageDownloadPanel(c); - c.expect.element('.wv-image-input-title span').text.to.equal('Edit Coordinates'); - c.expect.element('.wv-image-input-subtitle').to.not.be.present; - }, - 'Check that image download extent inputs open on click': function(c) { - c.click('.wv-image-input-title span'); - c.waitForElementVisible('.wv-image-input-subtitle', TIME_LIMIT); - }, - 'Verify that input updates crop boundary labels ': function(c) { - c.waitForElementVisible(input.maxLat, TIME_LIMIT); +// 'Check that image download inputs are hidden on initial load': function(c) { +// openImageDownloadPanel(c); +// c.expect.element('.wv-image-input-title span').text.to.equal('Edit Coordinates'); +// c.expect.element('.wv-image-input-subtitle').to.not.be.present; +// }, +// 'Check that image download extent inputs open on click': function(c) { +// c.click('.wv-image-input-title span'); +// c.waitForElementVisible('.wv-image-input-subtitle', TIME_LIMIT); +// }, +// 'Verify that input updates crop boundary labels ': function(c) { +// c.waitForElementVisible(input.maxLat, TIME_LIMIT); - changeInput(c, input.maxLat, '-14'); - changeInput(c, input.maxLon, '14'); - changeInput(c, input.minLat, '-40'); - changeInput(c, input.minLon, '-20'); +// changeInput(c, input.maxLat, '-14'); +// changeInput(c, input.maxLon, '14'); +// changeInput(c, input.minLat, '-40'); +// changeInput(c, input.minLon, '-20'); - c.waitForElementVisible('#wv-image-top', TIME_LIMIT); - c.assert.containsText('#wv-image-top', '-14.0000'); - c.assert.containsText('#wv-image-top', '14.0000'); - c.assert.containsText('#wv-image-bottom', '-40.0000'); - c.assert.containsText('#wv-image-bottom', '-20.0000'); - }, -}; +// c.waitForElementVisible('#wv-image-top', TIME_LIMIT); +// c.assert.containsText('#wv-image-top', '-14.0000'); +// c.assert.containsText('#wv-image-top', '14.0000'); +// c.assert.containsText('#wv-image-bottom', '-40.0000'); +// c.assert.containsText('#wv-image-bottom', '-20.0000'); +// }, +// }; diff --git a/e2e/features/layers/layer-picker-test.js b/e2e/features/layers/layer-picker-test.js index 1da936edf1..78b7f5e013 100644 --- a/e2e/features/layers/layer-picker-test.js +++ b/e2e/features/layers/layer-picker-test.js @@ -175,10 +175,10 @@ module.exports = { c.click(availableFilterCheckbox); c.pause(200); c.expect.element(availableFilterCheckboxInput).to.not.be.selected; - c.expect.elements(layersSearchRow).count.to.equal(8); + c.expect.elements(layersSearchRow).count.to.equal(10); c .assert - .containsText(layerResultsCountText, 'Showing 8 out of'); + .containsText(layerResultsCountText, 'Showing 10 out of'); }, 'Finding layer by ID with search': (c) => { c.clearValue(layersSearchField); diff --git a/package-lock.json b/package-lock.json index 61085341cd..33a24cc684 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,8 +35,8 @@ "moment": "^2.29.4", "moment-locales-webpack-plugin": "^1.2.0", "node-dir": "^0.1.17", - "ol": "7.1.0", - "ol-mapbox-style": "9.1.0", + "ol": "7.2.2", + "ol-mapbox-style": "9.4.0", "p-queue": "^7.3.0", "proj4": "2.6.1", "promise-queue": "2.2.5", @@ -2733,20 +2733,23 @@ }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", "engines": { "node": ">= 0.6" } }, "node_modules/@mapbox/mapbox-gl-style-spec": { - "version": "13.25.0", - "license": "ISC", + "version": "13.28.0", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.28.0.tgz", + "integrity": "sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg==", "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/point-geometry": "^0.1.0", "@mapbox/unitbezier": "^0.0.0", "csscolorparser": "~1.0.2", "json-stringify-pretty-compact": "^2.0.0", - "minimist": "^1.2.5", + "minimist": "^1.2.6", "rw": "^1.3.3", "sort-object": "^0.3.2" }, @@ -2759,11 +2762,13 @@ }, "node_modules/@mapbox/point-geometry": { "version": "0.1.0", - "license": "ISC" + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==" }, "node_modules/@mapbox/unitbezier": { "version": "0.0.0", - "license": "BSD-2-Clause" + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", + "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==" }, "node_modules/@nightwatch/chai": { "version": "5.0.2", @@ -2869,8 +2874,9 @@ } }, "node_modules/@petamoriken/float16": { - "version": "3.6.5", - "license": "MIT" + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.7.1.tgz", + "integrity": "sha512-oXZOc+aePd0FnhTWk15pyqK+Do87n0TyLV1nxdEougE95X/WXWDqmQobfhgnSY7QsWn5euZUWuDVeTQvoQ5VNw==" }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.7", @@ -6953,7 +6959,8 @@ }, "node_modules/csscolorparser": { "version": "1.0.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==" }, "node_modules/cssesc": { "version": "3.0.0", @@ -9815,25 +9822,37 @@ "license": "MIT" }, "node_modules/geotiff": { - "version": "2.0.4", - "license": "MIT", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.0.7.tgz", + "integrity": "sha512-FKvFTNowMU5K6lHYY2f83d4lS2rsCNdpUC28AX61x9ZzzqPNaWFElWv93xj0eJFaNyOYA63ic5OzJ88dHpoA5Q==", "dependencies": { "@petamoriken/float16": "^3.4.7", "lerc": "^3.0.0", - "lru-cache": "^6.0.0", "pako": "^2.0.4", "parse-headers": "^2.0.2", + "quick-lru": "^6.1.1", "web-worker": "^1.2.0", "xml-utils": "^1.0.2" }, "engines": { - "browsers": "defaults", "node": ">=10.19" } }, "node_modules/geotiff/node_modules/pako": { - "version": "2.0.4", - "license": "(MIT AND Zlib)" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, + "node_modules/geotiff/node_modules/quick-lru": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.1.tgz", + "integrity": "sha512-S27GBT+F0NTRiehtbrgaSE1idUAJ5bX8dPAQTdylEyNlrdcH5X4Lz7Edz3DYzecbsCluD5zO8ZNEe04z3D3u6Q==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/get-caller-file": { "version": "2.0.5", @@ -13367,7 +13386,8 @@ }, "node_modules/json-stringify-pretty-compact": { "version": "2.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz", + "integrity": "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==" }, "node_modules/json-stringify-safe": { "version": "5.0.1", @@ -13501,7 +13521,8 @@ }, "node_modules/lerc": { "version": "3.0.0", - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", + "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==" }, "node_modules/leven": { "version": "3.1.0", @@ -13868,6 +13889,7 @@ }, "node_modules/lru-cache": { "version": "6.0.0", + "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -14007,8 +14029,9 @@ } }, "node_modules/mapbox-to-css-font": { - "version": "2.4.1", - "license": "BSD-2-Clause" + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-2.4.2.tgz", + "integrity": "sha512-f+NBjJJY4T3dHtlEz1wCG7YFlkODEjFIYlxDdLIDMNpkSksqTt+l/d4rjuwItxuzkuMFvPyrjzV2lxRM4ePcIA==" }, "node_modules/masonry-layout": { "version": "4.2.2", @@ -15865,13 +15888,13 @@ "license": "MIT" }, "node_modules/ol": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/ol/-/ol-7.1.0.tgz", - "integrity": "sha512-mAeV5Ca4mFhYaJoGWNZnIMN5VNnFTf63FgZjBiYu/DjQDGKNsD5QyvvqVziioVdOOgl6b8rPB/ypj2XNBinPwA==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/ol/-/ol-7.2.2.tgz", + "integrity": "sha512-eqJ1hhVQQ3Ap4OhYq9DRu5pz9RMpLhmoTauDoIqpn7logVi1AJE+lXjEHrPrTSuZYjtFbMgqr07sxoLNR65nrw==", "dependencies": { "earcut": "^2.2.3", - "geotiff": "2.0.4", - "ol-mapbox-style": "9.1.0", + "geotiff": "^2.0.7", + "ol-mapbox-style": "^9.2.0", "pbf": "3.2.1", "rbush": "^3.0.1" }, @@ -15881,8 +15904,9 @@ } }, "node_modules/ol-mapbox-style": { - "version": "9.1.0", - "license": "BSD-2-Clause", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-9.4.0.tgz", + "integrity": "sha512-I9dK1K41P8DjozFeQ4x1pfou86q/WedRzjCMpNhtHW9APJ/l3UT6aGsp2HcbG7cLfx3EpzK4Q7HxKwcQYR5Chw==", "dependencies": { "@mapbox/mapbox-gl-style-spec": "^13.23.1", "mapbox-to-css-font": "^2.4.1" @@ -16253,7 +16277,8 @@ }, "node_modules/parse-headers": { "version": "2.0.5", - "license": "MIT" + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", + "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==" }, "node_modules/parse-import": { "version": "2.0.0", @@ -18961,7 +18986,8 @@ }, "node_modules/rw": { "version": "1.3.3", - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" }, "node_modules/safe-buffer": { "version": "5.1.2", @@ -20450,12 +20476,16 @@ }, "node_modules/sort-asc": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz", + "integrity": "sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw==", "engines": { "node": ">=0.10.0" } }, "node_modules/sort-desc": { "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.1.1.tgz", + "integrity": "sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw==", "engines": { "node": ">=0.10.0" } @@ -20473,6 +20503,8 @@ }, "node_modules/sort-object": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-0.3.2.tgz", + "integrity": "sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA==", "dependencies": { "sort-asc": "^0.1.0", "sort-desc": "^0.1.1" @@ -22533,7 +22565,8 @@ }, "node_modules/web-worker": { "version": "1.2.0", - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", + "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==" }, "node_modules/webidl-conversions": { "version": "6.1.0", @@ -23361,8 +23394,9 @@ "license": "Apache-2.0" }, "node_modules/xml-utils": { - "version": "1.1.0", - "license": "CC0-1.0" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.3.0.tgz", + "integrity": "sha512-i4PIrX33Wd66dvwo4syicwlwmnr6wuvvn4f2ku9hA67C2Uk62Xubczuhct+Evnd12/DV71qKNeDdJwES8HX1RA==" }, "node_modules/xmlchars": { "version": "2.2.0", @@ -23384,6 +23418,7 @@ }, "node_modules/yallist": { "version": "4.0.0", + "dev": true, "license": "ISC" }, "node_modules/yaml": { @@ -25206,26 +25241,34 @@ "dev": true }, "@mapbox/jsonlint-lines-primitives": { - "version": "2.0.2" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==" }, "@mapbox/mapbox-gl-style-spec": { - "version": "13.25.0", + "version": "13.28.0", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.28.0.tgz", + "integrity": "sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg==", "requires": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/point-geometry": "^0.1.0", "@mapbox/unitbezier": "^0.0.0", "csscolorparser": "~1.0.2", "json-stringify-pretty-compact": "^2.0.0", - "minimist": "^1.2.5", + "minimist": "^1.2.6", "rw": "^1.3.3", "sort-object": "^0.3.2" } }, "@mapbox/point-geometry": { - "version": "0.1.0" + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==" }, "@mapbox/unitbezier": { - "version": "0.0.0" + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", + "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==" }, "@nightwatch/chai": { "version": "5.0.2", @@ -25301,7 +25344,9 @@ } }, "@petamoriken/float16": { - "version": "3.6.5" + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.7.1.tgz", + "integrity": "sha512-oXZOc+aePd0FnhTWk15pyqK+Do87n0TyLV1nxdEougE95X/WXWDqmQobfhgnSY7QsWn5euZUWuDVeTQvoQ5VNw==" }, "@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.7", @@ -28272,7 +28317,9 @@ "dev": true }, "csscolorparser": { - "version": "1.0.3" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==" }, "cssesc": { "version": "3.0.0", @@ -30227,19 +30274,28 @@ "version": "1.52.0" }, "geotiff": { - "version": "2.0.4", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.0.7.tgz", + "integrity": "sha512-FKvFTNowMU5K6lHYY2f83d4lS2rsCNdpUC28AX61x9ZzzqPNaWFElWv93xj0eJFaNyOYA63ic5OzJ88dHpoA5Q==", "requires": { "@petamoriken/float16": "^3.4.7", "lerc": "^3.0.0", - "lru-cache": "^6.0.0", "pako": "^2.0.4", "parse-headers": "^2.0.2", + "quick-lru": "^6.1.1", "web-worker": "^1.2.0", "xml-utils": "^1.0.2" }, "dependencies": { "pako": { - "version": "2.0.4" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, + "quick-lru": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.1.tgz", + "integrity": "sha512-S27GBT+F0NTRiehtbrgaSE1idUAJ5bX8dPAQTdylEyNlrdcH5X4Lz7Edz3DYzecbsCluD5zO8ZNEe04z3D3u6Q==" } } }, @@ -32544,7 +32600,9 @@ "dev": true }, "json-stringify-pretty-compact": { - "version": "2.0.0" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz", + "integrity": "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==" }, "json-stringify-safe": { "version": "5.0.1", @@ -32644,7 +32702,9 @@ } }, "lerc": { - "version": "3.0.0" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", + "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==" }, "leven": { "version": "3.1.0", @@ -32924,6 +32984,7 @@ }, "lru-cache": { "version": "6.0.0", + "dev": true, "requires": { "yallist": "^4.0.0" } @@ -33026,7 +33087,9 @@ } }, "mapbox-to-css-font": { - "version": "2.4.1" + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-2.4.2.tgz", + "integrity": "sha512-f+NBjJJY4T3dHtlEz1wCG7YFlkODEjFIYlxDdLIDMNpkSksqTt+l/d4rjuwItxuzkuMFvPyrjzV2lxRM4ePcIA==" }, "masonry-layout": { "version": "4.2.2", @@ -34348,19 +34411,21 @@ "dev": true }, "ol": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/ol/-/ol-7.1.0.tgz", - "integrity": "sha512-mAeV5Ca4mFhYaJoGWNZnIMN5VNnFTf63FgZjBiYu/DjQDGKNsD5QyvvqVziioVdOOgl6b8rPB/ypj2XNBinPwA==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/ol/-/ol-7.2.2.tgz", + "integrity": "sha512-eqJ1hhVQQ3Ap4OhYq9DRu5pz9RMpLhmoTauDoIqpn7logVi1AJE+lXjEHrPrTSuZYjtFbMgqr07sxoLNR65nrw==", "requires": { "earcut": "^2.2.3", - "geotiff": "2.0.4", - "ol-mapbox-style": "9.1.0", + "geotiff": "^2.0.7", + "ol-mapbox-style": "^9.2.0", "pbf": "3.2.1", "rbush": "^3.0.1" } }, "ol-mapbox-style": { - "version": "9.1.0", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-9.4.0.tgz", + "integrity": "sha512-I9dK1K41P8DjozFeQ4x1pfou86q/WedRzjCMpNhtHW9APJ/l3UT6aGsp2HcbG7cLfx3EpzK4Q7HxKwcQYR5Chw==", "requires": { "@mapbox/mapbox-gl-style-spec": "^13.23.1", "mapbox-to-css-font": "^2.4.1" @@ -34600,7 +34665,9 @@ } }, "parse-headers": { - "version": "2.0.5" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", + "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==" }, "parse-import": { "version": "2.0.0", @@ -36375,7 +36442,9 @@ "dev": true }, "rw": { - "version": "1.3.3" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" }, "safe-buffer": { "version": "5.1.2" @@ -37432,10 +37501,14 @@ } }, "sort-asc": { - "version": "0.1.0" + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz", + "integrity": "sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw==" }, "sort-desc": { - "version": "0.1.1" + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.1.1.tgz", + "integrity": "sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw==" }, "sort-keys": { "version": "1.1.2", @@ -37446,6 +37519,8 @@ }, "sort-object": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-0.3.2.tgz", + "integrity": "sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA==", "requires": { "sort-asc": "^0.1.0", "sort-desc": "^0.1.1" @@ -38876,7 +38951,9 @@ } }, "web-worker": { - "version": "1.2.0" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", + "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==" }, "webidl-conversions": { "version": "6.1.0", @@ -39440,7 +39517,9 @@ "dev": true }, "xml-utils": { - "version": "1.1.0" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.3.0.tgz", + "integrity": "sha512-i4PIrX33Wd66dvwo4syicwlwmnr6wuvvn4f2ku9hA67C2Uk62Xubczuhct+Evnd12/DV71qKNeDdJwES8HX1RA==" }, "xmlchars": { "version": "2.2.0", @@ -39455,7 +39534,8 @@ "dev": true }, "yallist": { - "version": "4.0.0" + "version": "4.0.0", + "dev": true }, "yaml": { "version": "1.10.2" diff --git a/package.json b/package.json index 5e9d2c6826..862b08f1f4 100644 --- a/package.json +++ b/package.json @@ -185,8 +185,8 @@ "moment": "^2.29.4", "moment-locales-webpack-plugin": "^1.2.0", "node-dir": "^0.1.17", - "ol": "7.1.0", - "ol-mapbox-style": "9.1.0", + "ol": "7.2.2", + "ol-mapbox-style": "9.4.0", "p-queue": "^7.3.0", "proj4": "2.6.1", "promise-queue": "2.2.5", diff --git a/schemas/layer-config.json b/schemas/layer-config.json index f723534ca8..304a8e3f4e 100644 --- a/schemas/layer-config.json +++ b/schemas/layer-config.json @@ -290,7 +290,7 @@ }, "associatedLayers": { "description": "ID of an associated layers (e.g. a corresponding granule layer)", - "type": "string" + "type": "array" }, "layer": { "type": "string" diff --git a/web/images/layers/previews/antarctic/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg b/web/images/layers/previews/antarctic/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg new file mode 100644 index 0000000000..f515780cfb Binary files /dev/null and b/web/images/layers/previews/antarctic/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg differ diff --git a/web/images/layers/previews/antarctic/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg b/web/images/layers/previews/antarctic/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg new file mode 100644 index 0000000000..b6bbcb5d7a Binary files /dev/null and b/web/images/layers/previews/antarctic/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg differ diff --git a/web/images/layers/previews/antarctic/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.jpg b/web/images/layers/previews/antarctic/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.jpg new file mode 100644 index 0000000000..b779b2b529 Binary files /dev/null and b/web/images/layers/previews/antarctic/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.jpg differ diff --git a/web/images/layers/previews/antarctic/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg b/web/images/layers/previews/antarctic/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg new file mode 100644 index 0000000000..d6549dcdb8 Binary files /dev/null and b/web/images/layers/previews/antarctic/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg differ diff --git a/web/images/layers/previews/antarctic/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg b/web/images/layers/previews/antarctic/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg new file mode 100644 index 0000000000..3518319243 Binary files /dev/null and b/web/images/layers/previews/antarctic/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg differ diff --git a/web/images/layers/previews/antarctic/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.jpg b/web/images/layers/previews/antarctic/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.jpg new file mode 100644 index 0000000000..82b2854141 Binary files /dev/null and b/web/images/layers/previews/antarctic/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.jpg differ diff --git a/web/images/layers/previews/arctic/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg b/web/images/layers/previews/arctic/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg new file mode 100644 index 0000000000..07f4e7207e Binary files /dev/null and b/web/images/layers/previews/arctic/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg differ diff --git a/web/images/layers/previews/arctic/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg b/web/images/layers/previews/arctic/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg new file mode 100644 index 0000000000..6c39947073 Binary files /dev/null and b/web/images/layers/previews/arctic/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg differ diff --git a/web/images/layers/previews/arctic/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.jpg b/web/images/layers/previews/arctic/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.jpg new file mode 100644 index 0000000000..256b0a9237 Binary files /dev/null and b/web/images/layers/previews/arctic/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.jpg differ diff --git a/web/images/layers/previews/arctic/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg b/web/images/layers/previews/arctic/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg new file mode 100644 index 0000000000..9e4e8ca7d3 Binary files /dev/null and b/web/images/layers/previews/arctic/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg differ diff --git a/web/images/layers/previews/arctic/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg b/web/images/layers/previews/arctic/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg new file mode 100644 index 0000000000..c2117925ea Binary files /dev/null and b/web/images/layers/previews/arctic/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg differ diff --git a/web/images/layers/previews/arctic/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.jpg b/web/images/layers/previews/arctic/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.jpg new file mode 100644 index 0000000000..b01c762332 Binary files /dev/null and b/web/images/layers/previews/arctic/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.jpg differ diff --git a/web/images/layers/previews/geographic/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg b/web/images/layers/previews/geographic/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg new file mode 100644 index 0000000000..5f8658c869 Binary files /dev/null and b/web/images/layers/previews/geographic/VIIRS_NOAA20_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg differ diff --git a/web/images/layers/previews/geographic/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg b/web/images/layers/previews/geographic/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg new file mode 100644 index 0000000000..08a09d4a6a Binary files /dev/null and b/web/images/layers/previews/geographic/VIIRS_NOAA20_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg differ diff --git a/web/images/layers/previews/geographic/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.jpg b/web/images/layers/previews/geographic/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.jpg new file mode 100644 index 0000000000..d758703dd2 Binary files /dev/null and b/web/images/layers/previews/geographic/VIIRS_NOAA20_CorrectedReflectance_TrueColor_Granule.jpg differ diff --git a/web/images/layers/previews/geographic/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg b/web/images/layers/previews/geographic/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg new file mode 100644 index 0000000000..9c4c45b5d8 Binary files /dev/null and b/web/images/layers/previews/geographic/VIIRS_SNPP_CorrectedReflectance_BandsM11-I2-I1_Granule.jpg differ diff --git a/web/images/layers/previews/geographic/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg b/web/images/layers/previews/geographic/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg new file mode 100644 index 0000000000..4f1a7c0c8a Binary files /dev/null and b/web/images/layers/previews/geographic/VIIRS_SNPP_CorrectedReflectance_BandsM3-I3-M11_Granule.jpg differ diff --git a/web/images/layers/previews/geographic/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.jpg b/web/images/layers/previews/geographic/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.jpg new file mode 100644 index 0000000000..a603fa1435 Binary files /dev/null and b/web/images/layers/previews/geographic/VIIRS_SNPP_CorrectedReflectance_TrueColor_Granule.jpg differ diff --git a/web/js/combine-ui.js b/web/js/combine-ui.js deleted file mode 100644 index 5f7f482cef..0000000000 --- a/web/js/combine-ui.js +++ /dev/null @@ -1,78 +0,0 @@ -import util from './util/util'; -import mapui from './map/ui'; -import { - REDUX_ACTION_DISPATCHED, - MAP_MOUSE_OUT, - MAP_MOVE_END, - MAP_MOUSE_MOVE, - MAP_SINGLE_CLICK, - MAP_CONTEXT_MENU, -} from './util/constants'; - -const { events } = util; - -/** - * Legacy UI Rendering - * @param {Object} models | Legacy Models Object - * @param {Object} config - * @param {Object} store - */ -export default function combineUi(models, config, store) { - const ui = {}; - const subscribeToStore = function() { - const state = store.getState(); - const action = state.lastAction; - return events.trigger(REDUX_ACTION_DISPATCHED, action); - }; - store.subscribe(subscribeToStore); - ui.map = mapui(models, config, store, ui); - ui.supportsPassive = false; - try { - const opts = Object.defineProperty({}, 'passive', { - // eslint-disable-next-line getter-return - get() { - ui.supportsPassive = true; - }, - }); - window.addEventListener('testPassive', null, opts); - window.removeEventListener('testPassive', null, opts); - } catch (e) { - util.warn(e); - } - - registerMapMouseHandlers(ui.map.proj); - - // Sink all focus on inputs if click unhandled - document.addEventListener('click', (e) => { - if (e.target.nodeName !== 'INPUT') { - document.querySelectorAll('input').forEach((el) => el.blur()); - } - }); - document.activeElement.blur(); - document.querySelectorAll('input').forEach((el) => el.blur()); - - return ui; -} - -function registerMapMouseHandlers(maps) { - Object.values(maps).forEach((map) => { - const element = map.getTargetElement(); - const crs = map.getView().getProjection().getCode(); - - element.addEventListener('mouseleave', (event) => { - events.trigger(MAP_MOUSE_OUT, event); - }); - map.on('moveend', (event) => { - events.trigger(MAP_MOVE_END, event, map, crs); - }); - map.on('pointermove', (event) => { - events.trigger(MAP_MOUSE_MOVE, event, map, crs); - }); - map.on('singleclick', (event) => { - events.trigger(MAP_SINGLE_CLICK, event, map, crs); - }); - map.on('contextmenu', (event) => { - events.trigger(MAP_CONTEXT_MENU, event, map, crs); - }); - }); -} diff --git a/web/js/components/animation-widget/gif-button.js b/web/js/components/animation-widget/gif-button.js index 6a885aa35f..b55df48d97 100644 --- a/web/js/components/animation-widget/gif-button.js +++ b/web/js/components/animation-widget/gif-button.js @@ -107,6 +107,7 @@ const GifButton = (props) => { {showWarning ? warningMessage : labelText} diff --git a/web/js/components/animation-widget/loop-button.js b/web/js/components/animation-widget/loop-button.js index f6ce786a35..8c313bbe1b 100644 --- a/web/js/components/animation-widget/loop-button.js +++ b/web/js/components/animation-widget/loop-button.js @@ -25,6 +25,7 @@ const LoopButton = ({ looping, onLoop, isMobile }) => { {isMobile ? null : ( diff --git a/web/js/components/animation-widget/play-button.js b/web/js/components/animation-widget/play-button.js index 5fe8b783c4..5ebea5fa1c 100644 --- a/web/js/components/animation-widget/play-button.js +++ b/web/js/components/animation-widget/play-button.js @@ -26,6 +26,7 @@ const PlayButton = ({ > {!isMobile && ( diff --git a/web/js/components/animation-widget/play-queue.js b/web/js/components/animation-widget/play-queue.js index d9902d23c9..9cf8c472eb 100644 --- a/web/js/components/animation-widget/play-queue.js +++ b/web/js/components/animation-widget/play-queue.js @@ -8,12 +8,18 @@ import LoadingIndicator from './loading-indicator'; import util from '../../util/util'; // We assume anything this fast or faster is a frame that was pulled from the cache -const MIN_REQUEST_TIME = 200; // milliseconds +const MIN_REQUEST_TIME_MS = 200; const CONCURRENT_REQUESTS = 3; const toString = (date) => util.toISOStringSeconds(date); const toDate = (dateString) => util.parseDateUTC(dateString); -// Get the initiall buffer size, using a larger buffer for higher speeds +/** + * Calculate the initial buffer size, using a larger buffer for higher speeds + * @param {Number} numberOfFrames | Number of frames for the entire animation + * @param {Number} speed | Frames Per Second selected for this animation + * + * @return {Number} | The lower of the default buffer size & the calculated buffer size. + */ const getInitialBufferSize = (numberOfFrames, speed) => { const defaultSize = 10; const buffer = defaultSize + (speed * 1.5); @@ -58,9 +64,9 @@ class PlayQueue extends React.Component { componentDidMount() { this.mounted = true; - this.queue.on('completed', (dateStr) => { - console.debug(dateStr, this.queue.size, this.queue.pending); - }); + // this.queue.on('completed', (dateStr) => { + // console.debug(dateStr, this.queue.size, this.queue.pending); + // }); this.playingDate = this.getStartDate(); this.checkQueue(); this.checkShouldPlay(); @@ -128,13 +134,15 @@ class PlayQueue extends React.Component { }; /** - * Queue up initial dates to create a minimum buffer - */ - initialPreload(date) { + * Queue up initial dates to create a minimum buffer + * @param {Date} animStartDate | 1-Day prior to the Animation Start Date + * @return {void} + */ + initialPreload(animStartDate) { const { numberOfFrames, selectDate, togglePlaying, startDate, } = this.props; - let currentDate = date; + let currentDate = animStartDate; const lastInQueue = this.getLastInQueue(); if (numberOfFrames <= 1) { // if only one frame will play just move to that date @@ -171,11 +179,11 @@ class PlayQueue extends React.Component { bufferSize = Math.ceil(preloadTime / msPerSec); } - const totalLoadTime = ((avgFetchTime * numberOfFrames) / msPerSec / CONCURRENT_REQUESTS).toFixed(2); - console.debug('Total frames: ', numberOfFrames); - console.debug('Avg fetch time: ', (avgFetchTime / msPerSec).toFixed(2)); - console.debug('Play time (t/r): ', (totalPlayTime / msPerSec).toFixed(2), (remainingPlayTime / msPerSec).toFixed(2)); - console.debug('Load time (t/r): ', totalLoadTime, (remainingLoadTime / msPerSec).toFixed(2)); + // const totalLoadTime = ((avgFetchTime * numberOfFrames) / msPerSec / CONCURRENT_REQUESTS).toFixed(2); + // console.debug('Total frames: ', numberOfFrames); + // console.debug('Avg fetch time: ', (avgFetchTime / msPerSec).toFixed(2)); + // console.debug('Play time (t/r): ', (totalPlayTime / msPerSec).toFixed(2), (remainingPlayTime / msPerSec).toFixed(2)); + // console.debug('Load time (t/r): ', totalLoadTime, (remainingLoadTime / msPerSec).toFixed(2)); const totalBuffer = bufferSize + this.initialBufferSize; if (totalBuffer >= numberOfFrames) { @@ -200,7 +208,7 @@ class PlayQueue extends React.Component { if (!this.minBufferLength) { this.minBufferLength = this.calcBufferSize(); } - console.debug(`Buffer: ${currentBufferSize} / ${this.minBufferLength}`); + // console.debug(`Buffer: ${currentBufferSize} / ${this.minBufferLength}`); return currentBufferSize >= this.minBufferLength; } @@ -214,16 +222,17 @@ class PlayQueue extends React.Component { return; } if (this.isPreloadSufficient() || restartLoop) { - console.debug('Started: ', Date.now()); + // console.debug('Started: ', Date.now()); return this.play(); } this.checkQueue(); }; checkShouldLoop() { - const { isLoopActive, startDate, togglePlaying } = this.props; - // Could base this off animation speed? - const loopDelay = 1000; + const { + isLoopActive, startDate, togglePlaying, speed, + } = this.props; + const loopDelay = speed === 0.5 ? 2000 : 1000 / speed; if (isLoopActive) { this.playingDate = toString(startDate); @@ -234,7 +243,6 @@ class PlayQueue extends React.Component { }, loopDelay); } else { togglePlaying(); - console.debug('Stopped: ', Date.now()); } } @@ -310,7 +318,7 @@ class PlayQueue extends React.Component { const startTime = Date.now(); await promiseImageryForTime(date); const elapsedTime = Date.now() - startTime; - const fetchTime = elapsedTime >= MIN_REQUEST_TIME ? elapsedTime : MIN_REQUEST_TIME; + const fetchTime = elapsedTime >= MIN_REQUEST_TIME_MS ? elapsedTime : MIN_REQUEST_TIME_MS; this.fetchTimes.push(fetchTime); this.setState({ loadedItems: loadedItems += 1 }); return strDate; @@ -353,9 +361,9 @@ class PlayQueue extends React.Component { scheduleFrame(time); }; const scheduleFrame = (time) => { - const elapsed = time - start; - const roundedElapsed = Math.round(elapsed / ms) * ms; - const targetNext = start + roundedElapsed + ms; + const elapsedTime = time - start; + const roundedElapsedTime = Math.round(elapsedTime / ms) * ms; + const targetNext = start + roundedElapsedTime + ms; const delay = targetNext - performance.now(); setTimeout(() => requestAnimationFrame(frame), delay); }; @@ -409,7 +417,8 @@ class PlayQueue extends React.Component { } this.checkQueue(); }; - this.animationInterval(1000 / speed, player); + const animIntervalMS = speed === 0.5 ? 2000 : 1000 / speed; + this.animationInterval(animIntervalMS, player); } getPlaybackPosition() { diff --git a/web/js/components/global-settings/coordinate-format-buttons.js b/web/js/components/global-settings/coordinate-format-buttons.js index e234e5548a..129ab37452 100644 --- a/web/js/components/global-settings/coordinate-format-buttons.js +++ b/web/js/components/global-settings/coordinate-format-buttons.js @@ -1,12 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, ButtonGroup } from 'reactstrap'; +import { Button, ButtonGroup, UncontrolledTooltip } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import HoverTooltip from '../util/hover-tooltip'; import { COORDINATE_FORMATS } from '../../modules/settings/constants'; function CoordinateFormatButtons ({ changeCoordinateFormat, coordinateFormat }) { const coordinateMenuOptions = ['DD', 'DDM', 'DMS']; + const labelText = 'Applied to all on screen coordinates'; return (
@@ -14,12 +14,13 @@ function CoordinateFormatButtons ({ changeCoordinateFormat, coordinateFormat }) Coordinate Format (latitude, longitude) {' '} - + > + {labelText} + {COORDINATE_FORMATS.map((format, i) => ( diff --git a/web/js/components/global-settings/temperature-unit-buttons.js b/web/js/components/global-settings/temperature-unit-buttons.js index 426948a7ad..2edbc4f77f 100644 --- a/web/js/components/global-settings/temperature-unit-buttons.js +++ b/web/js/components/global-settings/temperature-unit-buttons.js @@ -1,8 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, ButtonGroup } from 'reactstrap'; +import { Button, ButtonGroup, UncontrolledTooltip } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import HoverTooltip from '../util/hover-tooltip'; import { TEMPERATURE_UNITS } from '../../modules/settings/constants'; const TemperatureUnitButtons = ({ changeTemperatureUnit, globalTemperatureUnit }) => ( @@ -11,12 +10,13 @@ const TemperatureUnitButtons = ({ changeTemperatureUnit, globalTemperatureUnit } Temperature Unit {' '} - + > + Applied to relevant temperature layers + {TEMPERATURE_UNITS.map((unit) => ( diff --git a/web/js/components/layer/product-picker/browse/browse-layers.js b/web/js/components/layer/product-picker/browse/browse-layers.js index 8c7a56ae90..92d07a2c8d 100644 --- a/web/js/components/layer/product-picker/browse/browse-layers.js +++ b/web/js/components/layer/product-picker/browse/browse-layers.js @@ -140,6 +140,7 @@ function BrowseLayers (props) { : (
)} {(layerNotices || layerIsUnavailable) && (

Recently Used Layers

@@ -189,6 +190,7 @@ class ProductPickerHeader extends React.Component { aria-label="Filtered layer search" > diff --git a/web/js/components/layer/product-picker/search/product-facet.js b/web/js/components/layer/product-picker/search/product-facet.js index 6af91616c4..cc7634dc44 100644 --- a/web/js/components/layer/product-picker/search/product-facet.js +++ b/web/js/components/layer/product-picker/search/product-facet.js @@ -27,6 +27,7 @@ function ProductFacet(props) { const renderHeaderIcons = () => ( <> {

Granule Count

diff --git a/web/js/components/location-search/coordinates-dialog.js b/web/js/components/location-search/coordinates-dialog.js index dd64dcb1fb..ec47352165 100644 --- a/web/js/components/location-search/coordinates-dialog.js +++ b/web/js/components/location-search/coordinates-dialog.js @@ -108,6 +108,7 @@ class CoordinatesDialog extends Component { {tooltipVisibilityCondition && ( {tooltipVisibilityCondition && ( {tooltipVisibilityCondition && ( {tooltipVisibilityCondition && ( {latitude && latitude && ( - + Change coordinates format )} diff --git a/web/js/components/measure-tool/measure-button.js b/web/js/components/measure-tool/measure-button.js index 2a7f3a96d2..026d5ddb00 100644 --- a/web/js/components/measure-tool/measure-button.js +++ b/web/js/components/measure-tool/measure-button.js @@ -92,6 +92,7 @@ class MeasureButton extends React.Component { style={mobileMeasureButtonStyle} > diff --git a/web/js/components/sidebar/collapsed-button.js b/web/js/components/sidebar/collapsed-button.js index 3d7a47d208..b0a04b085d 100644 --- a/web/js/components/sidebar/collapsed-button.js +++ b/web/js/components/sidebar/collapsed-button.js @@ -24,7 +24,7 @@ class CollapsedButton extends PureComponent { aria-label={labelText} onClick={onclick} > - + {labelText} diff --git a/web/js/components/sidebar/event-icon.js b/web/js/components/sidebar/event-icon.js index 005a367a9d..32fac28b0f 100644 --- a/web/js/components/sidebar/event-icon.js +++ b/web/js/components/sidebar/event-icon.js @@ -15,6 +15,7 @@ export default function EventIcon ({ return ( <> @@ -158,6 +159,7 @@ function EventFilterModalBody (props) { /> diff --git a/web/js/components/sidebar/nav/nav-case.js b/web/js/components/sidebar/nav/nav-case.js index ad416d4e25..d86df3f41b 100644 --- a/web/js/components/sidebar/nav/nav-case.js +++ b/web/js/components/sidebar/nav/nav-case.js @@ -110,7 +110,7 @@ function NavCase (props) { aria-label="Hide sidebar" style={collapseIconMobile} /> - + Hide sidebar
diff --git a/web/js/components/sidebar/paletteLegend.js b/web/js/components/sidebar/paletteLegend.js index 8679f69c33..d6b12723a4 100644 --- a/web/js/components/sidebar/paletteLegend.js +++ b/web/js/components/sidebar/paletteLegend.js @@ -384,6 +384,7 @@ class PaletteLegend extends React.Component { {isVisible && ( { {isMobile ? null : ( diff --git a/web/js/components/timeline/timeline-coverage/timeline-coverage.js b/web/js/components/timeline/timeline-coverage/timeline-coverage.js index e2ec93f714..770d1eb559 100644 --- a/web/js/components/timeline/timeline-coverage/timeline-coverage.js +++ b/web/js/components/timeline/timeline-coverage/timeline-coverage.js @@ -380,7 +380,7 @@ class TimelineLayerCoveragePanel extends Component { onClick={this.togglePanelOpenClose} style={panelHandleStyle} > - + {panelToggleLabelText}
diff --git a/web/js/components/toolbar/share/tooltips.js b/web/js/components/toolbar/share/tooltips.js index 2ae819e2be..47d6e9046a 100644 --- a/web/js/components/toolbar/share/tooltips.js +++ b/web/js/components/toolbar/share/tooltips.js @@ -64,6 +64,7 @@ class ShareToolTips extends PureComponent { return ( <> { return !isMobile && ( { <>
onRemoveClick(layer.id)} > - + {removeLayerBtnTitle} @@ -233,7 +237,7 @@ function LayerRow (props) { onMouseDown={stopPropagation} onClick={() => onOptionsClick(layer, title)} > - + {layerOptionsBtnTitle} @@ -245,7 +249,7 @@ function LayerRow (props) { onMouseDown={stopPropagation} onClick={() => onInfoClick(layer, title, measurementDescriptionPath)} > - + {layerInfoBtnTitle} @@ -269,7 +273,7 @@ function LayerRow (props) { onMouseDown={stopPropagation} onClick={openVectorAlertModal} > - + {title} @@ -328,6 +332,8 @@ function LayerRow (props) { ? ['far', 'eye-slash'] : ['far', 'eye']; + const collectionClass = collections?.type === 'NRT' ? 'collection-title badge badge-pill badge-secondary' : 'collection-title badge badge-pill badge-light'; + const renderLayerRow = () => ( <> {!isEmbedModeActive && ( @@ -339,6 +345,7 @@ function LayerRow (props) { > {!isAnimating && ( @@ -357,7 +364,21 @@ function LayerRow (props) { {showButtons && renderControls()}

{names.title}

-

+

+

+ + {collections && isVisible ? ( +

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