diff --git a/config/default/common/config/wv.json/layers/activate/ACTIVATE_HU-25_Falcon_Ozone.json b/config/default/common/config/wv.json/layers/activate/ACTIVATE_HU-25_Falcon_Ozone.json index 6bd45cae1a..0c8e0f1b4c 100644 --- a/config/default/common/config/wv.json/layers/activate/ACTIVATE_HU-25_Falcon_Ozone.json +++ b/config/default/common/config/wv.json/layers/activate/ACTIVATE_HU-25_Falcon_Ozone.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Ozone", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/activate/ACTIVATE_King_Air_Flight_Track.json b/config/default/common/config/wv.json/layers/activate/ACTIVATE_King_Air_Flight_Track.json index 16037b1292..7aa5a9df62 100644 --- a/config/default/common/config/wv.json/layers/activate/ACTIVATE_King_Air_Flight_Track.json +++ b/config/default/common/config/wv.json/layers/activate/ACTIVATE_King_Air_Flight_Track.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Suborbital Flight Tracks", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/aeronet/AERONET_ANGSTROM_440-870NM.json b/config/default/common/config/wv.json/layers/aeronet/AERONET_ANGSTROM_440-870NM.json index 3108dde3c8..b0966ee272 100644 --- a/config/default/common/config/wv.json/layers/aeronet/AERONET_ANGSTROM_440-870NM.json +++ b/config/default/common/config/wv.json/layers/aeronet/AERONET_ANGSTROM_440-870NM.json @@ -10,7 +10,7 @@ "tags": "vectors aeronet angstrom", "type": "vector", "layergroup": "Aerosol Optical Depth", - "disableSnapshot": true, + "disableGIF": true, "vectorStyle": { "id": "AERONET_ANGSTROM_440-870NM" }, diff --git a/config/default/common/config/wv.json/layers/aeronet/AERONET_AOD_500NM.json b/config/default/common/config/wv.json/layers/aeronet/AERONET_AOD_500NM.json index 44a4ac65be..47a3dbaf02 100644 --- a/config/default/common/config/wv.json/layers/aeronet/AERONET_AOD_500NM.json +++ b/config/default/common/config/wv.json/layers/aeronet/AERONET_AOD_500NM.json @@ -10,7 +10,7 @@ "tags": "vectors aeronet aod", "type": "vector", "layergroup": "Aerosol Optical Depth", - "disableSnapshot": true, + "disableGIF": true, "vectorStyle": { "id": "AERONET_AOD_500NM" }, diff --git a/config/default/common/config/wv.json/layers/aeronet/DAILY_AERONET_ANGSTROM_440-870NM.json b/config/default/common/config/wv.json/layers/aeronet/DAILY_AERONET_ANGSTROM_440-870NM.json index 090a5f0f6c..0fd645ee94 100644 --- a/config/default/common/config/wv.json/layers/aeronet/DAILY_AERONET_ANGSTROM_440-870NM.json +++ b/config/default/common/config/wv.json/layers/aeronet/DAILY_AERONET_ANGSTROM_440-870NM.json @@ -10,7 +10,7 @@ "tags": "vectors aeronet angstrom", "type": "vector", "layergroup": "Aerosol Optical Depth", - "disableSnapshot": true, + "disableGIF": true, "vectorStyle": { "id": "DAILY_AERONET_ANGSTROM_440-870NM" }, diff --git a/config/default/common/config/wv.json/layers/aeronet/DAILY_AERONET_AOD_500NM.json b/config/default/common/config/wv.json/layers/aeronet/DAILY_AERONET_AOD_500NM.json index 4c055d2055..a19d074b99 100644 --- a/config/default/common/config/wv.json/layers/aeronet/DAILY_AERONET_AOD_500NM.json +++ b/config/default/common/config/wv.json/layers/aeronet/DAILY_AERONET_AOD_500NM.json @@ -10,7 +10,7 @@ "tags": "vectors aeronet aod", "type": "vector", "layergroup": "Aerosol Optical Depth", - "disableSnapshot": true, + "disableGIF": true, "vectorStyle": { "id": "DAILY_AERONET_AOD_500NM" }, diff --git a/config/default/common/config/wv.json/layers/ajax/AJAX_Alpha_Jet_Ozone.json b/config/default/common/config/wv.json/layers/ajax/AJAX_Alpha_Jet_Ozone.json index 057d3d5ee5..d06df903d8 100644 --- a/config/default/common/config/wv.json/layers/ajax/AJAX_Alpha_Jet_Ozone.json +++ b/config/default/common/config/wv.json/layers/ajax/AJAX_Alpha_Jet_Ozone.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Ozone", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/arcsix/ARCSIX_G-III_Flight_Track.json b/config/default/common/config/wv.json/layers/arcsix/ARCSIX_G-III_Flight_Track.json index 8a81f2581d..3be4aa31b5 100644 --- a/config/default/common/config/wv.json/layers/arcsix/ARCSIX_G-III_Flight_Track.json +++ b/config/default/common/config/wv.json/layers/arcsix/ARCSIX_G-III_Flight_Track.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Suborbital Flight Tracks", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/arcsix/ARCSIX_P-3B_Flight_Track.json b/config/default/common/config/wv.json/layers/arcsix/ARCSIX_P-3B_Flight_Track.json index e21578e58d..abd1580036 100644 --- a/config/default/common/config/wv.json/layers/arcsix/ARCSIX_P-3B_Flight_Track.json +++ b/config/default/common/config/wv.json/layers/arcsix/ARCSIX_P-3B_Flight_Track.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Suborbital Flight Tracks", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/arcsix/ARCSIX_SPEC-Learjet_Flight_Track.json b/config/default/common/config/wv.json/layers/arcsix/ARCSIX_SPEC-Learjet_Flight_Track.json index 10d77b8bfb..70261d93dc 100644 --- a/config/default/common/config/wv.json/layers/arcsix/ARCSIX_SPEC-Learjet_Flight_Track.json +++ b/config/default/common/config/wv.json/layers/arcsix/ARCSIX_SPEC-Learjet_Flight_Track.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Suborbital Flight Tracks", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/asia-aq/ASIA-AQ_DC-8_NO2.json b/config/default/common/config/wv.json/layers/asia-aq/ASIA-AQ_DC-8_NO2.json index 28a8958b54..1c56d0a312 100644 --- a/config/default/common/config/wv.json/layers/asia-aq/ASIA-AQ_DC-8_NO2.json +++ b/config/default/common/config/wv.json/layers/asia-aq/ASIA-AQ_DC-8_NO2.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Nitrogen Dioxide", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/asia-aq/ASIA-AQ_G-III_Flight_Track.json b/config/default/common/config/wv.json/layers/asia-aq/ASIA-AQ_G-III_Flight_Track.json index cc6e721e46..78319d937f 100644 --- a/config/default/common/config/wv.json/layers/asia-aq/ASIA-AQ_G-III_Flight_Track.json +++ b/config/default/common/config/wv.json/layers/asia-aq/ASIA-AQ_G-III_Flight_Track.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Suborbital Flight Tracks", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/dcotss/DCOTSS_ER-2_ROZE_Ozone.json b/config/default/common/config/wv.json/layers/dcotss/DCOTSS_ER-2_ROZE_Ozone.json index c0d4876e63..83f4ae859c 100644 --- a/config/default/common/config/wv.json/layers/dcotss/DCOTSS_ER-2_ROZE_Ozone.json +++ b/config/default/common/config/wv.json/layers/dcotss/DCOTSS_ER-2_ROZE_Ozone.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Ozone", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/dcotss/DCOTSS_ER-2_UCATS_Ozone.json b/config/default/common/config/wv.json/layers/dcotss/DCOTSS_ER-2_UCATS_Ozone.json index d37bda0bf2..b8caf7a5d5 100644 --- a/config/default/common/config/wv.json/layers/dcotss/DCOTSS_ER-2_UCATS_Ozone.json +++ b/config/default/common/config/wv.json/layers/dcotss/DCOTSS_ER-2_UCATS_Ozone.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Ozone", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Airmass_RGB.json b/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Airmass_RGB.json index d5c82e48a6..b2aea5f0b9 100644 --- a/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Airmass_RGB.json +++ b/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Airmass_RGB.json @@ -10,7 +10,7 @@ "type": "wms", "format": "image/png", "wrapX": true, - "disableSnapshot": true, + "disableGIF": true, "period": "subdaily", "layergroup": "Geostationary", "startDate": "2021-02-06T00:00:00Z", diff --git a/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Ash_RGB.json b/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Ash_RGB.json index 3969f762b4..449ff9966c 100644 --- a/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Ash_RGB.json +++ b/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Ash_RGB.json @@ -10,7 +10,7 @@ "type": "wms", "format": "image/png", "wrapX": true, - "disableSnapshot": true, + "disableGIF": true, "period": "subdaily", "layergroup": "Geostationary", "startDate": "2021-02-06T00:00:00Z", diff --git a/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Dust_RGB.json b/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Dust_RGB.json index ad0df19004..b167a90161 100644 --- a/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Dust_RGB.json +++ b/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Dust_RGB.json @@ -10,7 +10,7 @@ "type": "wms", "format": "image/png", "wrapX": true, - "disableSnapshot": true, + "disableGIF": true, "period": "subdaily", "layergroup": "Geostationary", "startDate": "2021-02-06T00:00:00Z", diff --git a/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_IR108.json b/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_IR108.json index 9402dfed75..b522226442 100644 --- a/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_IR108.json +++ b/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_IR108.json @@ -10,7 +10,7 @@ "type": "wms", "format": "image/png", "wrapX": true, - "disableSnapshot": true, + "disableGIF": true, "period": "subdaily", "layergroup": "Geostationary", "startDate": "2021-02-06T00:00:00Z", diff --git a/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Natural_Color_RGB.json b/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Natural_Color_RGB.json index e950778734..19e02ac48c 100644 --- a/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Natural_Color_RGB.json +++ b/config/default/common/config/wv.json/layers/eumetsat/EUMETSAT_RING_Natural_Color_RGB.json @@ -10,7 +10,7 @@ "type": "wms", "format": "image/png", "wrapX": true, - "disableSnapshot": true, + "disableGIF": true, "period": "subdaily", "layergroup": "Geostationary", "startDate": "2021-02-06T00:00:00Z", diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Customizable_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Customizable_Landsat.json index 31b73c4c52..4a88bd4ea6 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Customizable_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Customizable_Landsat.json @@ -22,7 +22,7 @@ "color_formula": "Gamma RGB 2.5 Saturation 1.2 Sigmoidal RGB 10 0.35", "bands_regex": "B[0-9][0-9]" }, - "disableSnapshot": true, + "disableGIF": true, "wrapX": false, "minZoom": 7, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Customizable_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Customizable_Sentinel.json index 9a86baa634..7480986143 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Customizable_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Customizable_Sentinel.json @@ -23,7 +23,7 @@ "bands_regex": "B[0-9][0-9A-Za-z]" }, "wrapX": false, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "projections": { "geographic": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_EVI_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_EVI_Landsat.json index dbf143e766..4a16752a2c 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_EVI_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_EVI_Landsat.json @@ -22,7 +22,7 @@ "colormap_name": "brbg", "bands_regex": "B[0-9][0-9]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_EVI_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_EVI_Sentinel.json index ff7276257d..2ae2696496 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_EVI_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_EVI_Sentinel.json @@ -22,7 +22,7 @@ "colormap_name": "brbg", "bands_regex": "B[0-9][0-9A-Za-z]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Landsat.json index 8a5cb259f4..08c6415878 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Landsat.json @@ -23,7 +23,7 @@ "bands_regex": "B[0-9][0-9]" }, "wrapX": false, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "projections": { "geographic": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Sentinel.json index 292dd0f902..86afe47648 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Sentinel.json @@ -23,7 +23,7 @@ "bands_regex": "B[0-9][0-9A-Za-z]" }, "wrapX": false, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "projections": { "geographic": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Urban_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Urban_Landsat.json index 9842faf423..3d90ac90fa 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Urban_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Urban_Landsat.json @@ -23,7 +23,7 @@ "bands_regex": "B[0-9][0-9]" }, "wrapX": false, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "projections": { "geographic": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Urban_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Urban_Sentinel.json index 2f2d87a997..ff1d5031d3 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Urban_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Urban_Sentinel.json @@ -23,7 +23,7 @@ "bands_regex": "B[0-9][0-9A-Za-z]" }, "wrapX": false, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "projections": { "geographic": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Vegetation_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Vegetation_Landsat.json index bf42184683..4686968464 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Vegetation_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Vegetation_Landsat.json @@ -23,7 +23,7 @@ "bands_regex": "B[0-9][0-9]" }, "wrapX": false, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "projections": { "geographic": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Vegetation_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Vegetation_Sentinel.json index 6f619319b3..60830cfcd9 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Vegetation_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_False_Color_Vegetation_Sentinel.json @@ -23,7 +23,7 @@ "bands_regex": "B[0-9][0-9A-Za-z]" }, "wrapX": false, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "projections": { "geographic": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_MGRS_Granule_Grid.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_MGRS_Granule_Grid.json index 6a12b10fdc..ef795b313e 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_MGRS_Granule_Grid.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_MGRS_Granule_Grid.json @@ -8,7 +8,7 @@ "layergroup": "Reference", "type": "vector", "wrapX": true, - "disableSnapshot": true + "disableGIF": true } } } \ No newline at end of file diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_MSAVI_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_MSAVI_Landsat.json index ab05b2aaa1..988ef847b8 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_MSAVI_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_MSAVI_Landsat.json @@ -22,7 +22,7 @@ "colormap_name": "brbg", "bands_regex": "B[0-9][0-9]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_MSAVI_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_MSAVI_Sentinel.json index 06cd77de49..1ae1550b0c 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_MSAVI_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_MSAVI_Sentinel.json @@ -22,7 +22,7 @@ "colormap_name": "brbg", "bands_regex": "B[0-9][0-9A-Za-z]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Moisture_Index_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Moisture_Index_Landsat.json index e38b0564d8..2523ab42b3 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Moisture_Index_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Moisture_Index_Landsat.json @@ -22,7 +22,7 @@ "colormap_name": "bwr_r", "bands_regex": "B[0-9][0-9]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Moisture_Index_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Moisture_Index_Sentinel.json index 7794b6366d..fe43378be9 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Moisture_Index_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Moisture_Index_Sentinel.json @@ -22,7 +22,7 @@ "colormap_name": "bwr_r", "bands_regex": "B[0-9][0-9A-Za-z]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR2_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR2_Landsat.json index 933cf8ac4e..ebe8cc4a4b 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR2_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR2_Landsat.json @@ -22,7 +22,7 @@ "colormap_name": "puor", "bands_regex": "B[0-9][0-9]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR2_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR2_Sentinel.json index d6020a3c80..559246f43e 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR2_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR2_Sentinel.json @@ -22,7 +22,7 @@ "colormap_name": "puor", "bands_regex": "B[0-9][0-9A-Za-z]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR_Landsat.json index 6b4ae1bb09..25bc1a5d41 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR_Landsat.json @@ -22,7 +22,7 @@ "colormap_name": "puor", "bands_regex": "B[0-9][0-9]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR_Sentinel.json index 4437d7e7cd..e4cccfd713 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NBR_Sentinel.json @@ -22,7 +22,7 @@ "colormap_name": "puor", "bands_regex": "B[0-9][0-9A-Za-z]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDSI_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDSI_Landsat.json index 6db4643aac..f7a45baed4 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDSI_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDSI_Landsat.json @@ -22,7 +22,7 @@ "colormap_name": "winter_r", "bands_regex": "B[0-9][0-9]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDSI_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDSI_Sentinel.json index 99d14e9d15..6fe1ee7758 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDSI_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDSI_Sentinel.json @@ -22,7 +22,7 @@ "colormap_name": "winter_r", "bands_regex": "B[0-9][0-9A-Za-z]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDVI_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDVI_Landsat.json index 6c12a07207..0aeb3990f7 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDVI_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDVI_Landsat.json @@ -22,7 +22,7 @@ "colormap_name": "brbg", "bands_regex": "B[0-9][0-9]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDVI_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDVI_Sentinel.json index a07c93e66f..1dc9c3af78 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDVI_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDVI_Sentinel.json @@ -22,7 +22,7 @@ "colormap_name": "brbg", "bands_regex": "B[0-9][0-9A-Za-z]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDWI_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDWI_Landsat.json index 45b1d7fcd2..9fb075a31e 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDWI_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDWI_Landsat.json @@ -22,7 +22,7 @@ "colormap_name": "gnbu", "bands_regex": "B[0-9][0-9]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDWI_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDWI_Sentinel.json index fb92b63154..afc3c61d8c 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDWI_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_NDWI_Sentinel.json @@ -22,7 +22,7 @@ "colormap_name": "gnbu", "bands_regex": "B[0-9][0-9A-Za-z]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_SAVI_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_SAVI_Landsat.json index 6d76d97931..811e45b698 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_SAVI_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_SAVI_Landsat.json @@ -22,7 +22,7 @@ "colormap_name": "brbg", "bands_regex": "B[0-9][0-9]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_SAVI_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_SAVI_Sentinel.json index df1d9c9ce4..83be022605 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_SAVI_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_SAVI_Sentinel.json @@ -22,7 +22,7 @@ "colormap_name": "brbg", "bands_regex": "B[0-9][0-9A-Za-z]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Shortwave_Infrared_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Shortwave_Infrared_Landsat.json index 04719a113f..65cd14b718 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Shortwave_Infrared_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Shortwave_Infrared_Landsat.json @@ -23,7 +23,7 @@ "bands_regex": "B[0-9][0-9]" }, "wrapX": false, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "projections": { "geographic": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Shortwave_Infrared_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Shortwave_Infrared_Sentinel.json index 5c4e8e6d4e..df7ff529a4 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Shortwave_Infrared_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_Shortwave_Infrared_Sentinel.json @@ -23,7 +23,7 @@ "bands_regex": "B[0-9][0-9A-Za-z]" }, "wrapX": false, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "projections": { "geographic": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_TVI_Landsat.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_TVI_Landsat.json index c65fab35b4..eb7e12bb4d 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_TVI_Landsat.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_TVI_Landsat.json @@ -22,7 +22,7 @@ "colormap_name": "brbg", "bands_regex": "B[0-9][0-9]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_TVI_Sentinel.json b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_TVI_Sentinel.json index 5498e2decb..461f03a8a8 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_TVI_Sentinel.json +++ b/config/default/common/config/wv.json/layers/multi-mission/hls/HLS_TVI_Sentinel.json @@ -22,7 +22,7 @@ "colormap_name": "brbg", "bands_regex": "B[0-9][0-9A-Za-z]" }, - "disableSnapshot": true, + "disableGIF": true, "minZoom": 7, "wrapX": false, "projections": { diff --git a/config/default/common/config/wv.json/layers/multi-mission/merged/NOAA_2025_ERI_WMTS.json b/config/default/common/config/wv.json/layers/multi-mission/merged/NOAA_2025_ERI_WMTS.json index d67d90060d..51a147b6d4 100644 --- a/config/default/common/config/wv.json/layers/multi-mission/merged/NOAA_2025_ERI_WMTS.json +++ b/config/default/common/config/wv.json/layers/multi-mission/merged/NOAA_2025_ERI_WMTS.json @@ -34,7 +34,7 @@ ["2025-01-20", "2025-01-21"], ["2025-01-28", "2025-01-29"] ], - "disableSnapshot": true, + "disableGIF": true, "layergroup": "Corrected Reflectance", "wrapX": true, "projections": { diff --git a/config/default/common/config/wv.json/layers/pace-pax/PACE-PAX_CIRPAS-TO_Flight_Track.json b/config/default/common/config/wv.json/layers/pace-pax/PACE-PAX_CIRPAS-TO_Flight_Track.json index 2dbac52739..377a2fec23 100644 --- a/config/default/common/config/wv.json/layers/pace-pax/PACE-PAX_CIRPAS-TO_Flight_Track.json +++ b/config/default/common/config/wv.json/layers/pace-pax/PACE-PAX_CIRPAS-TO_Flight_Track.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Suborbital Flight Tracks", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/pace-pax/PACE-PAX_ER-2_Flight_Track.json b/config/default/common/config/wv.json/layers/pace-pax/PACE-PAX_ER-2_Flight_Track.json index c524fdc88f..305f9fd9b7 100644 --- a/config/default/common/config/wv.json/layers/pace-pax/PACE-PAX_ER-2_Flight_Track.json +++ b/config/default/common/config/wv.json/layers/pace-pax/PACE-PAX_ER-2_Flight_Track.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Suborbital Flight Tracks", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/pace-pax/PACE-PAX_RV-Shearwater_Ship_Track.json b/config/default/common/config/wv.json/layers/pace-pax/PACE-PAX_RV-Shearwater_Ship_Track.json index e2378cba91..ada4064461 100644 --- a/config/default/common/config/wv.json/layers/pace-pax/PACE-PAX_RV-Shearwater_Ship_Track.json +++ b/config/default/common/config/wv.json/layers/pace-pax/PACE-PAX_RV-Shearwater_Ship_Track.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Suborbital Ship Tracks", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/reference/Reference_Labels_15m.json b/config/default/common/config/wv.json/layers/reference/Reference_Labels_15m.json index 3b303fd502..831553ed60 100644 --- a/config/default/common/config/wv.json/layers/reference/Reference_Labels_15m.json +++ b/config/default/common/config/wv.json/layers/reference/Reference_Labels_15m.json @@ -10,7 +10,7 @@ "tags": "esri world basemap gcs v2, country, countries, state, province, cities, city, towns, town, lakes, lake, parks, park, airports, airport", "format": "application/x-protobuf", "type": "indexedVector", - "disableSnapshot": true, + "disableGIF": true, "layergroup": "Reference", "wrapX": true, "noTransition": true, diff --git a/config/default/common/config/wv.json/layers/reference/esri/World_Imagery.json b/config/default/common/config/wv.json/layers/reference/esri/World_Imagery.json index 2452a9b7d5..c440e46d78 100644 --- a/config/default/common/config/wv.json/layers/reference/esri/World_Imagery.json +++ b/config/default/common/config/wv.json/layers/reference/esri/World_Imagery.json @@ -9,8 +9,9 @@ "group": "baselayers", "layergroup": "Reference", "format": "image/png", - "type": "esriMapServer", "disableSnapshot": true, + "disableGIF": true, + "type": "esriMapServer", "wrapX": false, "projections": { "geographic": { diff --git a/config/default/common/config/wv.json/layers/staqs/STAQS_G-III_Flight_Track.json b/config/default/common/config/wv.json/layers/staqs/STAQS_G-III_Flight_Track.json index 53cb187d1b..e61e01326d 100644 --- a/config/default/common/config/wv.json/layers/staqs/STAQS_G-III_Flight_Track.json +++ b/config/default/common/config/wv.json/layers/staqs/STAQS_G-III_Flight_Track.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Suborbital Flight Tracks", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/staqs/STAQS_G-V_Flight_Track.json b/config/default/common/config/wv.json/layers/staqs/STAQS_G-V_Flight_Track.json index 26d4c037a0..f33e7d9e06 100644 --- a/config/default/common/config/wv.json/layers/staqs/STAQS_G-V_Flight_Track.json +++ b/config/default/common/config/wv.json/layers/staqs/STAQS_G-V_Flight_Track.json @@ -11,7 +11,7 @@ "ongoing": false, "dataAvailability": "dd", "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "disableCustomPalettes": true, "layergroup": "Suborbital Flight Tracks", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Cloud_Cloud_Fraction_Total_Granule.json b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Cloud_Cloud_Fraction_Total_Granule.json index 828379e66e..1deb7c1ebd 100644 --- a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Cloud_Cloud_Fraction_Total_Granule.json +++ b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Cloud_Cloud_Fraction_Total_Granule.json @@ -13,7 +13,7 @@ "count": 1, "dataAvailability": "dd", "startDate": "2023-08-02T15:12:49Z", - "disableSnapshot": true, + "disableGIF": true, "shiftadjacentdays": false } } diff --git a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Cloud_Cloud_Pressure_Total_Granule.json b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Cloud_Cloud_Pressure_Total_Granule.json index 3668a9c4ae..dba0c27a28 100644 --- a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Cloud_Cloud_Pressure_Total_Granule.json +++ b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Cloud_Cloud_Pressure_Total_Granule.json @@ -13,7 +13,7 @@ "count": 1, "dataAvailability": "dd", "startDate": "2023-08-02T15:12:49Z", - "disableSnapshot": true, + "disableGIF": true, "shiftadjacentdays": false } } diff --git a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Formaldehyde_Vertical_Column_Granule.json b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Formaldehyde_Vertical_Column_Granule.json index c389aada7e..3e8a0d8903 100644 --- a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Formaldehyde_Vertical_Column_Granule.json +++ b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Formaldehyde_Vertical_Column_Granule.json @@ -13,7 +13,7 @@ "count": 1, "dataAvailability": "dd", "startDate": "2023-08-02T15:12:49Z", - "disableSnapshot": true, + "disableGIF": true, "shiftadjacentdays": false } } diff --git a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_NO2_Vertical_Column_Stratosphere_Granule.json b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_NO2_Vertical_Column_Stratosphere_Granule.json index 910e0a0cd8..89cdeba74b 100644 --- a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_NO2_Vertical_Column_Stratosphere_Granule.json +++ b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_NO2_Vertical_Column_Stratosphere_Granule.json @@ -13,7 +13,7 @@ "count": 1, "dataAvailability": "dd", "startDate": "2023-08-02T15:12:49Z", - "disableSnapshot": true, + "disableGIF": true, "shiftadjacentdays": false } } diff --git a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_NO2_Vertical_Column_Troposphere_Granule.json b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_NO2_Vertical_Column_Troposphere_Granule.json index 28d34e9ab1..7deb80d33a 100644 --- a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_NO2_Vertical_Column_Troposphere_Granule.json +++ b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_NO2_Vertical_Column_Troposphere_Granule.json @@ -13,7 +13,7 @@ "count": 1, "dataAvailability": "dd", "startDate": "2023-08-02T15:12:49Z", - "disableSnapshot": true, + "disableGIF": true, "shiftadjacentdays": false } } diff --git a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Cloud_Fraction_Granule.json b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Cloud_Fraction_Granule.json index 14c1e09519..6bc05b30d4 100644 --- a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Cloud_Fraction_Granule.json +++ b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Cloud_Fraction_Granule.json @@ -13,7 +13,7 @@ "count": 1, "dataAvailability": "dd", "startDate": "2023-08-02T15:12:49Z", - "disableSnapshot": true, + "disableGIF": true, "shiftadjacentdays": false } } diff --git a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Column_Amount_Granule.json b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Column_Amount_Granule.json index b01409b6e5..045bc0a1d8 100644 --- a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Column_Amount_Granule.json +++ b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Column_Amount_Granule.json @@ -13,7 +13,7 @@ "count": 1, "dataAvailability": "dd", "startDate": "2023-08-02T15:12:49Z", - "disableSnapshot": true, + "disableGIF": true, "shiftadjacentdays": false } } diff --git a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Profile_Total_Column_Granule.json b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Profile_Total_Column_Granule.json index 789e1391ab..e00a927506 100644 --- a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Profile_Total_Column_Granule.json +++ b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Profile_Total_Column_Granule.json @@ -12,7 +12,7 @@ "period": "subdaily", "count": 1, "dataAvailability": "dd", - "disableSnapshot": true, + "disableGIF": true, "shiftadjacentdays": false } } diff --git a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Profile_Troposphere_Column_Granule.json b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Profile_Troposphere_Column_Granule.json index 1c31cdaf94..add1b0dc8b 100644 --- a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Profile_Troposphere_Column_Granule.json +++ b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_Profile_Troposphere_Column_Granule.json @@ -12,7 +12,7 @@ "period": "subdaily", "count": 1, "dataAvailability": "dd", - "disableSnapshot": true, + "disableGIF": true, "shiftadjacentdays": false } } diff --git a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_UV_Aerosol_Index_Granule.json b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_UV_Aerosol_Index_Granule.json index ca6b2c5caf..725112a376 100644 --- a/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_UV_Aerosol_Index_Granule.json +++ b/config/default/common/config/wv.json/layers/tempo/TEMPO_L2_Ozone_UV_Aerosol_Index_Granule.json @@ -13,7 +13,7 @@ "count": 1, "dataAvailability": "dd", "startDate": "2023-08-02T15:12:49Z", - "disableSnapshot": true, + "disableGIF": true, "shiftadjacentdays": false } } diff --git a/config/default/common/config/wv.json/layers/tracer/TRACER-AQ_G-V_Flight_Track.json b/config/default/common/config/wv.json/layers/tracer/TRACER-AQ_G-V_Flight_Track.json index 0e5b851a7e..6f326edd83 100644 --- a/config/default/common/config/wv.json/layers/tracer/TRACER-AQ_G-V_Flight_Track.json +++ b/config/default/common/config/wv.json/layers/tracer/TRACER-AQ_G-V_Flight_Track.json @@ -10,7 +10,7 @@ "tags": "vector tracer", "ongoing": false, "disableCharting": true, - "disableSnapshot": true, + "disableGIF": true, "dataAvailability": "dd", "layergroup": "Suborbital Flight Tracks", "vectorStyle": { diff --git a/config/default/common/config/wv.json/layers/wdpa/World_Database_on_Protected_Areas.json b/config/default/common/config/wv.json/layers/wdpa/World_Database_on_Protected_Areas.json index 1323619144..6e2d54d58b 100644 --- a/config/default/common/config/wv.json/layers/wdpa/World_Database_on_Protected_Areas.json +++ b/config/default/common/config/wv.json/layers/wdpa/World_Database_on_Protected_Areas.json @@ -12,6 +12,7 @@ "format": "image/png", "type": "xyz", "disableSnapshot": true, + "disableGIF": true, "layergroup": "Reference", "wrapX": true, "maxZoom": 12, diff --git a/e2e/features/global-unit/coordinate-format-test.spec.js b/e2e/features/global-unit/coordinate-format-test.spec.js index 2abe38e35c..016b3440dd 100644 --- a/e2e/features/global-unit/coordinate-format-test.spec.js +++ b/e2e/features/global-unit/coordinate-format-test.spec.js @@ -28,7 +28,7 @@ test('Open page', async () => { test('Change coordinates format from coordinate case updates global settings coordinate format', async ({ browserName }) => { test.skip(browserName === 'firefox', 'firefox cant find button') - await page.locator('canvas').click() + await page.locator('canvas').nth(1).click() await page.locator('#ol-coords-case').click() await page.getByRole('button', { name: 'Information' }).click() await page.getByRole('button', { name: 'Settings' }).click() diff --git a/e2e/features/image-download/download-2-test.spec.js b/e2e/features/image-download/download-2-test.spec.js new file mode 100644 index 0000000000..1d8c9d4579 --- /dev/null +++ b/e2e/features/image-download/download-2-test.spec.js @@ -0,0 +1,218 @@ +// @ts-check +const { test, expect } = require('@playwright/test') +const { + openImageDownloadPanel, + clickDownload, + closeModal +} = require('../../test-utils/hooks/wvHooks') +const { joinUrl } = require('../../test-utils/hooks/basicHooks') +const fs = require('fs') +const path = require('path') + +/** @type {import('@playwright/test').Page} */ +let page +/** @type {Promise} */ +let downloadPromise + +const downloadDir = 'test-downloads' + +test.beforeAll(() => { + if (!fs.existsSync(downloadDir)) { + fs.mkdirSync(downloadDir) + } +}) + +test.beforeEach(async ({ browser }) => { + page = await browser.newPage() + downloadPromise = page.waitForEvent('download') +}) + +test.afterEach(async () => { + await page.close() +}) + +test.afterAll(() => { + if (fs.existsSync(downloadDir)) { + fs.rmdirSync(downloadDir, { recursive: true }) + } +}) + +test('download KMZ format with larger area', async () => { + test.setTimeout(200_500) + const largeAreaParams = [ + 'l=MODIS_Terra_CorrectedReflectance_TrueColor', + 'v=-10,-10,10,10', // Larger viewport + 't=2018-06-01' + ] + + const url = await joinUrl(largeAreaParams, null) + await page.goto(url) + await closeModal(page) + await openImageDownloadPanel(page) + + // Change format to KMZ + const formatSelect = page.locator('#wv-image-format') + await formatSelect.selectOption('application/vnd.google-earth.kmz') + + // Set resolution to 1km for larger area + const resolutionSelect = page.locator('#wv-image-resolution') + await resolutionSelect.selectOption('1000') + + await clickDownload(page) + await expect(page.locator('.wv-snapshot-progress-dialog')).toBeVisible() + + const download = await downloadPromise + const suggestedFilename = download.suggestedFilename() + await expect(suggestedFilename).toMatch('snapshot-2018-06-01.kmz') + + const filePath = path.join(downloadDir, suggestedFilename) + await download.saveAs(filePath) + + expect(fs.existsSync(filePath)).toBeTruthy() + const fileStats = fs.statSync(filePath) + expect(fileStats.size).toBeGreaterThan(0) + + fs.unlinkSync(filePath) +}) + +test('download with different date and layers', async () => { + test.setTimeout(200_500) + const multiLayerParams = [ + 'l=MODIS_Terra_CorrectedReflectance_TrueColor,MODIS_Aqua_CorrectedReflectance_TrueColor', + 'v=-5,-5,5,5', + 't=2020-01-15' + ] + + const url = await joinUrl(multiLayerParams, null) + await page.goto(url) + await closeModal(page) + await openImageDownloadPanel(page) + + await clickDownload(page) + await expect(page.locator('.wv-snapshot-progress-dialog')).toBeVisible() + + const download = await downloadPromise + const suggestedFilename = download.suggestedFilename() + await expect(suggestedFilename).toMatch('snapshot-2020-01-15.jpeg') + + const filePath = path.join(downloadDir, suggestedFilename) + await download.saveAs(filePath) + + expect(fs.existsSync(filePath)).toBeTruthy() + const fileStats = fs.statSync(filePath) + expect(fileStats.size).toBeGreaterThan(0) + + fs.unlinkSync(filePath) +}) + +test('download Arctic projection with PNG format', async () => { + test.setTimeout(200_500) + const arcticParams = [ + 'p=arctic', + 'l=MODIS_Terra_CorrectedReflectance_TrueColor', + 'v=-1000000,-1000000,1000000,1000000', + 't=2018-06-01' + ] + + const url = await joinUrl(arcticParams, null) + await page.goto(url) + await closeModal(page) + await openImageDownloadPanel(page) + + // Change format to PNG (KMZ not available in polar projections) + const formatSelect = page.locator('#wv-image-format') + await formatSelect.selectOption('image/png') + + // Set appropriate resolution for Arctic + const resolutionSelect = page.locator('#wv-image-resolution') + await resolutionSelect.selectOption('1000') + + await clickDownload(page) + await expect(page.locator('.wv-snapshot-progress-dialog')).toBeVisible() + + const download = await downloadPromise + const suggestedFilename = download.suggestedFilename() + await expect(suggestedFilename).toMatch('snapshot-2018-06-01.png') + + const filePath = path.join(downloadDir, suggestedFilename) + await download.saveAs(filePath) + + expect(fs.existsSync(filePath)).toBeTruthy() + const fileStats = fs.statSync(filePath) + expect(fileStats.size).toBeGreaterThan(0) + + fs.unlinkSync(filePath) +}) + +test('download with high resolution (60m)', async () => { + test.setTimeout(200_500) + const highResParams = [ + 'l=MODIS_Terra_CorrectedReflectance_TrueColor', + 'v=-0.5,-0.5,0.5,0.5', // Small area for high resolution + 't=2018-06-01' + ] + + const url = await joinUrl(highResParams, null) + await page.goto(url) + await closeModal(page) + await openImageDownloadPanel(page) + + // Set high resolution + const resolutionSelect = page.locator('#wv-image-resolution') + await resolutionSelect.selectOption('60') + + await clickDownload(page) + await expect(page.locator('.wv-snapshot-progress-dialog')).toBeVisible() + + const download = await downloadPromise + const suggestedFilename = download.suggestedFilename() + await expect(suggestedFilename).toMatch('snapshot-2018-06-01.jpeg') + + const filePath = path.join(downloadDir, suggestedFilename) + await download.saveAs(filePath) + + expect(fs.existsSync(filePath)).toBeTruthy() + const fileStats = fs.statSync(filePath) + expect(fileStats.size).toBeGreaterThan(0) + + fs.unlinkSync(filePath) +}) + +test('download with Antarctic projection and GeoTIFF', async () => { + test.setTimeout(200_500) + const antarcticParams = [ + 'p=antarctic', + 'l=MODIS_Terra_CorrectedReflectance_TrueColor', + 'v=-1000000,-1000000,1000000,1000000', + 't=2018-06-01' + ] + + const url = await joinUrl(antarcticParams, null) + await page.goto(url) + await closeModal(page) + await openImageDownloadPanel(page) + + // Change format to GeoTIFF + const formatSelect = page.locator('#wv-image-format') + await formatSelect.selectOption('image/tiff') + + // Set resolution to 500m + const resolutionSelect = page.locator('#wv-image-resolution') + await resolutionSelect.selectOption('500') + + await clickDownload(page) + await expect(page.locator('.wv-snapshot-progress-dialog')).toBeVisible() + + const download = await downloadPromise + const suggestedFilename = download.suggestedFilename() + await expect(suggestedFilename).toMatch('snapshot-2018-06-01.tif') + + const filePath = path.join(downloadDir, suggestedFilename) + await download.saveAs(filePath) + + expect(fs.existsSync(filePath)).toBeTruthy() + const fileStats = fs.statSync(filePath) + expect(fileStats.size).toBeGreaterThan(0) + + fs.unlinkSync(filePath) +}) diff --git a/e2e/features/image-download/download-test.spec.js b/e2e/features/image-download/download-test.spec.js new file mode 100644 index 0000000000..028a6cd3c5 --- /dev/null +++ b/e2e/features/image-download/download-test.spec.js @@ -0,0 +1,206 @@ +// @ts-check +const { test, expect } = require('@playwright/test') +const { + openImageDownloadPanel, + clickDownload, + closeModal +} = require('../../test-utils/hooks/wvHooks') +const { joinUrl } = require('../../test-utils/hooks/basicHooks') +const fs = require('fs') +const path = require('path') + +/** @type {import('@playwright/test').Page} */ +let page +/** @type {Promise} */ +let downloadPromise + +const startParams = [ + 'l=MODIS_Terra_CorrectedReflectance_TrueColor', + 'v=-1,-1,1,1', + 't=2018-06-01' +] +const subdailyParams = [ + 'l=TEMPO_L2_Formaldehyde_Vertical_Column_Granule(count=1)', + 'v=-1,-1,1,1', + 't=2024-06-01' +] +const downloadDir = 'test-downloads' + +test.beforeAll(() => { + if (!fs.existsSync(downloadDir)) { + fs.mkdirSync(downloadDir) + } +}) + +test.beforeEach(async ({ browser }) => { + page = await browser.newPage() + downloadPromise = page.waitForEvent('download') +}) + +test.afterEach(async () => { + await page.close() +}) + +test.afterAll(() => { + if (fs.existsSync(downloadDir)) { + fs.rmdirSync(downloadDir, { recursive: true }) + } +}) + +test('download button downloads an image', async () => { + test.setTimeout(200_500) + const url = await joinUrl(startParams, null) + await page.goto(url) + await closeModal(page) + await openImageDownloadPanel(page) + + // Start download and verify progress indicator appears + await clickDownload(page) + await expect(page.locator('.wv-snapshot-progress-dialog')).toBeVisible() + + const download = await downloadPromise + const suggestedFilename = download.suggestedFilename() + await expect(suggestedFilename).toMatch('snapshot-2018-06-01.jpeg') + + const filePath = path.join(downloadDir, suggestedFilename) + + // Save the downloaded file + await download.saveAs(filePath) + + // Assert that the file exists + expect(fs.existsSync(filePath)).toBeTruthy() + + // Check file size is greater than 0 + const fileStats = fs.statSync(filePath) + expect(fileStats.size).toBeGreaterThan(0) + + // Clean up the downloaded file (optional) + fs.unlinkSync(filePath) +}) + +test('download PNG format with different resolution', async () => { + test.setTimeout(200_500) + const url = await joinUrl(startParams, null) + await page.goto(url) + await closeModal(page) + await openImageDownloadPanel(page) + + // Change format to PNG + const formatSelect = page.locator('#wv-image-format') + await formatSelect.selectOption('image/png') + + // Change resolution to 250m + const resolutionSelect = page.locator('#wv-image-resolution') + await resolutionSelect.selectOption('250') + + await clickDownload(page) + await expect(page.locator('.wv-snapshot-progress-dialog')).toBeVisible() + + const download = await downloadPromise + const suggestedFilename = download.suggestedFilename() + await expect(suggestedFilename).toMatch('snapshot-2018-06-01.png') + + const filePath = path.join(downloadDir, suggestedFilename) + await download.saveAs(filePath) + + expect(fs.existsSync(filePath)).toBeTruthy() + const fileStats = fs.statSync(filePath) + expect(fileStats.size).toBeGreaterThan(0) + + fs.unlinkSync(filePath) +}) + +test('download GeoTIFF with worldfile enabled', async () => { + test.setTimeout(200_500) + const url = await joinUrl(startParams, null) + await page.goto(url) + await closeModal(page) + await openImageDownloadPanel(page) + + // Change format to GeoTIFF + const formatSelect = page.locator('#wv-image-format') + await formatSelect.selectOption('image/tiff') + + // Enable worldfile + const worldfileCheckbox = page.locator('#wv-image-worldfile') + await worldfileCheckbox.selectOption('1') + + await clickDownload(page) + await expect(page.locator('.wv-snapshot-progress-dialog')).toBeVisible() + + const download = await downloadPromise + const suggestedFilename = download.suggestedFilename() + await expect(suggestedFilename).toMatch('snapshot-2018-06-01.tif') + + const filePath = path.join(downloadDir, suggestedFilename) + await download.saveAs(filePath) + + expect(fs.existsSync(filePath)).toBeTruthy() + const fileStats = fs.statSync(filePath) + expect(fileStats.size).toBeGreaterThan(0) + + fs.unlinkSync(filePath) +}) + +test('download GeoTIFF with entire globe selected', async () => { + test.setTimeout(200_500) + const url = await joinUrl(startParams, null) + await page.goto(url) + await closeModal(page) + await openImageDownloadPanel(page) + + // Change format to GeoTIFF + const formatSelect = page.locator('#wv-image-format') + await formatSelect.selectOption('image/tiff') + + const resolutionSelect = page.locator('#wv-image-resolution') + await resolutionSelect.selectOption('5000') + + // Select entire globe + const globeCheckbox = page.locator('#image-global-cb') + await globeCheckbox.click() + + await clickDownload(page) + await expect(page.locator('.wv-snapshot-progress-dialog')).toBeVisible() + + const download = await downloadPromise + const suggestedFilename = download.suggestedFilename() + await expect(suggestedFilename).toMatch('snapshot-2018-06-01.tif') + + const filePath = path.join(downloadDir, suggestedFilename) + await download.saveAs(filePath) + + expect(fs.existsSync(filePath)).toBeTruthy() + const fileStats = fs.statSync(filePath) + expect(fileStats.size).toBeGreaterThan(0) + + fs.unlinkSync(filePath) +}) + +test('download PNG format with subdaily layer', async () => { + test.setTimeout(200_500) + const url = await joinUrl(subdailyParams, null) + await page.goto(url) + await closeModal(page) + await openImageDownloadPanel(page) + + // Change format to PNG + const formatSelect = page.locator('#wv-image-format') + await formatSelect.selectOption('image/png') + + await clickDownload(page) + await expect(page.locator('.wv-snapshot-progress-dialog')).toBeVisible() + + const download = await downloadPromise + const suggestedFilename = download.suggestedFilename() + await expect(suggestedFilename).toMatch('snapshot-2024-06-01T00_00_59.000Z.png') + + const filePath = path.join(downloadDir, suggestedFilename) + await download.saveAs(filePath) + + expect(fs.existsSync(filePath)).toBeTruthy() + const fileStats = fs.statSync(filePath) + expect(fileStats.size).toBeGreaterThan(0) + + fs.unlinkSync(filePath) +}) diff --git a/e2e/features/image-download/formats-test.spec.js b/e2e/features/image-download/formats-test.spec.js index 73c7b5c4f9..46c179a95a 100644 --- a/e2e/features/image-download/formats-test.spec.js +++ b/e2e/features/image-download/formats-test.spec.js @@ -8,7 +8,6 @@ const { closeModal } = require('../../test-utils/hooks/wvHooks') const { - getAttribute, joinUrl, selectOption } = require('../../test-utils/hooks/basicHooks') @@ -18,8 +17,7 @@ let page const startParams = [ 'l=MODIS_Terra_CorrectedReflectance_TrueColor', 'v=-1,-1,1,1', - 't=2018-06-01', - 'imageDownload=' + 't=2018-06-01' ] test.describe.configure({ mode: 'serial' }) @@ -37,40 +35,92 @@ test('JPEG is the default', async () => { await page.goto(url) await closeModal(page) await openImageDownloadPanel(page) + + // Verify JPEG is selected as default format + const formatSelect = page.locator('#wv-image-format') + await expect(formatSelect).toHaveValue('image/jpeg') + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - expect(urlAttribute).not.toContain('WORLDFILE') - expect(urlAttribute).toContain('FORMAT=image/jpeg') + await expect(page.locator('.wv-snapshot-progress-dialog')).toBeVisible() + + // Wait for completion or cancel after reasonable time + await Promise.race([ + page.locator('.wv-snapshot-progress-dialog').waitFor({ state: 'hidden', timeout: 30000 }), + page.locator('button:text("Cancel")').click() + ]) + await closeImageDownloadPanel(page) }) test('Add a worldfile', async () => { await openImageDownloadPanel(page) + + // Enable worldfile option await selectOption(page, '#wv-image-worldfile', 1) + const worldfileSelect = page.locator('#wv-image-worldfile') + await expect(worldfileSelect).toHaveValue('1') + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - expect(urlAttribute).toContain('WORLDFILE=true') + await expect(page.locator('.wv-snapshot-progress-dialog')).toBeVisible() + + // Wait for completion or cancel after reasonable time + await Promise.race([ + page.locator('.wv-snapshot-progress-dialog').waitFor({ state: 'hidden', timeout: 30000 }), + page.locator('button:text("Cancel")').click() + ]) + + // Reset worldfile option await selectOption(page, '#wv-image-worldfile', 0) await closeImageDownloadPanel(page) }) test('Select PNG', async () => { await openImageDownloadPanel(page) + + // Select PNG format await selectOption(page, '#wv-image-format', 1) + const formatSelect = page.locator('#wv-image-format') + await expect(formatSelect).toHaveValue('image/png') + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - expect(urlAttribute).toContain('FORMAT=image/png') + await expect(page.locator('.wv-snapshot-progress-dialog')).toBeVisible() + + // Wait for completion or cancel after reasonable time + await Promise.race([ + page.locator('.wv-snapshot-progress-dialog').waitFor({ state: 'hidden', timeout: 30000 }), + page.locator('button:text("Cancel")').click() + ]) + await closeImageDownloadPanel(page) }) -test('Switch to geographic, select KMZ, switch to arctic, is PNG', async () => { +test('Switch to geographic, select KMZ, switch to arctic, is JPEG', async () => { await switchProjections(page, 'geographic') await openImageDownloadPanel(page) + + // Select KMZ format await selectOption(page, '#wv-image-format', 3) + const formatSelect = page.locator('#wv-image-format') + await expect(formatSelect).toHaveValue('application/vnd.google-earth.kmz') + await closeImageDownloadPanel(page) await switchProjections(page, 'arctic') await openImageDownloadPanel(page) + + // Verify format defaulted back to JPEG in arctic projection + const arcticFormatSelect = page.locator('#wv-image-format') + await expect(arcticFormatSelect).toHaveValue('image/jpeg') + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - expect(urlAttribute).toContain('FORMAT=image/jpeg') + await expect(page.locator('.wv-snapshot-progress-dialog')).toBeVisible() + + // Wait for completion or cancel after reasonable time + await Promise.race([ + page.locator('.wv-snapshot-progress-dialog').waitFor({ state: 'hidden', timeout: 30000 }), + page.locator('button:text("Cancel")').click() + ]) }) diff --git a/e2e/features/image-download/global-select-test.spec.js b/e2e/features/image-download/global-select-test.spec.js index 7d9f2ee201..e48028e438 100644 --- a/e2e/features/image-download/global-select-test.spec.js +++ b/e2e/features/image-download/global-select-test.spec.js @@ -12,8 +12,7 @@ const startParams = [ 'p=geographic', 'v=-180,-90,180,90', 'l=MODIS_Terra_CorrectedReflectance_TrueColor', - 't=2018-06-01', - 'imageDownload=' + 't=2018-06-01' ] const defaultCoords = '180.0000' diff --git a/e2e/features/image-download/initial-state-test.spec.js b/e2e/features/image-download/initial-state-test.spec.js index 340423d9bc..f0eb95d54d 100644 --- a/e2e/features/image-download/initial-state-test.spec.js +++ b/e2e/features/image-download/initial-state-test.spec.js @@ -16,7 +16,7 @@ let selectors const expectedResolutions = '30m60m125m250m500m1km5km10km' const expectedFormats = 'JPEGPNGGeoTIFFKMZ' const expectedWorldFile = 'NoYes' -const expectedSize = '8200px x 8200px' +const expectedSize = '16384px x 16384px' const expectedPolarFormats = 'JPEGPNGGeoTIFF' test.describe.configure({ mode: 'serial' }) diff --git a/e2e/features/image-download/layers-test.spec.js b/e2e/features/image-download/layers-test.spec.js index b9e1ace735..a1894978cc 100644 --- a/e2e/features/image-download/layers-test.spec.js +++ b/e2e/features/image-download/layers-test.spec.js @@ -1,20 +1,22 @@ // @ts-check const { test, expect } = require('@playwright/test') const { openImageDownloadPanel, clickDownload, closeModal } = require('../../test-utils/hooks/wvHooks') -const { joinUrl, getAttribute } = require('../../test-utils/hooks/basicHooks') +const { joinUrl } = require('../../test-utils/hooks/basicHooks') /** @type {import('@playwright/test').Page} */ let page +/** @type {Promise} */ +let downloadPromise const startParams = [ - 'v=-180,-90,180,90', - 't=2018-06-01', - 'imageDownload=' + 'v=-1,-1,1,1', + 't=2018-06-01' ] test.describe.configure({ mode: 'serial' }) test.beforeEach(async ({ browser }) => { page = await browser.newPage() + downloadPromise = page.waitForEvent('download') }) test.afterEach(async () => { @@ -26,22 +28,57 @@ test('List layers in draw order', async () => { await page.goto(url) await closeModal(page) await openImageDownloadPanel(page) + + // Verify the layers are loaded by checking they exist in the layer list + const layerItems = page.locator('li.layer-visible') + await expect(layerItems).toHaveCount(3) + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - expect(urlAttribute).toContain('LAYERS=MODIS_Terra_CorrectedReflectance_TrueColor') + + const progressDialog = page.locator('.wv-snapshot-progress-overlay') + await expect(progressDialog).toBeVisible() + + const cancelButton = page.locator('#wv-snapshot-cancel-button') + await expect(cancelButton).toBeVisible() + + // Wait for either the download to start or the progress dialog to disappear (timeout after 20s) + try { + await Promise.race([ + downloadPromise, + progressDialog.waitFor({ state: 'detached', timeout: 200_000 }), + cancelButton.click() + ]) + } catch (e) { + throw new Error('Snapshot download did not complete or progress dialog did not disappear in time') + } }) -test('Move AOD over the reference features', async ({ browserName }) => { - test.skip(browserName === 'firefox', 'firefox fails this test for unknown reasons') +test('Move AOD over the reference features', async () => { const url = await joinUrl(startParams, '&l=MODIS_Terra_CorrectedReflectance_TrueColor,MODIS_Terra_Aerosol,Reference_Features_15m') await page.goto(url) - await page.waitForTimeout(1000) await closeModal(page) - await page.waitForTimeout(1000) await openImageDownloadPanel(page) + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - expect(urlAttribute).toContain('LAYERS=MODIS_Terra_CorrectedReflectance_TrueColor,Reference_Features_15m,MODIS_Terra_Aerosol') + + const progressDialog = page.locator('.wv-snapshot-progress-overlay') + await expect(progressDialog).toBeVisible() + + const cancelButton = page.locator('#wv-snapshot-cancel-button') + await expect(cancelButton).toBeVisible() + + // Wait for either the download to start or the progress dialog to disappear (timeout after 20s) + try { + await Promise.race([ + downloadPromise, + progressDialog.waitFor({ state: 'detached', timeout: 200_000 }), + cancelButton.click() + ]) + } catch (e) { + throw new Error('Snapshot download did not complete or progress dialog did not disappear in time') + } }) test('Do not include obscured layers', async () => { @@ -49,9 +86,30 @@ test('Do not include obscured layers', async () => { await page.goto(url) await closeModal(page) await openImageDownloadPanel(page) + + // Verify layers are loaded + const layerItems = page.locator('li.layer-visible') + await expect(layerItems).toHaveCount(3) + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - expect(urlAttribute).toContain('LAYERS=MODIS_Terra_CorrectedReflectance_TrueColor') + + const progressDialog = page.locator('.wv-snapshot-progress-overlay') + await expect(progressDialog).toBeVisible() + + const cancelButton = page.locator('#wv-snapshot-cancel-button') + await expect(cancelButton).toBeVisible() + + // Wait for either the download to start or the progress dialog to disappear (timeout after 20s) + try { + await Promise.race([ + downloadPromise, + progressDialog.waitFor({ state: 'detached', timeout: 200_000 }), + cancelButton.click() + ]) + } catch (e) { + throw new Error('Snapshot download did not complete or progress dialog did not disappear in time') + } }) test('Multiple base layers when one is semi-transparent', async () => { @@ -59,8 +117,28 @@ test('Multiple base layers when one is semi-transparent', async () => { await page.goto(url) await closeModal(page) await openImageDownloadPanel(page) + + // Verify both layers are loaded + const layerItems = page.locator('li.layer-visible') + await expect(layerItems).toHaveCount(2) + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - expect(urlAttribute).toContain('LAYERS=MODIS_Aqua_CorrectedReflectance_TrueColor,MODIS_Terra_CorrectedReflectance_TrueColor') - expect(urlAttribute).toContain('OPACITIES=,0.5') + + const progressDialog = page.locator('.wv-snapshot-progress-overlay') + await expect(progressDialog).toBeVisible() + + const cancelButton = page.locator('#wv-snapshot-cancel-button') + await expect(cancelButton).toBeVisible() + + // Wait for either the download to start or the progress dialog to disappear (timeout after 20s) + try { + await Promise.race([ + downloadPromise, + progressDialog.waitFor({ state: 'detached', timeout: 200_000 }), + cancelButton.click() + ]) + } catch (e) { + throw new Error('Snapshot download did not complete or progress dialog did not disappear in time') + } }) diff --git a/e2e/features/image-download/projection-test.spec.js b/e2e/features/image-download/projection-test.spec.js index 90429ff010..69586b1721 100644 --- a/e2e/features/image-download/projection-test.spec.js +++ b/e2e/features/image-download/projection-test.spec.js @@ -6,21 +6,25 @@ const { clickDownload, closeModal } = require('../../test-utils/hooks/wvHooks') -const { joinUrl, getAttribute } = require('../../test-utils/hooks/basicHooks') +const { joinUrl } = require('../../test-utils/hooks/basicHooks') const { switchProjections } = require('../../test-utils/hooks/wvHooks') /** @type {import('@playwright/test').Page} */ let page +/** @type {Promise} */ +let downloadPromise + const startParams = [ + 'v=-1,-1,1,1', 'l=MODIS_Terra_CorrectedReflectance_TrueColor', - 't=2018-06-01', - 'imageDownload=' + 't=2018-06-01' ] test.describe.configure({ mode: 'serial' }) test.beforeAll(async ({ browser }) => { page = await browser.newPage() + downloadPromise = page.waitForEvent('download') }) test.afterAll(async () => { @@ -32,26 +36,78 @@ test('Geographic is EPSG:4326', async () => { await page.goto(url) await closeModal(page) await openImageDownloadPanel(page) + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - expect(urlAttribute).toContain('CRS=EPSG:4326') + + const progressDialog = page.locator('.wv-snapshot-progress-overlay') + await expect(progressDialog).toBeVisible() + + const cancelButton = page.locator('#wv-snapshot-cancel-button') + await expect(cancelButton).toBeVisible() + + // Wait for either the download to start or the progress dialog to disappear (timeout after 20s) + try { + await Promise.race([ + downloadPromise, + progressDialog.waitFor({ state: 'detached', timeout: 200_000 }), + cancelButton.click() + ]) + } catch (e) { + throw new Error('Snapshot download did not complete or progress dialog did not disappear in time') + } + await closeImageDownloadPanel(page) }) test('Arctic is EPSG:3413', async () => { await switchProjections(page, 'arctic') await openImageDownloadPanel(page) + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - expect(urlAttribute).toContain('CRS=EPSG:3413') + const progressDialog = page.locator('.wv-snapshot-progress-overlay') + await expect(progressDialog).toBeVisible() + + const cancelButton = page.locator('#wv-snapshot-cancel-button') + await expect(cancelButton).toBeVisible() + + // Wait for either the download to start or the progress dialog to disappear (timeout after 20s) + try { + await Promise.race([ + downloadPromise, + progressDialog.waitFor({ state: 'detached', timeout: 200_000 }), + cancelButton.click() + ]) + } catch (e) { + throw new Error('Snapshot download did not complete or progress dialog did not disappear in time') + } + await closeImageDownloadPanel(page) }) test('Antarctic is EPSG:3031', async () => { await switchProjections(page, 'antarctic') await openImageDownloadPanel(page) + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - expect(urlAttribute).toContain('CRS=EPSG:3031') + + const progressDialog = page.locator('.wv-snapshot-progress-overlay') + await expect(progressDialog).toBeVisible() + + const cancelButton = page.locator('#wv-snapshot-cancel-button') + await expect(cancelButton).toBeVisible() + + // Wait for either the download to start or the progress dialog to disappear (timeout after 20s) + try { + await Promise.race([ + downloadPromise, + progressDialog.waitFor({ state: 'detached', timeout: 200_000 }), + cancelButton.click() + ]) + } catch (e) { + throw new Error('Snapshot download did not complete or progress dialog did not disappear in time') + } await closeImageDownloadPanel(page) }) diff --git a/e2e/features/image-download/resolutions3413-test.spec.js b/e2e/features/image-download/resolutions3413-test.spec.js index e3a8c3c727..c4ae8bbf2b 100644 --- a/e2e/features/image-download/resolutions3413-test.spec.js +++ b/e2e/features/image-download/resolutions3413-test.spec.js @@ -8,7 +8,7 @@ const { zoomIn, closeModal } = require('../../test-utils/hooks/wvHooks') -const { joinUrl, getAttribute } = require('../../test-utils/hooks/basicHooks') +const { joinUrl } = require('../../test-utils/hooks/basicHooks') /** @type {import('@playwright/test').Page} */ let page @@ -18,8 +18,7 @@ const startParams = [ 'p=arctic', 'v=-4194304,-3145728,4194304,3145728', 'l=MODIS_Terra_CorrectedReflectance_TrueColor', - 't=2018-06-01', - 'imageDownload=' + 't=2018-06-01' ] test.describe.configure({ mode: 'serial' }) @@ -39,7 +38,7 @@ test('In the arctic, top zoom levels is 5km', async () => { await page.goto(url) await closeModal(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('20') + await expect(imageResolution).toHaveValue('5000') await closeImageDownloadPanel(page) }) @@ -47,11 +46,11 @@ test('Next two zooms are 1km', async () => { const { imageResolution } = selectors await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('4') + await expect(imageResolution).toHaveValue('1000') await closeImageDownloadPanel(page) await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('4') + await expect(imageResolution).toHaveValue('1000') await closeImageDownloadPanel(page) }) @@ -59,7 +58,7 @@ test('Next zoom is 500m', async () => { const { imageResolution } = selectors await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('2') + await expect(imageResolution).toHaveValue('500') await closeImageDownloadPanel(page) }) @@ -67,11 +66,11 @@ test('Next zoom is 250m', async () => { const { imageResolution } = selectors await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('1') + await expect(imageResolution).toHaveValue('250') await closeImageDownloadPanel(page) await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('1') + await expect(imageResolution).toHaveValue('250') await closeImageDownloadPanel(page) }) @@ -81,28 +80,37 @@ test('Last zoom level is 250m', async () => { await zoomIn(page) } await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('1') + await expect(imageResolution).toHaveValue('250') await closeImageDownloadPanel(page) }) -test('Confirm bounding box integrity', async () => { +test('Confirm bounding box integrity by testing snapshot functionality', async () => { await openImageDownloadPanel(page) + + const downloadPromise = page.waitForEvent('download') + + // Verify the bounding box display is present + const bboxDisplay = page.locator('.wv-image-input-case') + await expect(bboxDisplay).toBeVisible() + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - const matcher = /BBOX=([^,]+),([^,]+),([^,]+),([^&]+)/ - const matches = matcher.exec(urlAttribute) - if (matches !== null) { - const x0 = Number.parseFloat(matches[1]) - const y0 = Number.parseFloat(matches[2]) - const x1 = Number.parseFloat(matches[3]) - const y1 = Number.parseFloat(matches[4]) - expect(x0).toBeLessThan(0) - expect(x0).toBeGreaterThan(-20000) - expect(y0).toBeLessThan(0) - expect(y0).toBeGreaterThan(-20000) - expect(x1).toBeGreaterThan(0) - expect(x1).toBeLessThan(20000) - expect(y1).toBeGreaterThan(0) - expect(y1).toBeLessThan(20000) + const progressDialog = page.locator('.wv-snapshot-progress-overlay') + await expect(progressDialog).toBeVisible() + + const cancelButton = page.locator('#wv-snapshot-cancel-button') + await expect(cancelButton).toBeVisible() + + // Wait for either the download to start or the progress dialog to disappear (timeout after 20s) + try { + await Promise.race([ + downloadPromise, + progressDialog.waitFor({ state: 'detached', timeout: 200_000 }), + cancelButton.click() + ]) + } catch (e) { + throw new Error('Snapshot download did not complete or progress dialog did not disappear in time') } + + await closeImageDownloadPanel(page) }) diff --git a/e2e/features/image-download/resolutions4326-test.spec.js b/e2e/features/image-download/resolutions4326-test.spec.js index 6daf95c2d6..44b1a2c358 100644 --- a/e2e/features/image-download/resolutions4326-test.spec.js +++ b/e2e/features/image-download/resolutions4326-test.spec.js @@ -8,7 +8,7 @@ const { zoomIn, closeModal } = require('../../test-utils/hooks/wvHooks') -const { joinUrl, getAttribute } = require('../../test-utils/hooks/basicHooks') +const { joinUrl } = require('../../test-utils/hooks/basicHooks') /** @type {import('@playwright/test').Page} */ let page @@ -18,8 +18,7 @@ const startParams = [ 'p=geographic', 'v=-180,-90,180,90', 'l=MODIS_Terra_CorrectedReflectance_TrueColor', - 't=2018-06-01', - 'imageDownload=' + 't=2018-06-01' ] test.describe.configure({ mode: 'serial' }) @@ -39,11 +38,11 @@ test('In geographic, top two zoom levels are 10km', async () => { await page.goto(url) await closeModal(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('40') + await expect(imageResolution).toHaveValue('10000') await closeImageDownloadPanel(page) await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('40') + await expect(imageResolution).toHaveValue('10000') await closeImageDownloadPanel(page) }) @@ -51,7 +50,7 @@ test('Next zoom is 5km', async () => { const { imageResolution } = selectors await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('20') + await expect(imageResolution).toHaveValue('5000') await closeImageDownloadPanel(page) }) @@ -59,11 +58,11 @@ test('Next two zooms are 1km', async () => { const { imageResolution } = selectors await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('4') + await expect(imageResolution).toHaveValue('1000') await closeImageDownloadPanel(page) await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('4') + await expect(imageResolution).toHaveValue('1000') await closeImageDownloadPanel(page) }) @@ -71,7 +70,7 @@ test('Next zoom is 500m', async () => { const { imageResolution } = selectors await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('2') + await expect(imageResolution).toHaveValue('500') await closeImageDownloadPanel(page) }) @@ -79,11 +78,11 @@ test('Next two zooms are 250m', async () => { const { imageResolution } = selectors await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('1') + await expect(imageResolution).toHaveValue('250') await closeImageDownloadPanel(page) await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('1') + await expect(imageResolution).toHaveValue('250') await closeImageDownloadPanel(page) }) @@ -91,7 +90,7 @@ test('Next zoom is 125m', async () => { const { imageResolution } = selectors await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('0.5') + await expect(imageResolution).toHaveValue('125') await closeImageDownloadPanel(page) }) @@ -99,7 +98,7 @@ test('Next zoom is 60m', async () => { const { imageResolution } = selectors await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('0.25') + await expect(imageResolution).toHaveValue('60') await closeImageDownloadPanel(page) }) @@ -107,7 +106,7 @@ test('Next zoom is 30m', async () => { const { imageResolution } = selectors await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('0.125') + await expect(imageResolution).toHaveValue('30') await closeImageDownloadPanel(page) }) @@ -115,28 +114,37 @@ test('Last zoom level is 30m', async () => { const { imageResolution } = selectors await zoomIn(page) await openImageDownloadPanel(page) - await expect(imageResolution).toHaveValue('0.125') + await expect(imageResolution).toHaveValue('30') await closeImageDownloadPanel(page) }) -test('Confirm bounding box integrity', async () => { +test('Confirm bounding box integrity by testing snapshot functionality', async () => { await openImageDownloadPanel(page) + + const downloadPromise = page.waitForEvent('download') + + // Verify the bounding box display is present + const bboxDisplay = page.locator('.wv-image-input-case') + await expect(bboxDisplay).toBeVisible() + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - const matcher = /BBOX=([^,]+),([^,]+),([^,]+),([^&]+)/ - const matches = matcher.exec(urlAttribute) - if (matches !== null) { - const x0 = Number.parseFloat(matches[1]) - const y0 = Number.parseFloat(matches[2]) - const x1 = Number.parseFloat(matches[3]) - const y1 = Number.parseFloat(matches[4]) - expect(x0).toBeLessThan(0) - expect(x0).toBeGreaterThan(-20000) - expect(y0).toBeLessThan(0) - expect(y0).toBeGreaterThan(-20000) - expect(x1).toBeGreaterThan(0) - expect(x1).toBeLessThan(20000) - expect(y1).toBeGreaterThan(0) - expect(y1).toBeLessThan(20000) + const progressDialog = page.locator('.wv-snapshot-progress-overlay') + await expect(progressDialog).toBeVisible() + + const cancelButton = page.locator('#wv-snapshot-cancel-button') + await expect(cancelButton).toBeVisible() + + // Wait for either the download to start or the progress dialog to disappear (timeout after 20s) + try { + await Promise.race([ + downloadPromise, + progressDialog.waitFor({ state: 'detached', timeout: 200_000 }), + cancelButton.click() + ]) + } catch (e) { + throw new Error('Snapshot download did not complete or progress dialog did not disappear in time') } + + await closeImageDownloadPanel(page) }) diff --git a/e2e/features/image-download/time-test.spec.js b/e2e/features/image-download/time-test.spec.js index aaee630db8..4051284ca3 100644 --- a/e2e/features/image-download/time-test.spec.js +++ b/e2e/features/image-download/time-test.spec.js @@ -6,18 +6,22 @@ const { clickDownload, closeModal } = require('../../test-utils/hooks/wvHooks') -const { joinUrl, getAttribute } = require('../../test-utils/hooks/basicHooks') +const { joinUrl } = require('../../test-utils/hooks/basicHooks') /** @type {import('@playwright/test').Page} */ let page +/** @type {Promise} */ +let downloadPromise + const startParams = [ - 'imageDownload=' + 'v=-1,-1,1,1' ] test.describe.configure({ mode: 'serial' }) test.beforeAll(async ({ browser }) => { page = await browser.newPage() + downloadPromise = page.waitForEvent('download') }) test.afterAll(async () => { @@ -25,34 +29,97 @@ test.afterAll(async () => { }) test('Image for today', async () => { - const url = await joinUrl(startParams, '&now=2018-06-01T3') + const url = await joinUrl(startParams, null) await page.goto(url) await closeModal(page) await openImageDownloadPanel(page) + + // Verify current date is selected + const dateDisplay = page.locator('.wv-date-selector-widget') + await expect(dateDisplay).toBeVisible() + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - expect(urlAttribute).toContain('TIME=2018-06-01') + const progressDialog = page.locator('.wv-snapshot-progress-overlay') + await expect(progressDialog).toBeVisible() + + const cancelButton = page.locator('#wv-snapshot-cancel-button') + await expect(cancelButton).toBeVisible() + + // Wait for either the download to start or the progress dialog to disappear (timeout after 20s) + try { + await Promise.race([ + downloadPromise, + progressDialog.waitFor({ state: 'detached', timeout: 200_000 }), + cancelButton.click() + ]) + } catch (e) { + throw new Error('Snapshot download did not complete or progress dialog did not disappear in time') + } + await closeImageDownloadPanel(page) }) -test('Image for yesterday', async () => { - const url = await joinUrl(startParams, '&now=2018-06-01T0') +test('Image for past date', async () => { + const url = await joinUrl(startParams, 't=2018-05-31') await page.goto(url) await closeModal(page) await openImageDownloadPanel(page) + + // Verify test date is selected + const dateDisplay = page.locator('.wv-date-selector-widget') + await expect(dateDisplay).toBeVisible() + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - expect(urlAttribute).toContain('TIME=2018-05-31') + const progressDialog = page.locator('.wv-snapshot-progress-overlay') + await expect(progressDialog).toBeVisible() + + const cancelButton = page.locator('#wv-snapshot-cancel-button') + await expect(cancelButton).toBeVisible() + + // Wait for either the download to start or the progress dialog to disappear (timeout after 20s) + try { + await Promise.race([ + downloadPromise, + progressDialog.waitFor({ state: 'detached', timeout: 200_000 }), + cancelButton.click() + ]) + } catch (e) { + throw new Error('Snapshot download did not complete or progress dialog did not disappear in time') + } + await closeImageDownloadPanel(page) }) test('Image for 2018-05-15', async () => { - const url = await joinUrl(startParams, '&t=2018-05-15') + const url = await joinUrl(startParams, 't=2018-05-15') await page.goto(url) await closeModal(page) await openImageDownloadPanel(page) + + // Verify specific date is selected + const dateDisplay = page.locator('.wv-date-selector-widget') + await expect(dateDisplay).toBeVisible() + + // Start download and verify progress indicator appears await clickDownload(page) - const urlAttribute = await getAttribute(page, '#wv-image-download-url', 'url') - expect(urlAttribute).toContain('TIME=2018-05-15') + const progressDialog = page.locator('.wv-snapshot-progress-overlay') + await expect(progressDialog).toBeVisible() + + const cancelButton = page.locator('#wv-snapshot-cancel-button') + await expect(cancelButton).toBeVisible() + + // Wait for either the download to start or the progress dialog to disappear (timeout after 20s) + try { + await Promise.race([ + downloadPromise, + progressDialog.waitFor({ state: 'detached', timeout: 200_000 }), + cancelButton.click() + ]) + } catch (e) { + throw new Error('Snapshot download did not complete or progress dialog did not disappear in time') + } + await closeImageDownloadPanel(page) }) diff --git a/e2e/features/image-download/unsupported-test.spec.js b/e2e/features/image-download/unsupported-test.spec.js index f3be29f798..c9c37bce15 100644 --- a/e2e/features/image-download/unsupported-test.spec.js +++ b/e2e/features/image-download/unsupported-test.spec.js @@ -1,7 +1,7 @@ // @ts-check const { test, expect } = require('@playwright/test') const createSelectors = require('../../test-utils/global-variables/selectors') -const { closeImageDownloadPanel, closeModal } = require('../../test-utils/hooks/wvHooks') +const { closeModal } = require('../../test-utils/hooks/wvHooks') const { joinUrl } = require('../../test-utils/hooks/basicHooks') /** @type {import('@playwright/test').Page} */ @@ -13,7 +13,7 @@ let cancelNotify let acceptNotify let toolbarSnapshot -const startParams = ['v=-180,-90,180,90', 't=2018-06-01', 'imageDownload='] +const startParams = ['v=-180,-90,180,90', 't=2018-06-01'] test.describe.configure({ mode: 'serial' }) @@ -30,31 +30,6 @@ test.afterAll(async () => { await page.close() }) -test('Custom palettes are not supported dialog', async () => { - const { snapshotToolbarButton } = selectors - const url = await joinUrl(startParams, '&l=MODIS_Terra_Aerosol(palette=red_1)') - await page.goto(url) - await closeModal(page) - await snapshotToolbarButton.click() - await expect(notify).toBeVisible() -}) - -test('Custom palettes: Cancel button', async () => { - await cancelNotify.click() - await expect(notify).not.toBeVisible() - await expect(toolbarSnapshot).not.toBeVisible() -}) - -test('Custom palettes: OK button brings up download panel', async () => { - const { snapshotToolbarButton } = selectors - await snapshotToolbarButton.click() - await expect(notify).toBeVisible() - await acceptNotify.click() - await expect(notify).not.toBeVisible() - await expect(toolbarSnapshot).toBeVisible() - await closeImageDownloadPanel(page) -}) - test('Rotation is not supported dialog', async () => { const { snapshotToolbarButton } = selectors const url = await joinUrl(startParams, '&p=arctic&r=18') diff --git a/package-lock.json b/package-lock.json index 3894bf11b3..ce81426d14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "bluebird": "3.7.2", "bootstrap": "^5.3.8", "cachai": "^1.0.2", + "canvas-size": "^2.0.0", "compression": "^1.8.1", "coordinate-parser": "^1.0.7", "copy-to-clipboard": "^4.0.2", @@ -35,13 +36,17 @@ "elm-pep": "^1.0.6", "eslint-plugin-react-hooks": "^7.1.1", "file-saver": "^2.0.5", + "gdal3.js": "^2.8.1", "geographiclib-geodesic": "^2.2.0", "history": "^5.3.0", + "html2canvas": "^1.4.1", "imagesloaded": "^5.0.0", "immutability-helper": "^3.1.1", "jsdom-worker": "^0.3.0", + "jszip": "^3.10.1", "lodash": "^4.18.1", "masonry-layout": "^4.2.2", + "mathjs": "^15.2.0", "moment": "^2.30.1", "moment-locales-webpack-plugin": "^1.2.0", "node-dir": "^0.1.17", @@ -92,6 +97,7 @@ "babel-loader": "^10.1.1", "cheerio": "^1.2.0", "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^14.0.0", "cross-env": "^10.1.0", "css-loader": "^7.1.4", "css-minimizer-webpack-plugin": "^8.0.0", @@ -2688,40 +2694,6 @@ "value-equal": "^1.0.1" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -6484,6 +6456,40 @@ "node": ">=14.0.0" } }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", @@ -7496,6 +7502,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.38", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", @@ -7901,6 +7916,12 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-size": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/canvas-size/-/canvas-size-2.0.0.tgz", + "integrity": "sha512-D7yvTTPX5ZvmocInOaRXyAsLtvjy7nctyfA+SNOlTG0KPjsmhw/eoJJkx8risaY/VSUW04vd8UPN7j3Ge634OQ==", + "license": "MIT" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8233,6 +8254,19 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/complex.js": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.3.tgz", + "integrity": "sha512-UrQVSUur14tNX6tiP4y8T4w4FeJAX3bi2cIv0pu/DTLFNxoq7z2Yh83Vfzztj6Px3X/lubqQ9IrPp7Bpn6p4MQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -8363,6 +8397,30 @@ "integrity": "sha512-gklSft7IuhriZKHKpuoA1fpJSLPNgvUMWMo5BlnzAJm0zNKnznoSv23IjtNqclx8eKi6ZcdvFFzYEER/+U1LoQ==", "license": "MIT" }, + "node_modules/copy-webpack-plugin": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-14.0.0.tgz", + "integrity": "sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-parent": "^6.0.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^7.0.3", + "tinyglobby": "^0.2.12" + }, + "engines": { + "node": ">= 20.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, "node_modules/core-js-compat": { "version": "3.49.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", @@ -8393,7 +8451,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, "license": "MIT" }, "node_modules/cosmiconfig": { @@ -8504,6 +8561,15 @@ "node": ">=12" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-loader": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", @@ -9556,7 +9622,6 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, "license": "MIT" }, "node_modules/decimal.js-light": { @@ -9790,7 +9855,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, "license": "MIT" }, "node_modules/dns-packet": { @@ -10321,6 +10385,12 @@ "dev": true, "license": "MIT" }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -11604,7 +11674,6 @@ "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -11688,6 +11757,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gdal3.js": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/gdal3.js/-/gdal3.js-2.8.1.tgz", + "integrity": "sha512-Zg3T0vyDAPJ7kyyhyjhmG2OvUg7Xpn+Uh9+hTMIKXj4JDrJPERTrA8Q9wpthtqX9yvaftg7aS9YIrot4p7cNLA==", + "license": "LGPL-2.1-or-later", + "dependencies": { + "detect-node": "^2.1.0", + "xml-js": "^1.6.11" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -12373,6 +12452,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", @@ -12619,6 +12711,12 @@ "ev-emitter": "^2.1.2" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immer": { "version": "11.1.8", "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", @@ -12734,7 +12832,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -13533,6 +13630,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "license": "MIT" + }, "node_modules/jest": { "version": "30.4.2", "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", @@ -14854,6 +14957,60 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/kdbush": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz", @@ -14948,6 +15105,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -15217,6 +15383,29 @@ "node": ">= 0.4" } }, + "node_modules/mathjs": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-15.2.0.tgz", + "integrity": "sha512-UAQzSVob9rNLdGpqcFMYmSu9dkuLYy7Lr2hBEQS5SHQdknA9VppJz3cy2KkpMzTODunad6V6cNv+5kOLsePLow==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.26.10", + "complex.js": "^2.2.5", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "^5.2.1", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.2.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mathml-tag-names": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-4.0.0.tgz", @@ -17927,7 +18116,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "license": "MIT" }, "node_modules/proj4": { @@ -19143,7 +19331,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -19223,6 +19410,12 @@ "integrity": "sha512-bnnvJL28/Rtz/kz2+4wpBjHzWoEzXhVg/TE8BeVGJHUqE8THNIRnDxDWMktwM+qahvlRdvlLdsQfYe+cuqfZeA==", "license": "ISC" }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "license": "MIT" + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -19484,6 +19677,12 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -21050,6 +21249,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/thingies": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", @@ -21074,6 +21282,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -21461,6 +21675,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-function": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.2.tgz", + "integrity": "sha512-VwaXim9Gp1bngi/q3do8hgttYn2uC3MoT/gfuMWylnj1IeZBUAyPddHZlo1K05BDoj8DYPpMdiHqH1dDYdJf2A==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, "node_modules/typescript-eslint": { "version": "8.61.1", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz", @@ -21752,7 +21975,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -21765,6 +21987,15 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", @@ -23040,7 +23271,6 @@ "version": "1.6.11", "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "dev": true, "license": "MIT", "dependencies": { "sax": "^1.2.4" diff --git a/package.json b/package.json index b190d26270..1ab4377dd4 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "babel-loader": "^10.1.1", "cheerio": "^1.2.0", "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^14.0.0", "cross-env": "^10.1.0", "css-loader": "^7.1.4", "css-minimizer-webpack-plugin": "^8.0.0", @@ -174,6 +175,7 @@ "bluebird": "3.7.2", "bootstrap": "^5.3.8", "cachai": "^1.0.2", + "canvas-size": "^2.0.0", "compression": "^1.8.1", "coordinate-parser": "^1.0.7", "copy-to-clipboard": "^4.0.2", @@ -182,13 +184,17 @@ "elm-pep": "^1.0.6", "eslint-plugin-react-hooks": "^7.1.1", "file-saver": "^2.0.5", + "gdal3.js": "^2.8.1", "geographiclib-geodesic": "^2.2.0", "history": "^5.3.0", + "html2canvas": "^1.4.1", "imagesloaded": "^5.0.0", "immutability-helper": "^3.1.1", "jsdom-worker": "^0.3.0", + "jszip": "^3.10.1", "lodash": "^4.18.1", "masonry-layout": "^4.2.2", + "mathjs": "^15.2.0", "moment": "^2.30.1", "moment-locales-webpack-plugin": "^1.2.0", "node-dir": "^0.1.17", diff --git a/schemas/layer-config.json b/schemas/layer-config.json index 278e8773c0..507a0c83a3 100644 --- a/schemas/layer-config.json +++ b/schemas/layer-config.json @@ -41,6 +41,9 @@ "disableSnapshot": { "type": "boolean" }, + "disableGIF": { + "type": "boolean" + }, "sourceOverride": { "type": "string" }, diff --git a/web/css/components/image-download/snapshot-progress.css b/web/css/components/image-download/snapshot-progress.css new file mode 100644 index 0000000000..563f31e8af --- /dev/null +++ b/web/css/components/image-download/snapshot-progress.css @@ -0,0 +1,72 @@ +/* Snapshot progress styles */ +.wv-snapshot-progress { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px; + margin: 10px 0; + background-color: #222; + color: white; + border: none; + flex-direction: column; + border-radius: 6px; + opacity: 1; +} + +.wv-snapshot-progress-message { + display: flex; + align-items: center; + font-weight: 500; + color: white; +} + +.wv-snapshot-progress-message::before { + content: ''; + display: inline-block; + width: 16px; + height: 16px; + margin-right: 10px; + border: 2px solid white; + border-top: 2px solid transparent; + border-radius: 50%; + animation: wv-snapshot-spin 1s linear infinite; +} + +.wv-snapshot-progress-actions { + flex-shrink: 0; + padding: 1rem; +} + +@keyframes wv-snapshot-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.wv-snapshot-progress-dialog { + position: absolute; + background-color: #222; + color: white; + left: 0; + top: 40%; + z-index: 9999999999; + box-sizing: unset; + opacity: 1; + border: 1px solid #555; + border-radius: 6px; + + button { + cursor: pointer; + } +} + +.wv-snapshot-progress-overlay { + position: absolute; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.95); + opacity: 1; + cursor: wait; + width: 100%; + height: 100%; + z-index: 2; +} diff --git a/web/images/natural-events/dot.svg b/web/images/natural-events/dot.svg index 4d0b64441f..9507b5ba02 100644 --- a/web/images/natural-events/dot.svg +++ b/web/images/natural-events/dot.svg @@ -1,4 +1,4 @@ - + diff --git a/web/images/natural-events/icon-critical.svg b/web/images/natural-events/icon-critical.svg index 582f8b6cd7..83ac7654c9 100644 --- a/web/images/natural-events/icon-critical.svg +++ b/web/images/natural-events/icon-critical.svg @@ -1,5 +1,5 @@ - + diff --git a/web/images/natural-events/icon-default.svg b/web/images/natural-events/icon-default.svg index 66a4721fd9..6ebc11e2dd 100644 --- a/web/images/natural-events/icon-default.svg +++ b/web/images/natural-events/icon-default.svg @@ -1,3 +1,3 @@ -
@@ -65,11 +69,12 @@ export default class ResolutionTable extends React.Component {
@@ -84,4 +89,5 @@ ResolutionTable.propTypes = { validLayers: PropTypes.bool, validSize: PropTypes.bool, width: PropTypes.number, + isSnapshotInProgress: PropTypes.bool, }; diff --git a/web/js/components/image-download/image-download-panel.js b/web/js/components/image-download/image-download-panel.js index 3d4f62206e..bce171f85d 100644 --- a/web/js/components/image-download/image-download-panel.js +++ b/web/js/components/image-download/image-download-panel.js @@ -1,32 +1,39 @@ -import { useState, useEffect } from 'react'; -import { useSelector } from 'react-redux'; +import { useState, useEffect, useRef } from 'react'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; - import googleTagManager from 'googleTagManager'; -import { getActivePalettes } from '../../modules/palettes/selectors'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { imageSizeValid, + estimateMaxImageSize, getDimensions, - getDownloadUrl, getTruncatedGranuleDates, GRANULE_LIMIT, + snapshot, } from '../../modules/image-download/util'; import SelectionList from '../util/selector'; +import { openCustomContent } from '../../modules/modal/actions'; import ResTable from './grid'; import AlertUtil from '../util/alert'; import LatLongSelect from './lat-long-inputs'; import GlobalSelectCheckbox from './global-select'; +import WaitOverlay from './wait'; +import SnapshotError from './snapshot-error'; +import onClickFeedback from '../../modules/feedback/util'; +import initFeedback from '../../modules/feedback/actions'; +import { subdailyLayersActive } from '../../modules/layers/selectors'; -const MAX_DIMENSION_SIZE = 8200; const RESOLUTION_KEY = { - 0.125: '30m', - 0.25: '60m', - 0.5: '125m', - 1: '250m', - 2: '500m', - 4: '1km', - 20: '5km', - 40: '10km', + 0.075: '7.5cm', + 0.3: '30cm', + 30: '30m', + 60: '60m', + 125: '125m', + 250: '250m', + 500: '500m', + 1_000: '1km', + 5_000: '5km', + 10_000: '10km', }; function ImageDownloadPanel(props) { @@ -35,11 +42,9 @@ function ImageDownloadPanel(props) { isWorldfile = false, resolution = '1', getLayers, - url, lonlats, projection, date, - markerCoordinates, onPanelChange, fileTypeOptions = true, fileTypes, @@ -49,23 +54,48 @@ function ImageDownloadPanel(props) { map, viewExtent, resolutions, - maxImageSize = '8200px x 8200px', - firstLabel = 'Resolution (per pixel)', + firstLabel, geoLatLong, onLatLongChange, + boundaries, + openSnapshotErrorModal, + feedbackIsInitiated, + isMobile, + isSubdaily, + sendFeedback, + onResolutionChange, + onProgressChange, } = props; const [currFileType, setFileType] = useState(fileType); const [currIsWorldfile, setIsWorldfile] = useState(isWorldfile); const [currResolution, setResolution] = useState(resolution); - const [debugUrl, setDebugUrl] = useState(''); const [showGranuleWarning, setShowGranuleWarning] = useState(false); - const activePalettes = useSelector((state) => getActivePalettes( - state, - state.compare.activeString, - )); + const [maxWidth, setMaxWidth] = useState(0); + const [maxHeight, setMaxHeight] = useState(0); + const [isSnapshotInProgress, setIsSnapshotInProgress] = useState(false); + const [snapshotStatus, setSnapshotStatus] = useState(''); + const abortControllerRef = useRef(null); + + const onCancelSnapshot = () => { + abortControllerRef.current?.abort(); + setIsSnapshotInProgress(false); + setSnapshotStatus(''); + onProgressChange(false); + }; + + const changeResolution = (res) => { + onResolutionChange(res); + setResolution(res); + }; useEffect(() => { + const divElem = document.querySelector('body'); + const resizeHandler = async () => { + const { height, width } = await estimateMaxImageSize(map, Number(currResolution)); + setMaxHeight(height); + setMaxWidth(width); + }; const layerList = getLayers(); const granuleDatesMap = new Map(map.getLayers().getArray() .map((layer) => [ @@ -76,57 +106,99 @@ function ImageDownloadPanel(props) { ...def, granuleDates: granuleDatesMap.get(def.id), })); const isTruncated = getTruncatedGranuleDates(layerDefs, date).truncated; + const resizeObserver = new ResizeObserver(resizeHandler); + resizeObserver.observe(divElem); + resizeHandler(); setShowGranuleWarning(isTruncated); + + return () => { + resizeObserver.unobserve(divElem); + // Clean up any ongoing snapshot operation when component unmounts + onCancelSnapshot(); + }; }, []); useEffect(() => { - setResolution(resolution); + changeResolution(resolution); }, [resolution]); - const onDownload = (width, height) => { - const time = new Date(date.getTime()); - + const onDownload = async (width, height) => { const layerList = getLayers(); - const granuleDatesMap = new Map(map.getLayers().getArray() - .map((layer) => [ - layer.wv.id, layer.wv.granuleDates, - ])); - const layerDefs = layerList.map((def) => ({ - ...def, granuleDates: granuleDatesMap.get(def.id), - })); - const dlURL = getDownloadUrl( - url, + const snapshotFormat = currFileType === 'application/vnd.google-earth.kmz' ? 'kmz' : currFileType.split('/').at(-1); + + // Create abort controller for this snapshot operation + const abortController = new AbortController(); + abortControllerRef.current = abortController; + setIsSnapshotInProgress(true); + setSnapshotStatus('Preparing snapshot...'); + onProgressChange(true); + + let formattedDate = date.toISOString(); + if (!isSubdaily) { + formattedDate = formattedDate.split('T')[0]; + } + + const snapshotOptions = { + format: snapshotFormat, + metersPerPixel: Number(currResolution), + pixelBbox: boundaries, + map, + worldfile: currIsWorldfile, + abortSignal: abortController.signal, + filename: `snapshot-${formattedDate}`, projection, - layerDefs, - lonlats, - { width, height }, - time, - currFileType, - currFileType === 'application/vnd.google-earth.kmz' ? false : currIsWorldfile, - markerCoordinates, - activePalettes, - ); + onerror: openSnapshotErrorModal, + width, + height, + }; - window.open(dlURL, '_blank'); - googleTagManager.pushEvent({ - event: 'image_download', - layers: { - activeCount: layerList.length, - }, - image: { - resolution: RESOLUTION_KEY[currResolution], - format: currFileType, - worldfile: currIsWorldfile, - }, - }); - setDebugUrl(dlURL); + const timeout = setTimeout(onCancelSnapshot, 180_000); + try { + setSnapshotStatus('Creating snapshot...'); + const startTime = Date.now(); + await snapshot(snapshotOptions); + const endTime = Date.now(); + setSnapshotStatus('Download complete!'); + + googleTagManager.pushEvent({ + event: 'image_download', + layers: { + activeCount: layerList.length, + }, + duration: endTime - startTime, + image: { + resolution: RESOLUTION_KEY[currResolution], + format: currFileType, + worldfile: currIsWorldfile, + }, + }); + } catch (error) { + if (error.name === 'AbortError') { + console.warn('Snapshot operation was cancelled by user'); + } else { + throw new Error('Snapshot operation failed', { cause: error }); + } + } finally { + // Add a delay to show the 'Download complete!' message before clearing UI + if (snapshotStatus === 'Download complete!') { + await new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + } + clearTimeout(timeout); + setIsSnapshotInProgress(false); + setSnapshotStatus(''); + onProgressChange(false); + abortControllerRef.current = null; + changeResolution(currResolution); + } }; const handleChange = (type, value) => { let valueIn = value; if (type === 'resolution') { - setResolution(valueIn); + changeResolution(valueIn); } else if (type === 'worldfile') { valueIn = Boolean(Number(value)); setIsWorldfile(valueIn); @@ -191,8 +263,15 @@ function ImageDownloadPanel(props) { /> ); + const handleKeyDown = (e, feedbackIsInitiatedArg, isMobileArg) => { + if (e.key === 'Enter') { + return sendFeedback(feedbackIsInitiatedArg, isMobileArg); + } + return null; + }; + const { crs } = projection.selected; - const dimensions = getDimensions(projection.id, lonlats, currResolution); + const dimensions = getDimensions(map, lonlats, currResolution); const { height } = dimensions; const { width } = dimensions; const filetypeSelect = renderFileTypeSelect(); @@ -202,13 +281,13 @@ function ImageDownloadPanel(props) { return ( <> {crossesDatelineAlert()} -
-
+ )} +
0} onClick={onDownload} + isSnapshotInProgress={isSnapshotInProgress} /> +
+

+ + + + This snapshot feature has been upgraded to capture anything on the map, + including customized color palettes. If you notice any issues, please + {' '} + + handleKeyDown(e, feedbackIsInitiatedArg, isMobileArg)} + onClick={() => sendFeedback(feedbackIsInitiated, isMobile)} + > + contact us + + . +

); } +const mapStateToProps = (state) => { + const { + feedback, screenSize, + } = state; + return { + feedbackIsInitiated: feedback.isInitiated, + isMobile: screenSize.isMobileDevice, + isSubdaily: subdailyLayersActive(state), + }; +}; + +const mapDispatchToProps = (dispatch) => ({ + openSnapshotErrorModal: () => { + dispatch( + openCustomContent('SNAPSHOT_ERROR_MODAL', { + headerText: 'Snapshot Error', + backdrop: false, + bodyComponent: SnapshotError, + wrapClassName: 'unclickable-behind-modal', + modalClassName: 'snapshot-error', + }), + ); + }, + sendFeedback: (isInitiated, isMobile) => { + onClickFeedback(isInitiated, isMobile); + if (!isInitiated) { + dispatch(initFeedback()); + } + }, +}); + +ImageDownloadPanel.defaultProps = { + fileType: 'image/jpeg', + fileTypeOptions: true, + firstLabel: 'Resolution (per pixel)', + isWorldfile: false, + resolution: 250, + secondLabel: 'Format', + worldFileOptions: true, +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(ImageDownloadPanel); + ImageDownloadPanel.propTypes = { datelineMessage: PropTypes.string, fileType: PropTypes.string, @@ -267,19 +424,22 @@ ImageDownloadPanel.propTypes = { isWorldfile: PropTypes.bool, lonlats: PropTypes.oneOfType([PropTypes.array, PropTypes.oneOf(['null'])]), map: PropTypes.oneOfType([PropTypes.object, PropTypes.oneOf(['null'])]), - maxImageSize: PropTypes.string, - markerCoordinates: PropTypes.oneOfType([PropTypes.array, PropTypes.oneOf(['null'])]), onPanelChange: PropTypes.func, projection: PropTypes.oneOfType([PropTypes.object, PropTypes.oneOf(['null'])]), date: PropTypes.oneOfType([PropTypes.object, PropTypes.oneOf(['null'])]), - resolution: PropTypes.string, + resolution: PropTypes.number, resolutions: PropTypes.oneOfType([PropTypes.object, PropTypes.oneOf(['null'])]), secondLabel: PropTypes.string, - url: PropTypes.string, viewExtent: PropTypes.oneOfType([PropTypes.array, PropTypes.oneOf(['null'])]), worldFileOptions: PropTypes.bool, geoLatLong: PropTypes.oneOfType([PropTypes.array, PropTypes.oneOf(['null'])]), onLatLongChange: PropTypes.func, + boundaries: PropTypes.array, + openSnapshotErrorModal: PropTypes.func, + feedbackIsInitiated: PropTypes.bool, + isMobile: PropTypes.bool, + isSubdaily: PropTypes.bool, + sendFeedback: PropTypes.func, + onResolutionChange: PropTypes.func, + onProgressChange: PropTypes.func, }; - -export default ImageDownloadPanel; diff --git a/web/js/components/image-download/image-download-panel.test.js b/web/js/components/image-download/image-download-panel.test.js index 2046ad1f3d..336e0d1b86 100644 --- a/web/js/components/image-download/image-download-panel.test.js +++ b/web/js/components/image-download/image-download-panel.test.js @@ -8,18 +8,27 @@ jest.mock('react-redux', () => ({ compare: { activeString: 'a' }, palettes: { a: {} }, }), + connect: () => (Component) => Component, })); jest.mock('googleTagManager', () => ({ pushEvent: jest.fn(), })); +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); + // Mock util functions used by the component jest.mock('../../modules/image-download/util', () => ({ imageSizeValid: jest.fn(() => true), + estimateMaxImageSize: jest.fn(() => ({ width: 8192, height: 8192 })), getDimensions: jest.fn(() => ({ width: 10, height: 20 })), getDownloadUrl: jest.fn((url) => `${url}?download=true`), getTruncatedGranuleDates: jest.fn(() => ({ truncated: true })), + snapshot: jest.fn(), GRANULE_LIMIT: 42, })); @@ -56,7 +65,6 @@ jest.mock('./global-select', () => (props) => ( )); import ImageDownloadPanel from './image-download-panel'; -import googleTagManager from 'googleTagManager'; import * as util from '../../modules/image-download/util'; describe('ImageDownloadPanel', () => { @@ -82,6 +90,8 @@ describe('ImageDownloadPanel', () => { resolutions: ['1', '2'], geoLatLong: null, onLatLongChange: jest.fn(), + onResolutionChange: jest.fn(), + onProgressChange: jest.fn(), }; beforeEach(() => { @@ -109,17 +119,6 @@ describe('ImageDownloadPanel', () => { // click download button fireEvent.click(screen.getByText('Download')); - - // window.open called with constructed URL - expect(window.open).toHaveBeenCalledWith('http://example.com/wms?download=true', '_blank'); - - // googleTagManager called - expect(googleTagManager.pushEvent).toHaveBeenCalled(); - - // debug url written to hidden div - const urlDiv = document.getElementById('wv-image-download-url'); - // the component sets url as a prop; ensure exists - expect(urlDiv).toBeInTheDocument(); }); test('worldfile select disabled for kmz', () => { diff --git a/web/js/components/image-download/snapshot-error.js b/web/js/components/image-download/snapshot-error.js new file mode 100644 index 0000000000..fa78ad1087 --- /dev/null +++ b/web/js/components/image-download/snapshot-error.js @@ -0,0 +1,12 @@ +function SnapshotError() { + const msg = 'An error occurred while creating this snapshot. Please try again.'; + return ( +
+
+ {msg} +
+
+ ); +} + +export default SnapshotError; diff --git a/web/js/components/image-download/wait.js b/web/js/components/image-download/wait.js new file mode 100644 index 0000000000..ee445c2e12 --- /dev/null +++ b/web/js/components/image-download/wait.js @@ -0,0 +1,36 @@ +import { createPortal } from 'react-dom'; +import Button from '../util/button'; + +import '../../../css/components/image-download/snapshot-progress.css'; + +const Wait = (props) => { + const { complete = false, statusText, onCancel } = props; + + const root = document.querySelector('.wv-content') || document.body; + + return createPortal( +
+ +
+
+ {statusText} +
+
+ {!complete && ( +
+
+
+
, + root, + ); +}; + +export default Wait; diff --git a/web/js/components/sidebar/event-icon.js b/web/js/components/sidebar/event-icon.js index 02fcdfdd42..8e400d215c 100644 --- a/web/js/components/sidebar/event-icon.js +++ b/web/js/components/sidebar/event-icon.js @@ -1,19 +1,42 @@ import PropTypes from 'prop-types'; import { UncontrolledTooltip } from 'reactstrap'; +const eventMappings = { + dustHaze: 'dust-and-haze', + icebergs: 'icebergs', + floods: 'floods', + manmade: 'manmade', + seaLakeIce: 'icebergs', + severeStorms: 'severe-storms', + snow: 'snow', + volcanoes: 'volcanoes', + waterColor: 'water-color', + wildfires: 'wildfires', +}; + export default function EventIcon (props) { const { id, category, title, hideTooltip, + withPin, } = props; return (
- + )} + {!hideTooltip && ( ({ getZoom: jest.fn(() => 3) })), + getCoordinateFromPixel: jest.fn(() => [0, 0]), }, }, }, diff --git a/web/js/containers/image-download.js b/web/js/containers/image-download.js index 2aff44453a..5684827589 100644 --- a/web/js/containers/image-download.js +++ b/web/js/containers/image-download.js @@ -19,14 +19,15 @@ import { } from '../modules/layers/selectors'; import { getSelectedDate } from '../modules/date/selectors'; import { - resolutionsGeo, - resolutionsPolar, + RESOLUTIONS_GEO, + RESOLUTIONS_POLAR, fileTypesGeo, fileTypesPolar, } from '../modules/image-download/constants'; import { onPanelChange as onPanelChangeAction, updateBoundaries, + updateInProgress, } from '../modules/image-download/actions'; import { getNormalizedCoordinate } from '../components/location-search/util'; import { CRS } from '../modules/map/constants'; @@ -61,6 +62,7 @@ class ImageDownloadContainer extends Component { this.debounceBoundaryStateUpdate = lodashDebounce(onBoundaryChange, 200); this.onBoundaryChange = this.onBoundaryChange.bind(this); this.onLatLongChange = this.onLatLongChange.bind(this); + this.onResolutionChange = this.onResolutionChange.bind(this); } /** @@ -140,6 +142,16 @@ class ImageDownloadContainer extends Component { this.debounceBoundaryStateUpdate(newBoundaries); } + /** + * Update resolution value on change + * @param {Object} res + * + * @returns {null} + */ + onResolutionChange(res) { + this.setState({ resolution: res }); + } + render() { const { proj, @@ -153,6 +165,7 @@ class ImageDownloadContainer extends Component { hasSubdailyLayers, markerCoordinates, onPanelChange, + onProgressChange, } = this.props; const { resolution, isWorldfile, fileType, bottomLeftLatLong, topRightLatLong, boundaries, @@ -165,13 +178,14 @@ class ImageDownloadContainer extends Component { const lonLat2 = olProj.transform(topRightLatLong, CRS.GEOGRAPHIC, crs); const isGeoProjection = proj.id === 'geographic'; const fileTypes = isGeoProjection ? fileTypesGeo : fileTypesPolar; - const resolutions = isGeoProjection ? resolutionsGeo : resolutionsPolar; + const resolutions = isGeoProjection ? RESOLUTIONS_GEO : RESOLUTIONS_POLAR; const mapView = map.ui.selected.getView(); + const center = mapView.getCenter(); const newResolution = resolution || imageUtilCalculateResolution( Math.round(mapView.getZoom()), - isGeoProjection, - proj.selected.resolutions, + proj, + center, ); const viewExtent = mapView.calculateExtent(map.ui.selected.getSize()); const normalizedBottomLeftLatLong = getNormalizedCoordinate(bottomLeftLatLong); @@ -185,6 +199,7 @@ class ImageDownloadContainer extends Component { fileType={fileType} resolutions={resolutions} lonlats={[lonLat1, lonLat2]} + boundaries={[x, y, x2, y2]} resolution={newResolution} isWorldfile={isWorldfile} hasSubdailyLayers={hasSubdailyLayers} @@ -203,6 +218,8 @@ class ImageDownloadContainer extends Component { onLatLongChange={this.onLatLongChange} geoLatLong={[normalizedBottomLeftLatLong, normalizedTopRightLatLong]} map={map.ui.selected} + onResolutionChange={this.onResolutionChange} + onProgressChange={onProgressChange} /> ({ onBoundaryChange: (obj) => { dispatch(updateBoundaries(obj)); }, + onProgressChange: (obj) => { + dispatch(updateInProgress(obj)); + }, }); export default connect( @@ -298,6 +318,7 @@ ImageDownloadContainer.propTypes = { fileType: PropTypes.string.isRequired, map: PropTypes.oneOfType([PropTypes.object, PropTypes.oneOf(['null'])]), onBoundaryChange: PropTypes.func.isRequired, + onProgressChange: PropTypes.func.isRequired, onPanelChange: PropTypes.func.isRequired, proj: PropTypes.oneOfType([PropTypes.object, PropTypes.oneOf(['null'])]), url: PropTypes.string.isRequired, diff --git a/web/js/containers/image-download.test.js b/web/js/containers/image-download.test.js index 850391ccbb..ed4f9b96af 100644 --- a/web/js/containers/image-download.test.js +++ b/web/js/containers/image-download.test.js @@ -81,6 +81,7 @@ const makeMapUi = () => ({ getView: jest.fn(() => ({ getZoom: jest.fn(() => 4), calculateExtent: jest.fn(() => [0, 0, 100, 100]), + getCenter: jest.fn(() => [0, 0]), })), getSize: jest.fn(() => [1024, 768]), }, diff --git a/web/js/containers/toolbar.js b/web/js/containers/toolbar.js index a6f614f01a..f8d9d8348e 100644 --- a/web/js/containers/toolbar.js +++ b/web/js/containers/toolbar.js @@ -4,7 +4,6 @@ import { connect } from 'react-redux'; import { ButtonToolbar, Button } from 'reactstrap'; import { get as lodashGet, - cloneDeep as lodashCloneDeep, filter as lodashFilter, } from 'lodash'; import Promise from 'bluebird'; @@ -21,14 +20,13 @@ import { requestNotifications as requestNotificationsAction, setNotifications, } from '../modules/notifications/actions'; -import { clearCustomsSnapshot, refreshPalettes } from '../modules/palettes/actions'; +import { refreshPalettes } from '../modules/palettes/actions'; import { clearRotate, refreshRotation } from '../modules/map/actions'; import { showLayers, hideLayers, } from '../modules/layers/actions'; import { notificationWarnings } from '../modules/image-download/constants'; import Notify from '../components/image-download/notify'; -import { hasCustomPaletteInActiveProjection } from '../modules/palettes/util'; import LocationSearch from '../components/location-search/location-search'; import { toggleShowLocationSearch as toggleShowLocationSearchAction, @@ -126,9 +124,7 @@ class toolbarContainer extends Component { async openImageDownload() { const { openModal, - hasCustomPalette, isRotated, - activePalettes, rotation, refreshStateAfterImageDownload, toggleDialogVisible, @@ -136,21 +132,21 @@ class toolbarContainer extends Component { visibleLayersForProj, } = this.props; const nonDownloadableLayers = hasNonDownloadableLayer - ? getNonDownloadableLayers(visibleLayersForProj) + ? getNonDownloadableLayers(visibleLayersForProj, true) : null; - const paletteStore = lodashCloneDeep(activePalettes); toggleDialogVisible(false); - await this.getPromise(hasCustomPalette, 'palette', clearCustomsSnapshot, 'Notice'); await this.getPromise(isRotated, 'rotate', clearRotate, 'Reset rotation'); await this.getPromise(hasNonDownloadableLayer, 'layers', hideLayers, 'Remove Layers?'); + // Allow time for view to un-rotate + if (isRotated) { + await new Promise(resolve => setTimeout(resolve, 500)); + } await openModal( 'TOOLBAR_SNAPSHOT', { ...CUSTOM_MODAL_PROPS.TOOLBAR_SNAPSHOT, onClose: () => { - refreshStateAfterImageDownload(hasCustomPalette - ? paletteStore - : undefined, rotation, nonDownloadableLayers); + refreshStateAfterImageDownload(undefined, rotation, nonDownloadableLayers); }, }, ); @@ -468,7 +464,6 @@ const mapStateToProps = (state) => { modal, modalAbout, notifications, - palettes, proj, screenSize, sidebar, @@ -476,29 +471,25 @@ const mapStateToProps = (state) => { } = state; const { isDistractionFreeModeActive, isKioskModeActive } = ui; const { numberUnseen, type } = notifications; - const { activeString } = compare; const activeLayersForProj = getAllActiveLayers(state); const isMobile = screenSize.isMobileDevice; const faSize = isMobile ? '2x' : '1x'; const isCompareActive = compare.active; const isChartingActive = charting.active; const isLocationSearchExpanded = locationSearch.isExpanded; - const activePalettes = palettes[activeString]; const { isAnimatingToEvent } = events; const { activeTab } = sidebar; const isDataDownloadTabActive = activeTab === 'download'; const { isOpen: modalIsOpen } = modal; - const filteredLayers = activeLayersForProj.filter((layer) => !(layer.colormapType === 'classification' && layer.type !== 'vector')); // Collapse when Image download / GIF / is open or measure tool active const snapshotModalOpen = modalIsOpen && modal.id === 'TOOLBAR_SNAPSHOT'; const shouldBeCollapsed = snapshotModalOpen || measure.isActive || animation.gifActive; const visibleLayersForProj = lodashFilter(activeLayersForProj, 'visible'); return { - activePalettes, config: state.config, faSize, - hasNonDownloadableLayer: hasNonDownloadableVisibleLayer(visibleLayersForProj), + hasNonDownloadableLayer: hasNonDownloadableVisibleLayer(visibleLayersForProj, true), isAboutOpen: modalAbout.isOpen, isCompareActive, isChartingActive, @@ -514,10 +505,6 @@ const mapStateToProps = (state) => { isLocationSearchExpanded, isMobile, isRotated: Boolean(map.rotation !== 0), - hasCustomPalette: hasCustomPaletteInActiveProjection( - filteredLayers, - activePalettes, - ), modalIsOpen, notificationType: type, notificationContentNumber: numberUnseen, @@ -568,9 +555,9 @@ const mapDispatchToProps = (dispatch) => ({ ); }, notify: (type, action, visibleLayersForProj) => new Promise((resolve, reject, cancel) => { - const nonDownloadableLayers = type !== 'layers' ? null : getNonDownloadableLayers(visibleLayersForProj); + const nonDownloadableLayers = type !== 'layers' ? null : getNonDownloadableLayers(visibleLayersForProj, true); const bodyComponentProps = { - bodyText: type !== 'layers' ? notificationWarnings[type] : getNonDownloadableLayerWarning(nonDownloadableLayers), + bodyText: type !== 'layers' ? notificationWarnings[type] : getNonDownloadableLayerWarning(nonDownloadableLayers, true), cancel: () => { dispatch(onToggle()); }, @@ -608,11 +595,9 @@ export default connect( )(toolbarContainer); toolbarContainer.propTypes = { - activePalettes: PropTypes.oneOfType([PropTypes.object, PropTypes.oneOf(['null'])]), hasNonDownloadableLayer: PropTypes.bool, config: PropTypes.oneOfType([PropTypes.object, PropTypes.oneOf(['null'])]), faSize: PropTypes.string, - hasCustomPalette: PropTypes.bool, isAboutOpen: PropTypes.bool, isCompareActive: PropTypes.bool, isChartingActive: PropTypes.bool, diff --git a/web/js/containers/toolbar.test.js b/web/js/containers/toolbar.test.js index 6d19e2a7b9..27de7dce6a 100644 --- a/web/js/containers/toolbar.test.js +++ b/web/js/containers/toolbar.test.js @@ -103,7 +103,6 @@ const { const { clearCustomsSnapshot, refreshPalettes } = require('../modules/palettes/actions'); const { clearRotate, refreshRotation } = require('../modules/map/actions'); const { showLayers, hideLayers } = require('../modules/layers/actions'); -const { hasCustomPaletteInActiveProjection } = require('../modules/palettes/util'); const { isLocationSearchFeatureEnabled } = require('../modules/location-search/util'); const { getAllActiveLayers } = require('../modules/layers/selectors'); const { hasNonDownloadableVisibleLayer, getNonDownloadableLayers } = require('../modules/image-download/util'); @@ -298,14 +297,16 @@ describe('snapshot workflow (openImageDownload)', () => { await act(async () => { fireEvent.click(container.querySelector('#wv-image-button')); }); - expect(notify).toHaveBeenCalledTimes(3); - expect(notify).toHaveBeenCalledWith('palette', clearCustomsSnapshot, [{ id: 'vl' }]); + expect(notify).toHaveBeenCalledTimes(2); expect(notify).toHaveBeenCalledWith('rotate', clearRotate, [{ id: 'vl' }]); expect(notify).toHaveBeenCalledWith('layers', hideLayers, [{ id: 'vl' }]); - const { onClose } = openModal.mock.calls.at(-1)[1]; + let onClose; + await waitFor(() => { + ({ onClose } = openModal.mock.calls.at(-1)[1]); + }); onClose(); expect(refreshStateAfterImageDownload).toHaveBeenCalledWith( - activePalettes, 45, [{ id: 'bad-layer' }], + undefined, 45, [{ id: 'bad-layer' }], ); }); }); @@ -410,7 +411,6 @@ describe('mapStateToProps', () => { expect(result.shouldBeCollapsed).toBe(false); expect(getAllActiveLayers).toHaveBeenCalled(); expect(hasNonDownloadableVisibleLayer).toHaveBeenCalled(); - expect(hasCustomPaletteInActiveProjection).toHaveBeenCalled(); }); it('uses 2x icons on mobile', () => { @@ -513,7 +513,7 @@ describe('mapDispatchToProps', () => { it('notify uses the non-downloadable layer warning for layer type', () => { props.notify('layers', hideLayers, [{ id: 'vl' }]); const { bodyComponentProps } = openCustomContent.mock.calls.at(-1)[1]; - expect(getNonDownloadableLayers).toHaveBeenCalledWith([{ id: 'vl' }]); + expect(getNonDownloadableLayers).toHaveBeenCalledWith([{ id: 'vl' }], true); expect(bodyComponentProps.bodyText).toBe('non-downloadable-warning'); bodyComponentProps.accept(); expect(hideLayers).toHaveBeenCalledWith([{ id: 'bad-layer' }]); diff --git a/web/js/map/layerbuilder.js b/web/js/map/layerbuilder.js index 2c1a2a4ec9..01be8eea38 100644 --- a/web/js/map/layerbuilder.js +++ b/web/js/map/layerbuilder.js @@ -32,7 +32,7 @@ import { getGeographicResolutionWMS, mergeBreakpointLayerAttributes, } from './util'; -import { fetchSubdailyDateRanges, datesInDateRanges, prevDateInDateRange } from '../modules/layers/util'; +import { fetchSubdailyDateRanges, datesInDateRanges, prevDateInDateRange, nearestInterval } from '../modules/layers/util'; import { getSelectedDate } from '../modules/date/selectors'; import { isActive as isPaletteActive, @@ -44,7 +44,6 @@ import { getKey as getVectorStyleKeys, applyStyle, } from '../modules/vector-styles/selectors'; -import { nearestInterval } from '../modules/layers/util'; import { LEFT_WING_EXTENT, RIGHT_WING_EXTENT, LEFT_WING_ORIGIN, RIGHT_WING_ORIGIN, CENTER_MAP_ORIGIN, } from '../modules/map/constants'; @@ -76,6 +75,7 @@ export default function mapLayerBuilder(config, cache, store) { layerPrior.wv = attributes; layerNext.wv = attributes; return new OlLayerGroup({ + className: `wv-layer-group-${def.id}`, layers: [layer, layerNext, layerPrior], }); }; @@ -292,7 +292,9 @@ export default function mapLayerBuilder(config, cache, store) { const projectionURL = `images/map/bluemarble-${id}.jpg`; const layer = new ImageLayer({ + className: `wv-layer-static-${id}`, source: new Static({ + interpolate: false, url: projectionURL, projection: crs, imageExtent: maxExtent, @@ -454,6 +456,7 @@ export default function mapLayerBuilder(config, cache, store) { const sizes = !tileMatrices ? [] : tileMatrices.map(({ matrixWidth, matrixHeight }) => [matrixWidth, matrixHeight]); + const calcMatrixIds = matrixIds || resolutions.map((set, index) => index); // Also need to shift this if granule is shifted const tileGridOptions = { @@ -461,7 +464,7 @@ export default function mapLayerBuilder(config, cache, store) { extent: shifted ? RIGHT_WING_EXTENT : extent, sizes, resolutions, - matrixIds: matrixIds || resolutions.map((set, index) => index), + matrixIds: calcMatrixIds, tileSize: tileSize[0], }; @@ -469,17 +472,19 @@ export default function mapLayerBuilder(config, cache, store) { // This is to compensate for the inability to select seconds in the timeline layerDate = new Date(layerDate.getTime()); layerDate.setSeconds(59); + const tileGrid = new OlTileGridWMTS(tileGridOptions); const urlParameters = `?TIME=${util.toISOStringSeconds(layerDate, !isSubdaily)}`; const sourceURL = def.sourceOverride || configSource.url; const sourceOptions = { - url: sourceURL + urlParameters, + interpolate: false, + url: `${sourceURL}${urlParameters}`, layer: layer || id, cacheSize: 4096, crossOrigin: 'anonymous', format, transition: isGranule ? 350 : 0, matrixSet: configMatrixSet.id, - tileGrid: new OlTileGridWMTS(tileGridOptions), + tileGrid, wrapX: false, style: typeof style === 'undefined' ? 'default' : style, }; @@ -490,6 +495,7 @@ export default function mapLayerBuilder(config, cache, store) { const tileSource = new OlSourceWMTS(sourceOptions); const layerTile = new OlLayerTile({ + className: `wv-layer-${id}`, preload: 0, source: tileSource, }); @@ -566,6 +572,7 @@ export default function mapLayerBuilder(config, cache, store) { urlParameters = `?TIME=${util.toISOStringSeconds(util.roundTimeOneMinute(date), !isSubdaily)}`; const sourceOptions = { + interpolate: false, url: source.url + urlParameters, cacheSize: 4096, wrapX: true, @@ -587,6 +594,7 @@ export default function mapLayerBuilder(config, cache, store) { const tileSource = new OlSourceTileWMS(sourceOptions); const layer = new OlLayerTile({ + className: `wv-layer-${def.id}`, preload: 0, extent, ...!!resolutionBreakPoint && { minResolution: resolutionBreakPoint }, @@ -744,6 +752,7 @@ export default function mapLayerBuilder(config, cache, store) { } const layer = new OlLayerVector({ + className: `wv-layer-${def.id}`, extent: layerExtent, source: vectorSource, style (feature, resolution) { @@ -912,6 +921,7 @@ export default function mapLayerBuilder(config, cache, store) { }; const layer = new LayerVectorTile({ + className: `wv-layer-${def.id}`, renderOrder: orderFunction, extent: layerExtent, source: tileSource, @@ -929,6 +939,7 @@ export default function mapLayerBuilder(config, cache, store) { const newDef = { ...def, ...breakPointLayerDef }; const wmsLayer = createLayerWMS(newDef, options, day, state); const layerGroup = new OlLayerGroup({ + className: `wv-layer-group-${def.id}`, layers: [layer, wmsLayer], }); wmsLayer.wv = attributes; @@ -1043,6 +1054,7 @@ export default function mapLayerBuilder(config, cache, store) { }; const xyzSourceOptions = { + interpolate: false, crossOrigin: 'anonymous', projection: get(crs), tileUrlFunction, @@ -1065,6 +1077,7 @@ export default function mapLayerBuilder(config, cache, store) { maxZoom: def.minZoom, }); const layerGroup = new OlLayerGroup({ + className: `wv-layer-group-${def.id}`, layers: [footprintLayer, layer], }); @@ -1088,6 +1101,7 @@ export default function mapLayerBuilder(config, cache, store) { }; const xyzSourceOptions = { + interpolate: false, crossOrigin: 'anonymous', projection: get(crs), tileUrlFunction, @@ -1225,6 +1239,7 @@ export default function mapLayerBuilder(config, cache, store) { const isMaxarSource = source === 'MAXAR:wmts'; const sourceOptions = { + interpolate: false, url: `${baseUrl}/{z}/{x}/{y}`, layer: layerName, crossOrigin: 'anonymous', @@ -1325,7 +1340,10 @@ export default function mapLayerBuilder(config, cache, store) { extent: shifted ? RIGHT_WING_EXTENT : extent, }); }); - const layer = new OlLayerGroup({ layers }); + const layer = new OlLayerGroup({ + className: `wv-layer-group-${def.id}`, + layers, + }); return layer; }; diff --git a/web/js/map/natural-events/event-markers.js b/web/js/map/natural-events/event-markers.js index d6d4073a55..bb91149827 100644 --- a/web/js/map/natural-events/event-markers.js +++ b/web/js/map/natural-events/event-markers.js @@ -44,6 +44,7 @@ const createPin = function(id, category, isSelected, title, hideTooltip) { title, id, hideTooltip, + withPin: true, }), ); return new OlOverlay({ diff --git a/web/js/map/natural-events/event-track.js b/web/js/map/natural-events/event-track.js index 6dfda0696e..9bf25caa59 100644 --- a/web/js/map/natural-events/event-track.js +++ b/web/js/map/natural-events/event-track.js @@ -157,6 +157,7 @@ function EventTrack () { const [trackDetails, setTrackDetails] = useState({}); const [allTrackDetails, setAllTrackDetails] = useState([]); + const [mapMoves, setMapMoves] = useState(0); const trackDetailsRef = useRef(); trackDetailsRef.current = trackDetails; const allTrackDetailsRef = useRef(); @@ -165,6 +166,7 @@ function EventTrack () { const mapRef = useRef(map); useEffect(() => { mapRef.current = map; + map?.on?.('moveend', () => setMapMoves((c) => c + 1)); }, [map]); useEffect(() => { showAllTracksRef.current = showAllTracks; @@ -445,7 +447,7 @@ function EventTrack () { } }, [map, isPlaying, extent, selectedDate, isAnimatingToEvent, eventsData, selectedEvent, - showAllTracksRef.current, highlightedEvent, showAllTracks], + showAllTracksRef.current, highlightedEvent, showAllTracks, mapMoves], ); return null; diff --git a/web/js/mapUI/components/create-map/createMap.js b/web/js/mapUI/components/create-map/createMap.js index dac35fca57..6849d90b85 100644 --- a/web/js/mapUI/components/create-map/createMap.js +++ b/web/js/mapUI/components/create-map/createMap.js @@ -135,7 +135,7 @@ function CreateMap(props) { ], loadTilesWhileAnimating: true, loadTilesWhileInteracting: true, - maxTilesLoading: 32, + maxTilesLoading: Number.MAX_SAFE_INTEGER, }); map.wv = { scaleMetric, diff --git a/web/js/mapUI/components/create-map/createMap.test.js b/web/js/mapUI/components/create-map/createMap.test.js index 592ae0aba1..e8561c44fa 100644 --- a/web/js/mapUI/components/create-map/createMap.test.js +++ b/web/js/mapUI/components/create-map/createMap.test.js @@ -334,11 +334,11 @@ describe('CreateMap', () => { expect(mapConfig.loadTilesWhileInteracting).toBe(true); }); - it('sets maxTilesLoading to 32', () => { + it('sets maxTilesLoading to Number.MAX_SAFE_INTEGER', () => { const { props } = buildDefaultProps(); renderComponent(props); const mapConfig = OlMap.mock.calls[0][0]; - expect(mapConfig.maxTilesLoading).toBe(32); + expect(mapConfig.maxTilesLoading).toBe(Number.MAX_SAFE_INTEGER); }); it('sets map.wv with scaleMetric and scaleImperial', () => { diff --git a/web/js/mapUI/mapUI.js b/web/js/mapUI/mapUI.js index 12ce79d355..f7d3e6cc81 100644 --- a/web/js/mapUI/mapUI.js +++ b/web/js/mapUI/mapUI.js @@ -100,8 +100,7 @@ function MapUI(props) { const [vectorActions, setVectorActions] = useState({}); const [preloadAction, setPreloadAction] = useState({}); - // eslint-disable-next-line no-unused-vars - const [devTestMode, setDevTestMode] = useState(false); + const devTestMode = false; const subscribeToStore = function(action) { switch (action.type) { diff --git a/web/js/modules/gif-download/constants.js b/web/js/modules/gif-download/constants.js new file mode 100644 index 0000000000..61dbfc7f75 --- /dev/null +++ b/web/js/modules/gif-download/constants.js @@ -0,0 +1,21 @@ +export const RESOLUTIONS_GEO = { + values: [ + { value: '0.125', text: '30m' }, + { value: '0.25', text: '60m' }, + { value: '0.5', text: '125m' }, + { value: '1', text: '250m' }, + { value: '2', text: '500m' }, + { value: '4', text: '1km' }, + { value: '20', text: '5km' }, + { value: '40', text: '10km' }, + ], +}; +export const RESOLUTIONS_POLAR = { + values: [ + { value: '1', text: '250m' }, + { value: '2', text: '500m' }, + { value: '4', text: '1km' }, + { value: '20', text: '5km' }, + { value: '40', text: '10km' }, + ], +}; diff --git a/web/js/modules/gif-download/util.js b/web/js/modules/gif-download/util.js new file mode 100644 index 0000000000..522342b455 --- /dev/null +++ b/web/js/modules/gif-download/util.js @@ -0,0 +1,100 @@ +const GEO_ESTIMATION_CONSTANT = 256.0; +const POLAR_ESTIMATION_CONSTANT = 0.002197265625; + +export function imageUtilGetConversionFactor(proj) { + if (proj === 'geographic') return POLAR_ESTIMATION_CONSTANT; + return GEO_ESTIMATION_CONSTANT; +} + +export function imageUtilEstimateResolution(resolution, isGeoProjection) { + return isGeoProjection + ? resolution / POLAR_ESTIMATION_CONSTANT + : resolution / GEO_ESTIMATION_CONSTANT; +} + +export function imageUtilCalculateResolution( + zoom, + isGeoProjection, + resolutions, +) { + let resolution; + const nZoomLevels = resolutions.length; + const currentZoom = zoom < 0 ? 0 : zoom; + const curResolution = currentZoom >= nZoomLevels + ? resolutions[nZoomLevels - 1] + : resolutions[currentZoom]; + + // Estimate the option value used by "wv-image-resolution" + const resolutionEstimate = imageUtilEstimateResolution( + curResolution, + isGeoProjection, + ); + + // Find the closest match of resolution within the available values + const possibleResolutions = isGeoProjection + ? [0.125, 0.25, 0.5, 1, 2, 4, 20, 40] + : [1, 2, 4, 20, 40]; + let bestDiff = Infinity; + let bestIdx = -1; + let currDiff = 0; + for (let i = 0; i < possibleResolutions.length; i += 1) { + currDiff = Math.abs(possibleResolutions[i] - resolutionEstimate); + if (currDiff < bestDiff) { + resolution = possibleResolutions[i]; + bestDiff = currDiff; + bestIdx = i; + } + } + // Bump up resolution in certain cases where default is too low + if (bestIdx > 0) { + if (isGeoProjection) { + switch (currentZoom) { + case 3: + case 4: + case 6: + case 7: + resolution = possibleResolutions[bestIdx - 1]; + break; + default: + break; + } + } else { + switch (currentZoom) { + case 1: + case 2: + case 4: + case 5: + resolution = possibleResolutions[bestIdx - 1]; + break; + default: + break; + } + } + } + return resolution.toString(); +} + +export function imageUtilGetCoordsFromPixelValues(pixels, map) { + const { + x, y, x2, y2, + } = pixels; + return [ + map.getCoordinateFromPixel([Math.floor(x), Math.floor(y2)]), + map.getCoordinateFromPixel([Math.floor(x2), Math.floor(y)]), + ]; +} + +export function getDimensions(projection, bounds, resolution) { + const conversionFactor = imageUtilGetConversionFactor(projection); + const imgWidth = Math.round( + Math.abs(bounds[1][0] - bounds[0][0]) / + conversionFactor / + Number(resolution), + ); + const imgHeight = Math.round( + Math.abs(bounds[1][1] - bounds[0][1]) / + conversionFactor / + Number(resolution), + ); + return { width: imgWidth, height: imgHeight }; +} diff --git a/web/js/modules/gif-download/util.test.js b/web/js/modules/gif-download/util.test.js new file mode 100644 index 0000000000..0a70a768ff --- /dev/null +++ b/web/js/modules/gif-download/util.test.js @@ -0,0 +1,24 @@ +import { + imageUtilCalculateResolution, +} from './util'; + +const geoResolutions = [ + 0.5625, + 0.28125, + 0.140625, + 0.0703125, + 0.03515625, + 0.017578125, + 0.0087890625, + 0.00439453125, + 0.002197265625, + 0.0010986328125, + 0.00054931640625, + 0.00027465820313, +]; + +test('Default km resolution Calculation [imagedownload-default-resolution]', () => { + const zoom = 5; + const isGeo = true; + expect(imageUtilCalculateResolution(zoom, isGeo, geoResolutions)).toBe('4'); +}); diff --git a/web/js/modules/image-download/actions.js b/web/js/modules/image-download/actions.js index ad11d98199..1eb7019879 100644 --- a/web/js/modules/image-download/actions.js +++ b/web/js/modules/image-download/actions.js @@ -3,6 +3,7 @@ import { UPDATE_FILE_TYPE, UPDATE_RESOLUTION, UPDATE_WORLDFILE, + UPDATE_IN_PROGRESS, } from './constants'; export function updateBoundaries(obj) { @@ -23,3 +24,9 @@ export function onPanelChange(type, value) { value, }; } +export function updateInProgress(obj) { + return { + type: UPDATE_IN_PROGRESS, + inProgress: obj, + }; +} diff --git a/web/js/modules/image-download/constants.js b/web/js/modules/image-download/constants.js index e040bed2ce..00f1f33bc4 100644 --- a/web/js/modules/image-download/constants.js +++ b/web/js/modules/image-download/constants.js @@ -2,26 +2,29 @@ export const UPDATE_BOUNDARIES = 'IMAGE-DOWNLOAD/UPDATE_BOUNDARIES'; export const UPDATE_FILE_TYPE = 'IMAGE-DOWNLOAD/UPDATE_FILE_TYPE'; export const UPDATE_WORLDFILE = 'IMAGE-DOWNLOAD/UPDATE_WORLDFILE'; export const UPDATE_RESOLUTION = 'IMAGE-DOWNLOAD/UPDATE_RESOLUTION'; +export const UPDATE_IN_PROGRESS = 'IMAGE-DOWNLOAD/UPDATE_IN_PROGRESS'; -export const resolutionsGeo = { +export const RESOLUTIONS_GEO = { values: [ - { value: '0.125', text: '30m' }, - { value: '0.25', text: '60m' }, - { value: '0.5', text: '125m' }, - { value: '1', text: '250m' }, - { value: '2', text: '500m' }, - { value: '4', text: '1km' }, - { value: '20', text: '5km' }, - { value: '40', text: '10km' }, + { value: 0.075, text: '0.075m' }, + { value: 0.3, text: '0.3m' }, + { value: 30, text: '30m', tileMatrixID: 11 }, + { value: 60, text: '60m', tileMatrixID: 10 }, + { value: 125, text: '125m', tileMatrixID: 9 }, + { value: 250, text: '250m', tileMatrixID: 8 }, + { value: 500, text: '500m', tileMatrixID: 7 }, + { value: 1000, text: '1km', tileMatrixID: 6 }, + { value: 5000, text: '5km', tileMatrixID: 5 }, + { value: 10000, text: '10km', tileMatrixID: 4 }, ], }; -export const resolutionsPolar = { +export const RESOLUTIONS_POLAR = { values: [ - { value: '1', text: '250m' }, - { value: '2', text: '500m' }, - { value: '4', text: '1km' }, - { value: '20', text: '5km' }, - { value: '40', text: '10km' }, + { value: 250, text: '250m', tileMatrixID: 5 }, + { value: 500, text: '500m', tileMatrixID: 4 }, + { value: 1000, text: '1km', tileMatrixID: 3 }, + { value: 5000, text: '5km', tileMatrixID: 2 }, + { value: 10000, text: '10km', tileMatrixID: 1 }, ], }; export const fileTypesGeo = { @@ -52,3 +55,13 @@ export const notificationWarnings = { palette: PALETTE_WARNING, rotate: ROTATE_WARNING, }; + +export const GDAL_WASM_PATH = 'build/gdal3js'; + +export const DRIVER_DICT = { + tiff: 'GTiff', + tif: 'GTiff', + jpg: 'JPEG', + jpeg: 'JPEG', + png: 'PNG', +}; diff --git a/web/js/modules/image-download/reducers.js b/web/js/modules/image-download/reducers.js index cb40021151..e4e6cfbf0c 100644 --- a/web/js/modules/image-download/reducers.js +++ b/web/js/modules/image-download/reducers.js @@ -4,6 +4,7 @@ import { UPDATE_FILE_TYPE, UPDATE_WORLDFILE, UPDATE_RESOLUTION, + UPDATE_IN_PROGRESS, fileTypesPolar, } from './constants'; import { CHANGE_PROJECTION } from '../projection/constants'; @@ -12,7 +13,8 @@ export const defaultState = { fileType: 'image/jpeg', boundaries: undefined, isWorldfile: false, - resolution: '', + resolution: undefined, + inProgress: false, }; export function imageDownloadReducer(state = defaultState, action) { @@ -40,6 +42,10 @@ export function imageDownloadReducer(state = defaultState, action) { : state.fileType; return lodashAssign({}, defaultState, { fileType }); } + case UPDATE_IN_PROGRESS: + return lodashAssign({}, state, { + inProgress: action.inProgress, + }); default: return state; } diff --git a/web/js/modules/image-download/util.js b/web/js/modules/image-download/util.js index d2fb94d037..8c54fad542 100644 --- a/web/js/modules/image-download/util.js +++ b/web/js/modules/image-download/util.js @@ -1,11 +1,25 @@ +/* eslint no-underscore-dangle: 0 */ +import html2canvas from 'html2canvas'; import { get as lodashGet, } from 'lodash'; -import { transform } from 'ol/proj'; +import JSZip from 'jszip'; +import canvasSize from 'canvas-size'; +import { evaluate } from 'mathjs'; +import { transform, get } from 'ol/proj'; +import * as olExtent from 'ol/extent'; +import olTileState from 'ol/TileState'; +import initGdalJs from 'gdal3.js'; import util from '../../util/util'; import { formatDisplayDate } from '../date/util'; import { nearestInterval } from '../layers/util'; import { CRS } from '../map/constants'; +import { + GDAL_WASM_PATH, + DRIVER_DICT, + RESOLUTIONS_GEO, + RESOLUTIONS_POLAR, +} from './constants'; const GEO_ESTIMATION_CONSTANT = 256.0; const POLAR_ESTIMATION_CONSTANT = 0.002197265625; @@ -43,7 +57,7 @@ export function getLatestIntervalTime(layerDefs, dateTime) { * @param {Array} opacities * @returns {Object} layersArray, layerWraps, opacities */ -const imageUtilProcessKMZOrbitTracks = function(layersArray, layerWraps, opacities) { +const imageUtilProcessKMZOrbitTracks = (layersArray, layerWraps, opacities) => { const processedLayersArray = [...layersArray]; const processedLayerWraps = [...layerWraps]; const processedOpacities = [...opacities]; @@ -86,7 +100,7 @@ const imageUtilProcessKMZOrbitTracks = function(layersArray, layerWraps, opaciti * @param {Array} opacities * @returns {Object} layersArray, layerWraps, opacities */ -const imageUtilProcessWrap = function(fileType, layersArray, layerWraps, opacities) { +const imageUtilProcessWrap = (fileType, layersArray, layerWraps, opacities) => { if (fileType === 'application/vnd.google-earth.kmz') { return imageUtilProcessKMZOrbitTracks(layersArray, layerWraps, opacities); } @@ -103,6 +117,21 @@ export function imageUtilEstimateResolution(resolution, isGeoProjection) { : resolution / GEO_ESTIMATION_CONSTANT; } +function getMetersPerUnit(projection, center = [0, 0]) { + const units = projection.getUnits(); + let metersPerUnit = projection.getMetersPerUnit(); + + if (units === 'degrees') metersPerUnit *= evaluate(`cos((${center[1]} * pi) / ${180})`); + + return metersPerUnit; +} + +function convertResolutionToMetersPerPixel(resolution, projection, center = [0, 0]) { + const metersPerUnit = getMetersPerUnit(projection, center); + + return evaluate(`${resolution} * ${metersPerUnit}`); +} + /* * Estimate appropriate Resolution based on zoom * This is only run if user has not already selected @@ -110,31 +139,36 @@ export function imageUtilEstimateResolution(resolution, isGeoProjection) { */ export function imageUtilCalculateResolution( zoom, - isGeoProjection, - resolutions, + proj, + center, ) { let resolution; + const isGeoProjection = proj.id === 'geographic'; + const { crs, resolutions } = proj.selected || proj; + const projection = get(crs); const nZoomLevels = resolutions.length; const currentZoom = zoom < 0 ? 0 : zoom; const curResolution = currentZoom >= nZoomLevels ? resolutions[nZoomLevels - 1] : resolutions[currentZoom]; - // Estimate the option value used by "wv-image-resolution" - const resolutionEstimate = imageUtilEstimateResolution( + const currResolutionInMeters = convertResolutionToMetersPerPixel( curResolution, - isGeoProjection, + projection, + center, ); + const getResolutions = (config) => config.values.map((res) => res.value); + // Find the closest match of resolution within the available values const possibleResolutions = isGeoProjection - ? [0.125, 0.25, 0.5, 1, 2, 4, 20, 40] - : [1, 2, 4, 20, 40]; + ? getResolutions(RESOLUTIONS_GEO) + : getResolutions(RESOLUTIONS_POLAR); let bestDiff = Infinity; let bestIdx = -1; let currDiff = 0; for (let i = 0; i < possibleResolutions.length; i += 1) { - currDiff = Math.abs(possibleResolutions[i] - resolutionEstimate); + currDiff = Math.abs(possibleResolutions[i] - currResolutionInMeters); if (currDiff < bestDiff) { resolution = possibleResolutions[i]; bestDiff = currDiff; @@ -168,7 +202,7 @@ export function imageUtilCalculateResolution( } } } - return resolution.toString(); + return resolution; } /* @@ -301,6 +335,40 @@ export function getTruncatedGranuleDates(layerDefs) { }); } +/** + * Calculate ground resolution from map state and target spatial resolution + * @param {Number} targetMetersPerPixel - Target spatial resolution in meters per pixel + * @param {Object} projection - Map projection + * @param {Number} mapResolution - Current map resolution + * @param {Array} center - Map center coordinates + * @returns {Number} - Scale factor to apply to map + */ +function calculateScaleFactor(targetMetersPerPixel, projection, mapResolution, center) { + const currentResolutionInMeters = convertResolutionToMetersPerPixel( + mapResolution, + projection, + center, + ); + + // Calculate scale factor needed to achieve target resolution + return evaluate(`${currentResolutionInMeters} / ${targetMetersPerPixel}`); +} + +export const estimateMaxCanvasSize = () => canvasSize.maxArea(); + +export async function estimateMaxImageSize() { + const { height: maxHeight, width: maxWidth } = await estimateMaxCanvasSize(); + + const devicePixelRatio = window.devicePixelRatio || 1; + const aoiMaxHeight = maxHeight / devicePixelRatio; + const aoiMaxWidth = maxWidth / devicePixelRatio; + + return { + height: Math.floor(aoiMaxHeight), + width: Math.floor(aoiMaxWidth), + }; +} + /** * Get the snapshots URL to download an image * @param {String} url @@ -370,7 +438,6 @@ export function getDownloadUrl( if (markerCoordinates.length > 0) { const coords = markerCoordinates.reduce((validCoords, { longitude: lon, latitude: lat }) => { const mCoord = transform([lon, lat], CRS.GEOGRAPHIC, crs); - // const inExtent = containsCoordinate(boundingExtent(bbox), mCoord); return validCoords.concat([mCoord[0], mCoord[1]]); }, []); params.push(`MARKER=${coords.join(',')}`); @@ -378,6 +445,614 @@ export function getDownloadUrl( return `${url}?${params.join('&')}&ts=${Date.now()}`; } +/** + * Convert a PNG image to a georeferenced KML file + * @param {Blob} pngBlob - The input PNG Blob + * @param {Object} options - Additional options for georeferencing + * @param {Array} options.extent - Bounding box [minX, minY, maxX, maxY] in map units + * @param {String} options.crs - The Coordinate Reference System identifier (e.g., 'EPSG:4326') + * @param {String} options.name - Optional name for the KML overlay (default: 'Image Overlay') + * @param {String} options.description - Optional description for the KML overlay + * @returns {Promise} - A promise that resolves to the KML Blob + */ +export function convertPngToKml(pngBlob, options) { + return new Promise((resolve, reject) => { + try { + // Validate input + if (!(pngBlob instanceof Blob)) { + reject(new Error('Input must be a Blob')); + return; + } + + // KML requires coordinates in EPSG:4326 (WGS84) + if (options.crs !== 'EPSG:4326') { + reject(new Error('KML requires WGS84 coordinates')); + return; + } + + const reader = new FileReader(); + + reader.onload = () => { + try { + // Extract the base64 part of the data URL + const dataUrl = reader.result; + const base64Data = dataUrl.split(',')[1]; + + const [minX, minY, maxX, maxY] = options.extent; + + // Create the KML document + const kmlContent = ` + + + ${options.name || 'Image Overlay'} + + ${options.name || 'Image Overlay'} + ${options.description ? `${options.description}` : ''} + + data:image/png;base64,${base64Data} + + + ${maxY} + ${minY} + ${maxX} + ${minX} + 0 + + + + `; + // Create the KML Blob + const kmlBlob = new Blob([kmlContent], { type: 'application/vnd.google-earth.kml+xml' }); + resolve(kmlBlob); + } catch (error) { + console.error('Error creating KML content:', error); + reject(error); + } + }; + + reader.onerror = () => reject(new Error('Failed to read image data')); + + reader.readAsDataURL(pngBlob); + } catch (error) { + console.error('Error creating KML:', error); + reject(error); + } + }); +} + +/** + * Convert a input Blob to a properly georeferenced GeoTIFF Blob + * @param {Blob} pngBlob - The input TIFF Blob + * @param {Object} options - Additional options for georeferencing + * @param {Array} options.extent - Bounding box [minX, minY, maxX, maxY] in map units + * @param {String} options.crs - The Coordinate Reference System identifier (e.g., 'EPSG:4326') + * @param {Number} options.metersPerPixel - Ground resolution in meters per pixel + * @param {Number} options.captureWidth - Width of the output image in pixels + * @param {Number} options.captureHeight - Height of the output image in pixels + * @param {String} options.inputFormat - Input image format (default: 'png') + * @param {String} options.outputFormat - Output image format (default: 'tif') + * @param {Boolean} options.worldfile - Whether to create a worldfile + * @param {String} options.name - Optional name for the output file + * @param {String} options.description - Optional description for the output file + * @returns {Promise} - A promise that resolves to the GeoTIFF Blob + */ +export async function georeference (inputBlob, options) { + let { + outputFormat = 'tif', + } = options; + const { + inputFormat = 'png', + crs = 'EPSG:4326', + captureHeight, + captureWidth, + } = options; + // Normalize tiff to tif for consistency + if (outputFormat === 'tiff' || outputFormat === 'geotiff') outputFormat = 'tif'; + // Normalize jpg to jpeg for consistency + if (outputFormat === 'jpeg') outputFormat = 'jpg'; + if (outputFormat === 'kml') { + const blob = await convertPngToKml(inputBlob, options); + + return [ + { + name: 'image.kml', + blob, + }, + ]; + } + const worldfile = options.worldfile ? 'YES' : 'NO'; + + const file = new File([inputBlob], `image.${inputFormat}`, { type: `image/${inputFormat}` }); + const gdal = await initGdalJs({ path: GDAL_WASM_PATH, useWorker: true }); + const openResult = await gdal.open(file); + const dataset = openResult.datasets[0]; + + const width = captureWidth || dataset.info.size[0]; + const height = captureHeight || dataset.info.size[1]; + const extent = options.extent.map((coord) => `${coord}`); + const driver = DRIVER_DICT[outputFormat]; + + const translateOpts = [ + '-strict', + '-of', driver, // Output format + '-a_srs', crs, // Set the spatial reference system + '-outsize', `${width}`, `${height}`, // Set the output size + '-r', 'average', // Resampling method + '-a_ullr', extent[0], extent[3], extent[2], extent[1], // Set the bounding box + ]; + if (driver !== 'GTiff') { + translateOpts.push('-co', `WORLDFILE=${worldfile}`); // Create a world file if requested + } else { + translateOpts.push('-co', `TFW=${worldfile}`); // Create ESRI tfw file + } + + // For JPEG output, ensure RGB color space by selecting + // only RGB bands and setting color interpretation + if (driver === 'JPEG') { + const jpegOpts = [ + '-co', 'QUALITY=75', // Maximum quality + '-b', '1', '-b', '2', '-b', '3', // Select only RGB bands, drop alpha + '-colorinterp', 'red,green,blue', // RGB color interpretation + ]; + translateOpts.push(...jpegOpts); + } + + const translate = await gdal.gdal_translate(dataset, translateOpts); + + gdal.close(dataset); + + const files = translate.all.map((p) => ({ path: p.local })); + const imageFilePath = files.find((f) => f.path.endsWith(`.${outputFormat}`))?.path; + const worldFilePath = files.find((f) => f.path.endsWith('.wld') || f.path.endsWith('.tfw'))?.path; + const imageFileBytes = await gdal.getFileBytes(imageFilePath); + const imageFileName = imageFilePath.split('/').pop(); + const imageFileBlob = new Blob([imageFileBytes]); + const output = [ + { + name: imageFileName, + blob: imageFileBlob, + }, + ]; + if (worldFilePath) { + const worldfileBytes = await gdal.getFileBytes(worldFilePath); + const worldFileName = worldFilePath.split('/').pop(); + const worldFileBlob = new Blob([worldfileBytes], { type: 'text/plain' }); + output.push({ + name: worldFileName, + blob: worldFileBlob, + }); + } + + return output; +} + +/** + * Update high-resolution tile grids for a specific layer + * @param {*} layer - The OpenLayers layer to update + * @returns {Function} - A function to restore the original tile grids + */ +function updateHighResTileGrids(layer, abortSignal, tileMatrixID = -1, onerror) { + const originalSource = layer.getSource(); + // No tile grid to update + if (typeof originalSource?.getTileGrid !== 'function') return () => null; + const SourceConstructor = originalSource.constructor; + const originalTileGrid = originalSource.getTileGrid(); + const TileGridConstructor = originalTileGrid.constructor; + const resolutions = originalTileGrid.getResolutions(); + // No matrix IDs to update + if (typeof originalTileGrid.getMatrixIds !== 'function') return () => null; + const matrixIds = originalTileGrid.getMatrixIds?.(); + const maxResolutions = resolutions.slice(0, tileMatrixID + 1); + const maxMatrixIds = matrixIds ? matrixIds.slice(0, tileMatrixID + 1) : undefined; + + const tileGrid = new TileGridConstructor({ + ...originalTileGrid, + origin: originalTileGrid.getOrigin?.() || originalTileGrid.origin_, + extent: originalTileGrid.getExtent?.() || originalTileGrid.extent_, + resolutions: maxResolutions, + matrixIds: maxMatrixIds, + tileSize: originalTileGrid.getTileSize?.() || originalTileGrid.tileSize_, + minZoom: tileMatrixID, + maxZoom: tileMatrixID, + }); + + const originalTileLoadFunction = originalSource.getTileLoadFunction?.() || + originalSource.tileLoadFunction_; + + const cancellableTileLoadFunction = async (tile, src) => { + if (tile.getState() === 2) return; + try { + tile.setState(olTileState.LOADING); + const response = await fetch(src, { signal: abortSignal }); + const blob = await response.blob(); + const imageUrl = URL.createObjectURL(blob); + await originalTileLoadFunction(tile, imageUrl); + URL.revokeObjectURL(imageUrl); + tile.setState(olTileState.LOADED); + } catch (error) { + if (error.name === 'AbortError') return; + tile.setState(olTileState.ERROR); + onerror(); + } + }; + const sourceOptions = { + ...originalSource, + urls: originalSource.getUrls?.() || originalSource.urls_, + format: originalSource.getFormat?.() || originalSource.format_, + projection: originalSource.getProjection?.() || originalSource.projection_, + tileGrid, + layer: originalSource.getLayer?.() || originalSource.layer_, + tileLoadFunction: cancellableTileLoadFunction, + matrixSet: originalSource.getMatrixSet?.() || originalSource.matrixSet_, + dimensions: originalSource.getDimensions?.() || originalSource.dimensions_, + crossOrigin: 'anonymous', + }; + + const hrSource = new SourceConstructor(sourceOptions); + layer.setSource(hrSource); + + return () => layer.setSource(originalSource); +} + +/** + * Toggle high-resolution tile grids for all layers in the map + * @param {Object} map - The OpenLayers map instance + * @returns {Function} - A function to restore the original tile grids + */ +function toggleHighResTileGrids (map, abortSignal, tileMatrixID, onerror) { + const layers = map.getAllLayers(); + const restoreSources = layers + .filter((layer) => layer.getVisible()) + .map((layer) => updateHighResTileGrids(layer, abortSignal, tileMatrixID, onerror)); + + return () => restoreSources.forEach((restoreSource) => restoreSource()); +} + +/** + * Create a restore function for the map and (optionally) configure high-resolution tile grids + * @param {Object} map + * @returns {Function} - A function to restore the original map state + */ +function createMapRestore(map, extent, abortSignal, tileMatrixID, onerror) { + const mapElement = map.getTargetElement(); + const originalView = map.getView(); + const ViewConstructor = originalView.constructor; + const newView = new ViewConstructor({ + center: originalView.getCenter(), + resolution: originalView.getResolution(), + projection: originalView.getProjection(), + zoom: originalView.getZoom(), + rotation: originalView.getRotation(), + maxZoom: originalView.getMaxZoom(), + minZoom: originalView.getMinZoom(), + extent: extent || originalView.getExtent(), + maxResolution: originalView.getMaxResolution(), + minResolution: originalView.getMinResolution(), + resolutions: originalView.getResolutions(), + multiWorld: false, + showFullExtent: true, + smoothResolutionConstraint: false, + }); + map.setView(newView); + const originalStyleWidth = mapElement.style.width; + const originalStyleHeight = mapElement.style.height; + + const restoreLayers = toggleHighResTileGrids(map, abortSignal, tileMatrixID, onerror); + + return () => { + // Restore original map size + restoreLayers(); + map.setView(originalView); + mapElement.style.width = originalStyleWidth; + mapElement.style.height = originalStyleHeight; + map.updateSize(); + }; +} + +/** + * Initiates a download and waits for a reasonable delay to simulate download completion + * @param {Blob} blob - The blob to download + * @param {String} filename - The filename for the download + * @param {AbortSignal} abortSignal - Optional abort signal + * @returns {Promise} - Resolves when download is initiated and delay is complete + */ +async function initiateDownload(blob, filename, abortSignal, parentReject) { + // Wait for download to initiate with cancellation support + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(resolve, 0); + + if (abortSignal) { + const abortHandler = () => { + clearTimeout(timeoutId); + reject(new DOMException('Snapshot operation was cancelled', 'AbortError')); + }; + + if (abortSignal.aborted) { + clearTimeout(timeoutId); + reject(new DOMException('Snapshot operation was cancelled', 'AbortError')); + return; + } + + abortSignal.addEventListener('abort', abortHandler, { once: true }); + + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + URL.revokeObjectURL(url); + + // Clean up the event listener when the timeout completes + setTimeout(() => { + abortSignal.removeEventListener('abort', abortHandler); + }, 0); + } + }); +} + +function getExtentFromPixelBbox(pixelBbox, map) { + const [minPixelX, minPixelY, maxPixelX, maxPixelY] = pixelBbox; + + // Calculate geographic extent + const topLeft = map.getCoordinateFromPixel([minPixelX, minPixelY]); + const topRight = map.getCoordinateFromPixel([maxPixelX, minPixelY]); + const bottomLeft = map.getCoordinateFromPixel([minPixelX, maxPixelY]); + const bottomRight = map.getCoordinateFromPixel([maxPixelX, maxPixelY]); + + // Put everything in the correct order + const minX = Math.min(topLeft[0], bottomLeft[0]); + const maxX = Math.max(topRight[0], bottomRight[0]); + const minY = Math.min(bottomLeft[1], bottomRight[1]); + const maxY = Math.max(topLeft[1], topRight[1]); + const extent = [minX, minY, maxX, maxY]; + + return extent; +} + +async function fitViewToExtent(map, extent) { + return new Promise((resolve) => { + const view = map.getView(); + // the callback option in view.fit is called before the view is actually fitted in safari, + // so we need to wait for the render complete event + map.once('rendercomplete', () => resolve()); + view.fit(extent, { callback: () => map.render() }); + }); +} + +async function waitForRenderComplete(map) { + return new Promise((resolve) => { + map.once('rendercomplete', () => resolve()); + map.render(); + }); +} + +// Classnames to ignore for rendering in snapshot +const classesToIgnore = [ + 'wv-map-scale-imperial', + 'wv-map-scale-metric', +]; + +// Helper function for which elements to ignore for snapshot rendering +function ignoreElementsFunc(element) { + return classesToIgnore.some((className) => element.classList.contains(className)); +} + +/** + * Create a snapshot of the map with the given options + * @param {Object} options - Snapshot configuration options + * @param {String} options.format - Output format (e.g., 'tif', 'png', 'kmz') + * @param {Number} options.metersPerPixel - Target spatial resolution in meters per pixel + * @param {Array} options.pixelBbox - Pixel bounding box [minX, minY, maxX, maxY] + * @param {Object} options.map - OpenLayers map instance + * @param {Boolean} options.worldfile - Whether to include a worldfile + * @param {Boolean} options.useHighResTileGrids - Whether to use high resolution tile grids + * @param {AbortSignal} options.abortSignal - Optional AbortController signal to cancel operation + * @returns {Promise} - Promise that resolves when snapshot is complete + * @throws {DOMException} - Throws AbortError if the operation is cancelled + */ +export async function snapshot(options) { + const { height: maxHeight = 0, width: maxWidth = 0 } = await estimateMaxCanvasSize(); + + const { + format, + metersPerPixel, + pixelBbox, + map, + worldfile, + abortSignal, + filename = 'Worldview Snapshot', + projection, + onerror, + width, + height, + } = options; + + // Check if operation was cancelled before starting + if (abortSignal?.aborted) { + throw new DOMException('Snapshot operation was cancelled before starting', 'AbortError'); + } + + // Save original viewport size + const [originalWidth, originalHeight] = map.getSize(); + const extent = getExtentFromPixelBbox(pixelBbox, map); + + const isGeoProjection = projection.id === 'geographic'; + + const config = isGeoProjection ? RESOLUTIONS_GEO : RESOLUTIONS_POLAR; + const tileMatrixID = config.values.find((res) => res.value === metersPerPixel)?.tileMatrixID; + + // Create a restore function for the map state. + // This also manages the use of high-res tilegrids for the layers. + const restoreMap = createMapRestore(map, extent, abortSignal, tileMatrixID, onerror); + const view = map.getView(); + + // Add abort event listener to clean up if cancelled + const abortHandler = () => { + restoreMap(); + }; + + if (abortSignal) { + abortSignal.addEventListener('abort', abortHandler); + } + + await fitViewToExtent(map, extent); + + const viewResolution = view.getResolution(); + + // Calculate scale factor based on target spatial resolution + const center = view.getCenter(); + const scaleFactor = calculateScaleFactor( + metersPerPixel, + view.getProjection(), + viewResolution, + center, + ); + + // Scale the entire map up to the target resolution + const scaledMapWidth = evaluate(`${originalWidth} * ${scaleFactor}`); + const scaledMapHeight = evaluate(`${originalHeight} * ${scaleFactor}`); + const devicePixelRatio = window.devicePixelRatio || 1; + const scaledMapWidthWithDPR = evaluate(`${scaledMapWidth} * ${devicePixelRatio}`); + const scaledMapHeightWithDPR = evaluate(`${scaledMapHeight} * ${devicePixelRatio}`); + + if (scaledMapWidthWithDPR > maxWidth || scaledMapHeightWithDPR > maxHeight) { + throw new Error(`Scaled area exceeds maximum allowed size: ${maxWidth}x${maxHeight}. + Current size: ${Math.floor(scaledMapWidthWithDPR)}x${Math.floor(scaledMapHeightWithDPR)}.`); + } + + const scaledResolution = evaluate(`${viewResolution} / ${scaleFactor}`); + const mapElement = map.getTargetElement(); + + map.setSize([scaledMapWidth, scaledMapHeight]); + mapElement.style.width = `${scaledMapWidth}px`; + mapElement.style.height = `${scaledMapHeight}px`; + map.updateSize(); + view.setResolution(scaledResolution); + + await waitForRenderComplete(map); + // Allows time for tracks to re-adjust position/size + await new Promise((resolve) => { + setTimeout(resolve, 250); + }); + + const topLeft = olExtent.getTopLeft(extent); + const bottomLeft = olExtent.getBottomLeft(extent); + const topRight = olExtent.getTopRight(extent); + + const aoiPixelTopLeft = map.getPixelFromCoordinate(topLeft); + const aoiPixelBottomLeft = map.getPixelFromCoordinate(bottomLeft); + const aoiPixelTopRight = map.getPixelFromCoordinate(topRight); + + const aoiPixelXOffset = aoiPixelTopLeft[0]; + const aoiPixelYOffset = aoiPixelTopLeft[1]; + const aoiPixelWidth = Math.abs(evaluate(`${aoiPixelTopRight[0]} - ${aoiPixelTopLeft[0]}`)); + const aoiPixelHeight = Math.abs(evaluate(`${aoiPixelBottomLeft[1]} - ${aoiPixelTopLeft[1]}`)); + + const [mapWidth, mapHeight] = map.getSize(); + + const dpr = window.devicePixelRatio || 1; + + // Create our output canvas with exact dimensions we want + const outputWidth = evaluate(`${aoiPixelWidth} * ${dpr}`); + const outputHeight = evaluate(`${aoiPixelHeight} * ${dpr}`); + const outputCanvas = new OffscreenCanvas(outputWidth, outputHeight); + + const ctx = outputCanvas.getContext('2d'); + ctx.imageSmoothingEnabled = false; // Disable smoothing for pixel-perfect rendering + + // Scale the context to ensure correct drawing operations + ctx.scale(dpr, dpr); + + const capturedCanvas = new OffscreenCanvas(mapWidth * dpr, mapHeight * dpr); + + // Capture the map at its new scaled size + await html2canvas(mapElement, { + canvas: capturedCanvas, + backgroundColor: null, + useCORS: true, + allowTaint: true, + scrollX: 0, + scrollY: 0, + scale: dpr, + logging: false, + imageTimeout: 0, + removeContainer: true, + ignoreElements: ignoreElementsFunc, + }); + + const sourceX = evaluate(`${aoiPixelXOffset} * ${dpr}`); + const sourceY = evaluate(`${aoiPixelYOffset} * ${dpr}`); + const sourceWidth = outputCanvas.width; // Use the actual width of the output canvas + const sourceHeight = outputCanvas.height; // Use the actual height of the output canvas + + const capturedCtx = capturedCanvas.getContext('2d'); + capturedCtx.imageSmoothingEnabled = false; // Disable smoothing for pixel-perfect rendering + const capturedImageData = capturedCtx.getImageData( + Math.round(sourceX), // source x + sourceY, // source y + sourceWidth, // source width + sourceHeight, // source height + { colorSpace: 'srgb' }, + ); + + ctx.colorSpace = 'srgb'; + + ctx.putImageData( + capturedImageData, + 0, // dest x + 0, // dest y + 0, // source x + 0, // source y + sourceWidth, // dest width + sourceHeight, // dest height + ); + + // Reset map to original size + restoreMap(); + + const pngBlob = await outputCanvas.convertToBlob({ + type: 'image/png', + quality: 1, // Maximum quality + }); + + const crs = map.getView() + .getProjection() + .getCode(); + + const georeferencedOutput = await georeference(pngBlob, { + extent, + crs, + metersPerPixel, + captureWidth: width, + captureHeight: height, + inputFormat: 'png', + outputFormat: format === 'kmz' ? 'kml' : format, + worldfile, + name: 'Worldview Snapshot', + description: 'Snapshot created with NASA Worldview', + }); + + if (georeferencedOutput.length > 1 || format === 'kmz') { + const zip = new JSZip(); + georeferencedOutput.forEach(({ name, blob }) => zip.file(name, blob)); + const zipBlob = await zip.generateAsync({ + type: 'blob', + compression: 'DEFLATE', + compressionOptions: { level: 9 }, + mimeType: format !== 'kmz' ? 'application/zip' : 'application/vnd.google-earth.kmz', + }); + + return initiateDownload( + zipBlob, + `${filename}.${format !== 'kmz' ? 'zip' : 'kmz'}`, + abortSignal, + ); + } + + const { blob } = georeferencedOutput[0]; + return initiateDownload(blob, `${filename}.${format}`, abortSignal); +} + export function imageUtilGetConversionFactor(proj) { if (proj === 'geographic') return POLAR_ESTIMATION_CONSTANT; return GEO_ESTIMATION_CONSTANT; @@ -408,28 +1083,52 @@ export function imageUtilGetPixelValuesFromCoords(bottomLeft, topRight, map) { }; } -export function imageSizeValid(imgHeight, imgWidth, maxSize) { - if (imgHeight === 0 && imgWidth === 0) { - return false; - } - if (imgHeight > maxSize || imgWidth > maxSize) { - return false; - } +export function imageSizeValid(options) { + const { + maxHeight, + maxWidth, + map, + resolution, + pixelBbox, + } = options; + + const imgWidth = Math.abs(pixelBbox[1][0] - pixelBbox[0][0]); + const imgHeight = Math.abs(pixelBbox[1][1] - pixelBbox[0][1]); + const view = map.getView(); + const projection = view.getProjection(); + const center = view.getCenter(); + const extent = getExtentFromPixelBbox(pixelBbox, map); + const viewResolution = view.getResolutionForExtent(extent); + + const scaleFactor = calculateScaleFactor( + resolution, + projection, + viewResolution, + center, + ); + + const mapSize = map.getSize(); + const [scaledWidth, scaledHeight] = mapSize.map((size) => size * scaleFactor); + + const isZero = imgHeight === 0 && imgWidth === 0; + const isTooBig = scaledWidth > maxHeight || scaledHeight > maxWidth; + + if (isZero || isTooBig) return false; + return true; } -export function getDimensions(projection, bounds, resolution) { - const conversionFactor = imageUtilGetConversionFactor(projection); - const imgWidth = Math.round( - Math.abs(bounds[1][0] - bounds[0][0]) / - conversionFactor / - Number(resolution), - ); - const imgHeight = Math.round( - Math.abs(bounds[1][1] - bounds[0][1]) / - conversionFactor / - Number(resolution), - ); +export function getDimensions(map, bounds, resolution) { + const projection = map.getView().getProjection(); + const center = map.getView().getCenter(); + const metersPerUnit = getMetersPerUnit(projection, center); + + const mapWidth = Math.abs(bounds[1][0] - bounds[0][0]) * metersPerUnit; + const mapHeight = Math.abs(bounds[1][1] - bounds[0][1]) * metersPerUnit; + + const imgWidth = Math.round(mapWidth / resolution); + const imgHeight = Math.round(mapHeight / resolution); + return { width: imgWidth, height: imgHeight }; } export function getPercentageFromPixel(maxDimension, dimension) { @@ -442,11 +1141,16 @@ export function getPixelFromPercentage(maxDimension, percent) { /** * Find if there are layers that cannot be downloaded * @param {Array} visibleLayers + * @param {boolean} isFromSnapshot * * @return {Bool} */ -export function hasNonDownloadableVisibleLayer(visibleLayers) { - return visibleLayers.some(({ disableSnapshot = false }) => disableSnapshot); +export function hasNonDownloadableVisibleLayer(visibleLayers, isFromSnapshot = false) { + // Only needed during transition period where image snapshot is frontend and + // gif creation is backend. Should be removed when both are frontend, + // and the actual layer objects updated to remove disableSnapshot as needed + if (isFromSnapshot) return visibleLayers.some(({ disableSnapshot = false }) => disableSnapshot); + return visibleLayers.some(({ disableGIF = false }) => disableGIF); } /** * Get string of layers to be removed if alert is accepted @@ -467,25 +1171,33 @@ export function getNamesOfNondownloadableLayers(nonDownloadableLayers) { /** * Get warning that shows layers that will be removed if notification is accepted * @param {Array} nonDownloadableLayers + * @param {boolean} isFromSnapshot * * @return {String} */ -export function getNonDownloadableLayerWarning(nonDownloadableLayer) { +export function getNonDownloadableLayerWarning(nonDownloadableLayer, isFromSnapshot = false) { const layerStr = getNamesOfNondownloadableLayers(nonDownloadableLayer); if (!layerStr) return ''; const multiLayers = layerStr.indexOf(',') > -1; const layerPluralStr = multiLayers ? 'layers' : 'layer'; const thisTheseStr = multiLayers ? 'these' : 'this'; - return `The ${layerStr} ${layerPluralStr} cannot be included in a snapshot. Would you like to temporarily hide ${thisTheseStr} layer?`; + const typeName = isFromSnapshot ? 'snapshot' : 'GIF'; + return `The ${layerStr} ${layerPluralStr} cannot be included in a ${typeName}. + Would you like to temporarily hide ${thisTheseStr} layer?`; } /** * Get array of layers that will be removed if notification is accepted * @param {Array} visibleLayers + * @param {boolean} isFromSnapshot * * @return {Array} */ -export function getNonDownloadableLayers(visibleLayers) { - return visibleLayers.filter(({ disableSnapshot = false }) => disableSnapshot); +export function getNonDownloadableLayers(visibleLayers, isFromSnapshot = false) { + // Only needed during transition period where image snapshot is frontend and + // gif creation is backend. Should be removed when both are frontend, + // and the actual layer objects updated to remove disableSnapshot as needed + if (isFromSnapshot) return visibleLayers.filter(({ disableSnapshot = false }) => disableSnapshot); + return visibleLayers.filter(({ disableGIF = false }) => disableGIF); } /** * Get dateline message for selecting snapshot area @@ -505,10 +1217,14 @@ export function getAlertMessageIfCrossesDateline(date, geolonlat1, geolonlat2, p const zeroedDate = util.clearTimeUTC(date); const nextDay = formatDisplayDate(util.dateAdd(zeroedDate, 'day', 1)); const prevDay = formatDisplayDate(util.dateAdd(zeroedDate, 'day', -1)); - const buildString = (lineStr, dateStr) => `The selected snapshot area crosses ${lineStr} and uses imagery from the ${dateStr}.`; + const buildString = (lineStr, dateStr) => + `The selected snapshot area crosses ${lineStr} and uses imagery from the ${dateStr}.`; if (crossesNextDay && crossesPrevDay) { // snapshot extends over both map wings - alertMessage = buildString('both datelines', `previous day ${prevDay} and next day ${nextDay}`); + alertMessage = buildString( + 'both datelines', + `previous day ${prevDay} and next day ${nextDay}`, + ); } else if (crossesNextDay) { // min longitude less than maxExtent min longitude (-180 geographic) alertMessage = buildString('the dateline', `next day ${nextDay}`); diff --git a/web/js/modules/image-download/util.test.js b/web/js/modules/image-download/util.test.js index 630774e5d3..2b2c66ae81 100644 --- a/web/js/modules/image-download/util.test.js +++ b/web/js/modules/image-download/util.test.js @@ -21,8 +21,24 @@ import { imageUtilGetCoordsFromPixelValues, imageUtilGetPixelValuesFromCoords, GRANULE_LIMIT, + convertPngToKml, + georeference, + snapshot, + estimateMaxCanvasSize, + estimateMaxImageSize, } from './util'; +jest.mock('ol/proj', () => ({ + ...jest.requireActual('ol/proj'), + get: jest.fn((projString) => { + if (projString === 'EPSG:4326') { + return { getUnits: () => 'degrees', getMetersPerUnit: () => 111319.49079327358 }; + } else { + return { getUnits: () => 'm', getMetersPerUnit: () => 1 }; + } + }), +})); + const geoResolutions = [ 0.5625, 0.28125, @@ -203,47 +219,69 @@ test('bboxWMS13 [imagedownload-bbox]', () => { test('Default km resolution Calculation [imagedownload-default-resolution]', () => { const zoom = 5; - const isGeo = true; - expect(imageUtilCalculateResolution(zoom, isGeo, geoResolutions)).toBe('4'); + const proj = { + id: 'geographic', + selected: { + crs: 'EPSG:4326', + resolutions: geoResolutions, + }, + }; + const center = [0, 0]; + expect(imageUtilCalculateResolution(zoom, proj, center)).toBe(1000); }); test('Resolution calculation with negative zoom clamps to 0 [imagedownload-resolution-negative-zoom]', () => { - expect(imageUtilCalculateResolution(-1, true, geoResolutions)).toBe('40'); + const proj = { selected: { id: 'geographic', crs: 'EPSG:4326', resolutions: geoResolutions } }; + const center = [0, 0]; + expect(imageUtilCalculateResolution(-1, proj, center)).toBe(10000); }); test('Resolution calculation with zoom beyond max clamps to last resolution [imagedownload-resolution-zoom-overflow]', () => { - expect(imageUtilCalculateResolution(999, true, geoResolutions)).toBe('0.125'); + const proj = { selected: { id: 'geographic', crs: 'EPSG:4326', resolutions: geoResolutions } }; + const center = [0, 0]; + expect(imageUtilCalculateResolution(999, proj, center)).toBe(250); }); test('Resolution calculation for polar projection [imagedownload-resolution-polar]', () => { - const polarResolutions = [500, 250, 125, 62.5, 31.25]; - expect(imageUtilCalculateResolution(1, false, polarResolutions)).toBe('1'); + const proj = { selected: { id: 'arctic', crs: 'EPSG:3413', resolutions: [500, 250, 125, 62.5, 31.25] } }; + const center = [0, 0]; + expect(imageUtilCalculateResolution(1, proj, center)).toBe(250); }); test('Resolution calculation bumps up at geo zoom 3 [imagedownload-resolution-zoom3]', () => { - expect(imageUtilCalculateResolution(3, true, geoResolutions)).toBe('20'); + const proj = { selected: { id: 'geographic', crs: 'EPSG:4326', resolutions: geoResolutions } }; + const center = [0, 0]; + expect(imageUtilCalculateResolution(3, proj, center)).toBe(10000); }); test('Resolution calculation bumps up at geo zoom 4 [imagedownload-resolution-zoom4]', () => { - expect(imageUtilCalculateResolution(4, true, geoResolutions)).toBe('4'); + const proj = { selected: { id: 'geographic', crs: 'EPSG:4326', resolutions: geoResolutions } }; + const center = [0, 0]; + expect(imageUtilCalculateResolution(4, proj, center)).toBe(1000); }); test('Resolution calculation bumps up at geo zoom 6 [imagedownload-resolution-zoom6]', () => { - expect(imageUtilCalculateResolution(6, true, geoResolutions)).toBe('2'); + const proj = { selected: { id: 'geographic', crs: 'EPSG:4326', resolutions: geoResolutions } }; + const center = [0, 0]; + expect(imageUtilCalculateResolution(6, proj, center)).toBe(1000); }); test('Resolution calculation bumps up at geo zoom 7 [imagedownload-resolution-zoom7]', () => { - expect(imageUtilCalculateResolution(7, true, geoResolutions)).toBe('1'); + const proj = { selected: { id: 'geographic', crs: 'EPSG:4326', resolutions: geoResolutions } }; + const center = [0, 0]; + expect(imageUtilCalculateResolution(7, proj, center)).toBe(500); }); -test('Resolution calculation bumps up at polar zoom 1 [imagedownload-resolution-polar-zoom1]', () => { - const polarResolutions = [500, 250, 125, 62.5, 31.25]; - expect(imageUtilCalculateResolution(1, false, polarResolutions)).toBe('1'); +test('Resolution calculation bumps up at polar zoom 0 [imagedownload-resolution-polar-zoom0]', () => { + const proj = { selected: { id: 'arctic', crs: 'EPSG:3413', resolutions: [500, 250, 125, 62.5, 31.25] } }; + const center = [0, 0]; + expect(imageUtilCalculateResolution(0, proj, center)).toBe(500); }); test('Resolution calculation bumps up at polar zoom 2 [imagedownload-resolution-polar-zoom2]', () => { - const polarResolutions = [500, 250, 125, 62.5, 31.25]; - expect(imageUtilCalculateResolution(2, false, polarResolutions)).toBe('1'); + const proj = { selected: { id: 'arctic', crs: 'EPSG:3413', resolutions: [500, 250, 125, 62.5, 31.25] } }; + const center = [0, 0]; + expect(imageUtilCalculateResolution(2, proj, center)).toBe(250); }); // ------- imageUtilEstimateResolution ------- @@ -399,9 +437,20 @@ test('getTruncatedGranuleDates truncates when over GRANULE_LIMIT [imagedownload- // ------- getDimensions ------- +const mockMap = { + getView: () => ({ + getProjection: () => ({ + getUnits: () => 'degrees', + getMetersPerUnit: () => 111000, + }), + getCenter: () => [0, 0], + }), +}; +const resolution = 1000; + test('getDimensions returns correct width and height for geographic [imagedownload-dimensions-geo]', () => { const bounds = [[0, 0], [1, 1]]; - const result = getDimensions('geographic', bounds, 1); + const result = getDimensions(mockMap, bounds, resolution); expect(result).toHaveProperty('width'); expect(result).toHaveProperty('height'); expect(result.width).toBeGreaterThan(0); @@ -410,7 +459,7 @@ test('getDimensions returns correct width and height for geographic [imagedownlo test('getDimensions returns correct width and height for polar [imagedownload-dimensions-polar]', () => { const bounds = [[0, 0], [1000000, 1000000]]; - const result = getDimensions('arctic', bounds, 1); + const result = getDimensions(mockMap, bounds, resolution); expect(result.width).toBeGreaterThan(0); expect(result.height).toBeGreaterThan(0); }); @@ -428,18 +477,46 @@ test('getPixelFromPercentage calculates correct pixel [imagedownload-pixel-from- }); // ------- imageSizeValid ------- - -test('imageSizeValid returns false when both dimensions are 0 [imagedownload-size-valid-zero]', () => { - expect(imageSizeValid(0, 0, 8200)).toBe(false); -}); - -test('imageSizeValid returns false when a dimension exceeds maxSize [imagedownload-size-valid-exceed]', () => { - expect(imageSizeValid(9000, 100, 8200)).toBe(false); - expect(imageSizeValid(100, 9000, 8200)).toBe(false); -}); - -test('imageSizeValid returns true for valid dimensions [imagedownload-size-valid-ok]', () => { - expect(imageSizeValid(500, 500, 8200)).toBe(true); +test('should return false for zero size', () => { + const mockMap = { + getView: () => ({ + getProjection: () => ({ getUnits: () => 'degrees', getMetersPerUnit: () => 111000 }), + getCenter: () => [0, 0], + getResolutionForExtent: () => 1000, + }), + getSize: () => [1000, 1000], + getCoordinateFromPixel: jest.fn().mockReturnValue([0, 0]), + }; + const options = { + maxHeight: 8192, + maxWidth: 8192, + map: mockMap, + resolution: 1000, + pixelBbox: [[0, 0], [0, 0]], + }; + const result = imageSizeValid(options); + expect(result).toBe(false); +}); + +test('should return false for oversized image', () => { + const mockMapLarge = { + getView: () => ({ + getProjection: () => ({ getUnits: () => 'degrees', getMetersPerUnit: () => 111000 }), + getCenter: () => [0, 0], + getResolutionForExtent: () => 1000, + }), + getSize: () => [10000, 10000], + getCoordinateFromPixel: jest.fn().mockReturnValue([0, 0]), + }; + const options = { + maxHeight: 1000, + maxWidth: 1000, + map: mockMapLarge, + resolution: 1, + pixelBbox: [[0, 0], [100, 100]], + }; + const result = imageSizeValid(options); + expect(result).toBe(false); }); // ------- hasNonDownloadableVisibleLayer ------- @@ -447,7 +524,7 @@ test('imageSizeValid returns true for valid dimensions [imagedownload-size-valid test('hasNonDownloadableVisibleLayer returns true when a layer has disableSnapshot [imagedownload-has-non-dl-true]', () => { expect(hasNonDownloadableVisibleLayer([ { disableSnapshot: true }, { disableSnapshot: false }, - ])).toBe(true); + ], true)).toBe(true); }); test('hasNonDownloadableVisibleLayer returns false when no layers have disableSnapshot [imagedownload-has-non-dl-false]', () => { @@ -458,7 +535,7 @@ test('hasNonDownloadableVisibleLayer returns false when no layers have disableSn test('getNonDownloadableLayers filters to only disableSnapshot layers [imagedownload-get-non-dl-layers]', () => { const layers = [{ id: 'A', disableSnapshot: true }, { id: 'B' }, { id: 'C', disableSnapshot: false }]; - expect(getNonDownloadableLayers(layers)).toEqual([{ id: 'A', disableSnapshot: true }]); + expect(getNonDownloadableLayers(layers, true)).toEqual([{ id: 'A', disableSnapshot: true }]); }); test('getNonDownloadableLayers returns empty array when none disabled [imagedownload-get-non-dl-empty]', () => { @@ -701,3 +778,117 @@ test('Download URL includes granule_dates when granuleDates present [imagedownlo test('GRANULE_LIMIT is 30 [imagedownload-granule-limit]', () => { expect(GRANULE_LIMIT).toBe(30); }); + +// Tests for coordinate conversion functions +describe('coordinate conversion functions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('getDimensions should calculate image dimensions', () => { + const mockMap = { + getView: () => ({ + getProjection: () => ({ + getUnits: () => 'degrees', + getMetersPerUnit: () => 111000, + }), + getCenter: () => [0, 0], + }), + }; + const bounds = [[0, 0], [1, 1]]; + const resolution = 1000; + + const result = getDimensions(mockMap, bounds, resolution); + expect(result).toHaveProperty('width'); + expect(result).toHaveProperty('height'); + }); +}); + +// Tests for canvas size estimation +describe('canvas size functions', () => { + test('estimateMaxCanvasSize should return a function', () => { + const result = estimateMaxCanvasSize(); + expect(typeof result).toBe('object'); + }); + + test('estimateMaxImageSize should be an async function', async () => { + // Mock canvas-size to avoid environment issues + jest.doMock('canvas-size', () => ({ + maxArea: () => Promise.resolve({ height: 8192, width: 8192 }), + })); + + // This test would need proper mocking of canvas-size in a real test environment + expect(estimateMaxImageSize).toBeInstanceOf(Function); + }); +}); + +// Tests for convertPngToKml +describe('convertPngToKml', () => { + test('should reject non-Blob input', async () => { + await expect(convertPngToKml('not a blob', {})).rejects.toThrow('Input must be a Blob'); + }); + + test('should reject non-WGS84 CRS', async () => { + const blob = new Blob(['fake png data'], { type: 'image/png' }); + const options = { crs: 'EPSG:3857', extent: [0, 0, 1, 1] }; + await expect(convertPngToKml(blob, options)).rejects.toThrow('KML requires WGS84 coordinates'); + }); + + test('should create KML with valid inputs', async () => { + const blob = new Blob(['fake png data'], { type: 'image/png' }); + const options = { + crs: 'EPSG:4326', + extent: [-180, -90, 180, 90], + name: 'Test Image', + }; + + const result = await convertPngToKml(blob, options); + expect(result).toBeInstanceOf(Blob); + expect(result.type).toBe('application/vnd.google-earth.kml+xml'); + }); +}); + +// Mock tests for async functions that require complex dependencies +describe('complex async functions (mocked)', () => { + beforeEach(() => { + // Reset all mocks before each test + jest.clearAllMocks(); + }); + + describe('georeference', () => { + test('should handle KML output format', async () => { + // Mock convertPngToKml to avoid complex dependencies + const mockKmlBlob = new Blob([''], { type: 'application/vnd.google-earth.kml+xml' }); + jest.doMock('./util', () => ({ + ...jest.requireActual('./util'), + convertPngToKml: jest.fn().mockResolvedValue(mockKmlBlob), + })); + + // This would need proper mocking of GDAL in a real test environment + // For now, we're just testing that the function exists and is async + expect(georeference).toBeInstanceOf(Function); + }); + }); + + describe('snapshot', () => { + test('should throw AbortError when already aborted', async () => { + const abortController = new AbortController(); + abortController.abort(); + + const options = { + format: 'png', + metersPerPixel: 1000, + pixelBbox: [0, 0, 100, 100], + map: { getSize: () => [1000, 1000] }, + abortSignal: abortController.signal, + }; + + await expect(snapshot(options)).rejects.toThrow(DOMException); + }); + + test('should be an async function', () => { + expect(snapshot).toBeInstanceOf(Function); + expect(snapshot.constructor.name).toBe('AsyncFunction'); + }); + }); +}); diff --git a/web/scss/components/image.scss b/web/scss/components/image.scss index 008ce2cf64..862f742276 100644 --- a/web/scss/components/image.scss +++ b/web/scss/components/image.scss @@ -79,7 +79,7 @@ .wv-image-coords { position: absolute; - z-index: 4; + z-index: 1; white-space: pre; color: #fff; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; @@ -145,3 +145,30 @@ right: 20px; width: 5px; } + +.modal-dialog.snapshot-error { + position: absolute; + width: 25%; + left: 37.5%; + top: 160px; +} + +.wv-snapshot-warning { + margin: 4px; + margin-bottom: 8px; + line-height: 19px !important; +} + +.wv-snapshot-warning-icon { + font-size: 1.2em; + margin-right: 8px; +} + +.snapshot-feedback { + color: #7bf !important; + cursor: pointer; +} + +.snapshot-feedback:hover { + text-decoration: underline !important; +} diff --git a/web/scss/features/events.scss b/web/scss/features/events.scss index 5c3bb60512..da78943c32 100644 --- a/web/scss/features/events.scss +++ b/web/scss/features/events.scss @@ -193,7 +193,6 @@ display: block; height: 26px; width: 26px; - background: url("../images/natural-events/dot.svg"); } .marker.selected { @@ -204,7 +203,6 @@ transform-origin: center bottom; position: relative; height: 31px; - background: url("../images/natural-events/pin.svg"); } } @@ -389,52 +387,18 @@ display: block; height: 30px; width: 30px; - background: url("../images/natural-events/icon-critical.svg"); +} + +.event-icon-pin { + transform: scale(1.175); + transform-origin: center top; + position: absolute; } .wv-map .event-icon { height: 26px; width: 26px; -} - -.event-icon-dustHaze { - background: url("../images/natural-events/icon-dust-and-haze.svg"); -} - -.event-icon-icebergs { - background: url("../images/natural-events/icon-icebergs.svg"); -} - -.event-icon-floods { - background: url("../images/natural-events/icon-floods.svg"); -} - -.event-icon-manmade { - background: url("../images/natural-events/icon-manmade.svg"); -} - -.event-icon-seaLakeIce { - background: url("../images/natural-events/icon-icebergs.svg"); -} - -.event-icon-severeStorms { - background: url("../images/natural-events/icon-severe-storms.svg"); -} - -.event-icon-snow { - background: url("../images/natural-events/icon-snow.svg"); -} - -.event-icon-volcanoes { - background: url("../images/natural-events/icon-volcanoes.svg"); -} - -.event-icon-waterColor { - background: url("../images/natural-events/icon-water-color.svg"); -} - -.event-icon-wildfires { - background: url("../images/natural-events/icon-wildfires.svg"); + position: absolute; } .events-loading-text { diff --git a/webpack.config.js b/webpack.config.js index b1be38aa4e..734ee603a1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -7,6 +7,7 @@ const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') const MomentLocalesPlugin = require('moment-locales-webpack-plugin') const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin') const CssUrlRelativePlugin = require('css-url-relative-plugin') +const CopyWebpackPlugin = require('copy-webpack-plugin') const devMode = process.env.NODE_ENV !== 'production' @@ -42,6 +43,16 @@ if (process.env.DEBUG !== undefined) { ) } +pluginSystem.push( + new CopyWebpackPlugin({ + patterns: [ + { from: './node_modules/gdal3.js/dist/package/gdal3.js', to: 'gdal3js' }, + { from: './node_modules/gdal3.js/dist/package/gdal3WebAssembly.wasm', to: 'gdal3js' }, + { from: './node_modules/gdal3.js/dist/package/gdal3WebAssembly.data', to: 'gdal3js' } + ] + }) +) + const babelLoaderExcludes = [ /\.test\.js$/, /fixtures\.js$/,