diff --git a/package-lock.json b/package-lock.json index 5bd3b97..9a9dd1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@arcgis/core": "^4.26.5", + "@arcgis/core": "4.28", "@entryline/gifstream": "^1.2.1", "@esri/arcgis-rest-feature-service": "^4.0.4", "@esri/arcgis-rest-request": "^4.2.0", @@ -93,16 +93,17 @@ } }, "node_modules/@arcgis/core": { - "version": "4.26.5", - "resolved": "https://registry.npmjs.org/@arcgis/core/-/core-4.26.5.tgz", - "integrity": "sha512-Z8KoJrTD1ZQwVVZAIpkdjAQVT/MjAaWAr5u0krifKd6Y1m0TrEb8/iK86KvyWX8yNNWEroEP9SiE19vH/JKquQ==", + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/@arcgis/core/-/core-4.28.9.tgz", + "integrity": "sha512-UpWcWsBg+/yhfjzPx45+iiCJXhPWx5F+xB/sdMai1mfMvwlAxbsL9YzbLM/S4CawVpEPwC3i5ZGEgp17gxu7Kg==", "dependencies": { "@esri/arcgis-html-sanitizer": "~3.0.1", "@esri/calcite-colors": "~6.1.0", - "@esri/calcite-components": "~1.0.7", - "@popperjs/core": "~2.11.6", - "focus-trap": "~7.2.0", - "luxon": "~3.2.1", + "@esri/calcite-components": "^1.9.2", + "@popperjs/core": "~2.11.8", + "@zip.js/zip.js": "~2.7.29", + "focus-trap": "~7.5.3", + "luxon": "~3.4.3", "sortablejs": "~1.15.0" } }, @@ -2603,33 +2604,45 @@ "integrity": "sha512-wHQYWFtDa6c328EraXEVZvgOiaQyYr0yuaaZ0G3cB4C3lSkWefW34L/e5TLAhtuG3zJ/wR6pl5X1YYNfBc0/4Q==" }, "node_modules/@esri/calcite-components": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@esri/calcite-components/-/calcite-components-1.0.8.tgz", - "integrity": "sha512-aeBSZQdtv7CRGbcGZh49RT4Z481muFspnY+98X6RRnf8QHdjpwTk+JbyS1pLAvenyLqTV+Gwa/Q5BPhbsfLiRw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@esri/calcite-components/-/calcite-components-1.10.0.tgz", + "integrity": "sha512-nKTwU7hseSAXTqEQelVfo+qDa5jCGvabky4atRsJxROkHbSQkhdFFfwOrrffHOr9CtZBxtvZhfh/Tb6Nky15hw==", "dependencies": { - "@floating-ui/dom": "1.2.1", - "@stencil/core": "2.20.0", - "@types/color": "3.0.3", + "@floating-ui/dom": "1.5.3", + "@stencil/core": "2.22.3", + "@types/color": "3.0.4", "color": "4.2.3", - "focus-trap": "7.2.0", + "composed-offset-position": "0.0.4", + "dayjs": "1.11.10", + "focus-trap": "7.5.4", "form-request-submit-polyfill": "2.0.0", "lodash-es": "4.17.21", - "sortablejs": "1.15.0" + "sortablejs": "1.15.0", + "timezone-groups": "0.8.0" } }, "node_modules/@floating-ui/core": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.6.tgz", - "integrity": "sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", + "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", + "dependencies": { + "@floating-ui/utils": "^0.1.3" + } }, "node_modules/@floating-ui/dom": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.1.tgz", - "integrity": "sha512-Rt45SmRiV8eU+xXSB9t0uMYiQ/ZWGE/jumse2o3i5RGlyvcbqOF4q+1qBnzLE2kZ5JGhq0iMkcGXUKbFe7MpTA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", "dependencies": { - "@floating-ui/core": "^1.2.1" + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" } }, + "node_modules/@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -3307,9 +3320,9 @@ "dev": true }, "node_modules/@stencil/core": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.20.0.tgz", - "integrity": "sha512-ka+eOW+dNteXIfLCRipNbbAlBEQjqJ2fkx3fxzlKgnNHEQMdZiuIjlWt63KzvOJStNeuADdQXo89BB1dC2VRUw==", + "version": "2.22.3", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.22.3.tgz", + "integrity": "sha512-kmVA0M/HojwsfkeHsifvHVIYe4l5tin7J5+DLgtl8h6WWfiMClND5K3ifCXXI2ETDNKiEk21p6jql3Fx9o2rng==", "bin": { "stencil": "bin/stencil" }, @@ -3394,25 +3407,25 @@ } }, "node_modules/@types/color": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz", - "integrity": "sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.4.tgz", + "integrity": "sha512-OpisS4bqJJwbkkQRrMvURf3DOxBoAg9mysHYI7WgrWpSYHqHGKYBULHdz4ih77SILcLDo/zyHGFyfIl9yb8NZQ==", "dependencies": { "@types/color-convert": "*" } }, "node_modules/@types/color-convert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz", - "integrity": "sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.3.tgz", + "integrity": "sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==", "dependencies": { "@types/color-name": "*" } }, "node_modules/@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-87W6MJCKZYDhLAx/J1ikW8niMvmGRyY+rpUxWpL1cO7F8Uu5CHuQoFv+R0/L5pgNdW4jTyda42kv60uwVIPjLw==" }, "node_modules/@types/connect": { "version": "3.4.35", @@ -4532,6 +4545,16 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/@zip.js/zip.js": { + "version": "2.7.30", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.30.tgz", + "integrity": "sha512-nhMvQCj+TF1ATBqYzFds7v+yxPBhdDYHh8J341KtC1D2UrVBUIYcYK4Jy1/GiTsxOXEiKOXSUxvPG/XR+7jMqw==", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=16.5.0" + } + }, "node_modules/abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -6559,6 +6582,11 @@ "node": "*" } }, + "node_modules/composed-offset-position": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/composed-offset-position/-/composed-offset-position-0.0.4.tgz", + "integrity": "sha512-vMlvu1RuNegVE0YsCDSV/X4X10j56mq7PCIyOKK74FxkXzGLwhOUmdkJLSdOBOMwWycobGUMgft2lp+YgTe8hw==" + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -7826,6 +7854,11 @@ "node": ">= 12" } }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -9298,11 +9331,11 @@ "dev": true }, "node_modules/focus-trap": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.2.0.tgz", - "integrity": "sha512-v4wY6HDDYvzkBy4735kW5BUEuw6Yz9ABqMYLuTNbzAFPcBOGiGHwwcNVMvUz4G0kgSYh13wa/7TG3XwTeT4O/A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", "dependencies": { - "tabbable": "^6.0.1" + "tabbable": "^6.2.0" } }, "node_modules/follow-redirects": { @@ -12336,9 +12369,9 @@ } }, "node_modules/luxon": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", - "integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", + "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", "engines": { "node": ">=12" } @@ -16849,9 +16882,9 @@ } }, "node_modules/tabbable": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.1.2.tgz", - "integrity": "sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ==" + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, "node_modules/tailwindcss": { "version": "3.3.2", @@ -17090,6 +17123,14 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/timezone-groups": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/timezone-groups/-/timezone-groups-0.8.0.tgz", + "integrity": "sha512-t7E/9sPfCU0m0ZbS7Cqw52D6CB/UyeaiIBmyJCokI1SyOyOgA/ESiQ/fbreeFaUG9QSenGlZSSk/7rEbkipbOA==", + "bin": { + "timezone-groups": "dist/cli.cjs" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -18186,16 +18227,17 @@ "dev": true }, "@arcgis/core": { - "version": "4.26.5", - "resolved": "https://registry.npmjs.org/@arcgis/core/-/core-4.26.5.tgz", - "integrity": "sha512-Z8KoJrTD1ZQwVVZAIpkdjAQVT/MjAaWAr5u0krifKd6Y1m0TrEb8/iK86KvyWX8yNNWEroEP9SiE19vH/JKquQ==", + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/@arcgis/core/-/core-4.28.9.tgz", + "integrity": "sha512-UpWcWsBg+/yhfjzPx45+iiCJXhPWx5F+xB/sdMai1mfMvwlAxbsL9YzbLM/S4CawVpEPwC3i5ZGEgp17gxu7Kg==", "requires": { "@esri/arcgis-html-sanitizer": "~3.0.1", "@esri/calcite-colors": "~6.1.0", - "@esri/calcite-components": "~1.0.7", - "@popperjs/core": "~2.11.6", - "focus-trap": "~7.2.0", - "luxon": "~3.2.1", + "@esri/calcite-components": "^1.9.2", + "@popperjs/core": "~2.11.8", + "@zip.js/zip.js": "~2.7.29", + "focus-trap": "~7.5.3", + "luxon": "~3.4.3", "sortablejs": "~1.15.0" } }, @@ -19950,33 +19992,45 @@ "integrity": "sha512-wHQYWFtDa6c328EraXEVZvgOiaQyYr0yuaaZ0G3cB4C3lSkWefW34L/e5TLAhtuG3zJ/wR6pl5X1YYNfBc0/4Q==" }, "@esri/calcite-components": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@esri/calcite-components/-/calcite-components-1.0.8.tgz", - "integrity": "sha512-aeBSZQdtv7CRGbcGZh49RT4Z481muFspnY+98X6RRnf8QHdjpwTk+JbyS1pLAvenyLqTV+Gwa/Q5BPhbsfLiRw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@esri/calcite-components/-/calcite-components-1.10.0.tgz", + "integrity": "sha512-nKTwU7hseSAXTqEQelVfo+qDa5jCGvabky4atRsJxROkHbSQkhdFFfwOrrffHOr9CtZBxtvZhfh/Tb6Nky15hw==", "requires": { - "@floating-ui/dom": "1.2.1", - "@stencil/core": "2.20.0", - "@types/color": "3.0.3", + "@floating-ui/dom": "1.5.3", + "@stencil/core": "2.22.3", + "@types/color": "3.0.4", "color": "4.2.3", - "focus-trap": "7.2.0", + "composed-offset-position": "0.0.4", + "dayjs": "1.11.10", + "focus-trap": "7.5.4", "form-request-submit-polyfill": "2.0.0", "lodash-es": "4.17.21", - "sortablejs": "1.15.0" + "sortablejs": "1.15.0", + "timezone-groups": "0.8.0" } }, "@floating-ui/core": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.6.tgz", - "integrity": "sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", + "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", + "requires": { + "@floating-ui/utils": "^0.1.3" + } }, "@floating-ui/dom": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.1.tgz", - "integrity": "sha512-Rt45SmRiV8eU+xXSB9t0uMYiQ/ZWGE/jumse2o3i5RGlyvcbqOF4q+1qBnzLE2kZ5JGhq0iMkcGXUKbFe7MpTA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", "requires": { - "@floating-ui/core": "^1.2.1" + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" } }, + "@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + }, "@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -20516,9 +20570,9 @@ "dev": true }, "@stencil/core": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.20.0.tgz", - "integrity": "sha512-ka+eOW+dNteXIfLCRipNbbAlBEQjqJ2fkx3fxzlKgnNHEQMdZiuIjlWt63KzvOJStNeuADdQXo89BB1dC2VRUw==" + "version": "2.22.3", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.22.3.tgz", + "integrity": "sha512-kmVA0M/HojwsfkeHsifvHVIYe4l5tin7J5+DLgtl8h6WWfiMClND5K3ifCXXI2ETDNKiEk21p6jql3Fx9o2rng==" }, "@trysound/sax": { "version": "0.2.0", @@ -20599,25 +20653,25 @@ } }, "@types/color": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz", - "integrity": "sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.4.tgz", + "integrity": "sha512-OpisS4bqJJwbkkQRrMvURf3DOxBoAg9mysHYI7WgrWpSYHqHGKYBULHdz4ih77SILcLDo/zyHGFyfIl9yb8NZQ==", "requires": { "@types/color-convert": "*" } }, "@types/color-convert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz", - "integrity": "sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.3.tgz", + "integrity": "sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==", "requires": { "@types/color-name": "*" } }, "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-87W6MJCKZYDhLAx/J1ikW8niMvmGRyY+rpUxWpL1cO7F8Uu5CHuQoFv+R0/L5pgNdW4jTyda42kv60uwVIPjLw==" }, "@types/connect": { "version": "3.4.35", @@ -21610,6 +21664,11 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "@zip.js/zip.js": { + "version": "2.7.30", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.30.tgz", + "integrity": "sha512-nhMvQCj+TF1ATBqYzFds7v+yxPBhdDYHh8J341KtC1D2UrVBUIYcYK4Jy1/GiTsxOXEiKOXSUxvPG/XR+7jMqw==" + }, "abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -23283,6 +23342,11 @@ } } }, + "composed-offset-position": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/composed-offset-position/-/composed-offset-position-0.0.4.tgz", + "integrity": "sha512-vMlvu1RuNegVE0YsCDSV/X4X10j56mq7PCIyOKK74FxkXzGLwhOUmdkJLSdOBOMwWycobGUMgft2lp+YgTe8hw==" + }, "compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -24113,6 +24177,11 @@ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" }, + "dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -25213,11 +25282,11 @@ "dev": true }, "focus-trap": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.2.0.tgz", - "integrity": "sha512-v4wY6HDDYvzkBy4735kW5BUEuw6Yz9ABqMYLuTNbzAFPcBOGiGHwwcNVMvUz4G0kgSYh13wa/7TG3XwTeT4O/A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", "requires": { - "tabbable": "^6.0.1" + "tabbable": "^6.2.0" } }, "follow-redirects": { @@ -27439,9 +27508,9 @@ } }, "luxon": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", - "integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==" + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", + "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==" }, "make-dir": { "version": "3.1.0", @@ -30411,9 +30480,9 @@ } }, "tabbable": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.1.2.tgz", - "integrity": "sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ==" + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, "tailwindcss": { "version": "3.3.2", @@ -30593,6 +30662,11 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "timezone-groups": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/timezone-groups/-/timezone-groups-0.8.0.tgz", + "integrity": "sha512-t7E/9sPfCU0m0ZbS7Cqw52D6CB/UyeaiIBmyJCokI1SyOyOgA/ESiQ/fbreeFaUG9QSenGlZSSk/7rEbkipbOA==" + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index 902fdde..d878571 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "webpack-dev-server": "4.9" }, "dependencies": { - "@arcgis/core": "^4.26.5", + "@arcgis/core": "4.28", "@entryline/gifstream": "^1.2.1", "@esri/arcgis-rest-feature-service": "^4.0.4", "@esri/arcgis-rest-request": "^4.2.0", diff --git a/src/app-config.ts b/src/app-config.ts index 034bf3c..336f3d5 100644 --- a/src/app-config.ts +++ b/src/app-config.ts @@ -21,7 +21,8 @@ const config: IAppConfig = { // this world imagery basemap will be used when user saves selected Wayback items into a new webmap 'world-imagery-basemap': 'https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/', - 'wayback-export-base': '', + 'wayback-export-base': + 'https://wayport.maptiles.arcgis.com/arcgis/rest/services/Wayport/GPServer/Wayport', }, }, // The dev enivornment is optional, please comment out the dev section below if don't need the dev enivornment @@ -39,7 +40,7 @@ const config: IAppConfig = { 'world-imagery-basemap': 'https://servicesdev.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/', 'wayback-export-base': - 'https://34.220.147.218:6443/arcgis/rest/services/Wayport/GPServer/Wayport', + 'https://wayportdev.maptiles.arcgis.com/arcgis/rest/services/Wayport/GPServer/Wayport', }, }, defaultMapExtent: { diff --git a/src/components/AnimationPanel/AnimationPanel.tsx b/src/components/AnimationPanel/AnimationPanel.tsx index f0c6747..3977f74 100644 --- a/src/components/AnimationPanel/AnimationPanel.tsx +++ b/src/components/AnimationPanel/AnimationPanel.tsx @@ -10,7 +10,7 @@ import LoadingIndicator from './LoadingIndicator'; import DownloadGIFDialog from './DownloadGIFDialog'; import CloseBtn from './CloseBtn'; -import { whenFalse } from '@arcgis/core/core/watchUtils'; +// import { whenFalse } from '@arcgis/core/core/watchUtils'; import { IWaybackItem } from '@typings/index'; import { useDispatch, useSelector } from 'react-redux'; @@ -24,6 +24,7 @@ import { toggleIsLoadingFrameData, } from '@store/AnimationMode/reducer'; import Background from './Background'; +import { watch } from '@arcgis/core/core/reactiveUtils'; type Props = { waybackItems4Animation: IWaybackItem[]; @@ -186,15 +187,25 @@ const AnimationPanel: React.FC = ({ }, [waybackItems4Animation]); useEffect(() => { - const onUpdating = whenFalse(mapView, 'stationary', () => { - loadingWaybackItems4AnimationRef.current = true; - setFrameData(null); - }); - - return () => { - // onStationary.remove(); - onUpdating.remove(); - }; + // const onUpdating = whenFalse(mapView, 'stationary', () => { + // loadingWaybackItems4AnimationRef.current = true; + // setFrameData(null); + // }); + + watch( + () => mapView.stationary, + () => { + if (!mapView.stationary) { + loadingWaybackItems4AnimationRef.current = true; + setFrameData(null); + } + } + ); + + // return () => { + // // onStationary.remove(); + // onUpdating.remove(); + // }; }, []); useEffect(() => { diff --git a/src/components/AppLayout/AppLayout.tsx b/src/components/AppLayout/AppLayout.tsx index e096b57..451eac7 100644 --- a/src/components/AppLayout/AppLayout.tsx +++ b/src/components/AppLayout/AppLayout.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { AboutThisApp, @@ -16,7 +16,7 @@ import { ReferenceLayerToggle, Sidebar, SearchWidget, - ShareDialog, + // ShareDialog, SwipeWidget, SaveAsWebMapDialog, SwipeWidgetToggleBtn, @@ -34,11 +34,26 @@ import { OpenDownloadPanelBtn, DownloadDialog, } from '..'; -import { AppContext } from '@contexts/AppContextProvider'; +// import { AppContext } from '@contexts/AppContextProvider'; import { getServiceUrl } from '@utils/Tier'; +import useCurrenPageBecomesVisible from '@hooks/useCurrenPageBecomesVisible'; +import { revalidateToken } from '@utils/Esri-OAuth'; const AppLayout: React.FC = () => { - const { onPremises } = React.useContext(AppContext); + // const { onPremises } = React.useContext(AppContext); + + const currentPageIsVisibleAgain = useCurrenPageBecomesVisible(); + + useEffect(() => { + if (!currentPageIsVisibleAgain) { + return; + } + + // should re-validate when current tab becomes visible again, + // so that we can sign out the current user if the token is no longer valid, + // this can heppen when user signs out it's ArcGIS Online account from another tab + revalidateToken(); + }, [currentPageIsVisibleAgain]); return ( <> @@ -49,7 +64,7 @@ const AppLayout: React.FC = () => { - {/* */} + @@ -91,9 +106,9 @@ const AppLayout: React.FC = () => { - {!onPremises && } + {/* {!onPremises && } */} - {/* */} + diff --git a/src/components/DownloadDialog/DownloadDialog.tsx b/src/components/DownloadDialog/DownloadDialog.tsx index d7f0f63..dbebdf0 100644 --- a/src/components/DownloadDialog/DownloadDialog.tsx +++ b/src/components/DownloadDialog/DownloadDialog.tsx @@ -1,12 +1,18 @@ import { DownloadJob } from '@store/DownloadMode/reducer'; import React, { FC } from 'react'; import { DownloadJobCard } from './DownloadJobCard'; +import { DownloadJobPlaceholder } from './DownloadJobPlaceholder'; type Props = { /** * list of donwload jobs */ jobs: DownloadJob[]; + /** + * if true, the system is in process of adding a new download job and + * a placeholder card should be displayed + */ + isAddingNewDownloadJob: boolean; /** * fires when user clicks on the create tile package button to start the download job * @param id job id @@ -40,6 +46,7 @@ type Props = { export const DownloadDialog: FC = ({ jobs, + isAddingNewDownloadJob, createTilePackageButtonOnClick, downloadTilePackageButtonOnClick, closeButtonOnClick, @@ -47,7 +54,7 @@ export const DownloadDialog: FC = ({ levelsOnChange, }: Props) => { const getJobsList = () => { - if (!jobs?.length) { + if (!jobs?.length && !isAddingNewDownloadJob) { return
No download jobs.
; } @@ -72,8 +79,13 @@ export const DownloadDialog: FC = ({ }; return ( -
-
+
+
= ({ />
-
-

Download Local Tile Cache

+
+

+ Wayback Export ( + + beta + + ) +

- Based on your current map extent, choose a scale range - for your download. Downloads are limited to 150,000 - tiles. + Exported basemap tiles are intended for offline use in + ArcGIS applications and{' '} + + offline applications + {' '} + built with an ArcGIS Runtime SDK in accordance with + Esri’s terms of use:{' '} + + View Summary + {' '} + and{' '} + + View Terms of Use + + . {/* You can choose this window while your tiles are prepared. */}

+
    +
  • + Exports are based on map extent, with a minimum zoom + level of 12. +
  • +
  • + Each export request is limited to a maximum of + 150,000 tiles. +
  • +
  • + No more than five exports may be requested + concurrently. +
  • +
  • + This dialog can safely be closed while tile packages + are being created. +
  • +
+ +
+ + {isAddingNewDownloadJob && } +
{getJobsList()}
diff --git a/src/components/DownloadDialog/DownloadDialogContainer.tsx b/src/components/DownloadDialog/DownloadDialogContainer.tsx index cd842e5..41564df 100644 --- a/src/components/DownloadDialog/DownloadDialogContainer.tsx +++ b/src/components/DownloadDialog/DownloadDialogContainer.tsx @@ -7,6 +7,7 @@ import { import { selectDownloadJobs, + selectIsAddingNewDownloadJob, selectIsDownloadDialogOpen, selectNumOfPendingDownloadJobs, } from '@store/DownloadMode/selectors'; @@ -33,17 +34,19 @@ export const DownloadDialogContainer = () => { const numPendingJobs = useSelector(selectNumOfPendingDownloadJobs); + const isAddingNewDownloadJob = useSelector(selectIsAddingNewDownloadJob); + useEffect(() => { // save jobs to localhost so they can be restored saveDownloadJobs2LocalStorage(jobs); + + if (jobs?.length && isAnonymouns()) { + signIn(); + } }, [jobs]); useEffect(() => { updateHashParams('downloadMode', isOpen ? 'true' : null); - - if (isOpen && isAnonymouns()) { - signIn(); - } }, [isOpen]); useEffect(() => { @@ -65,6 +68,7 @@ export const DownloadDialogContainer = () => { return ( { dispatch(isDownloadDialogOpenToggled()); }} diff --git a/src/components/DownloadDialog/DownloadJobCard.tsx b/src/components/DownloadDialog/DownloadJobCard.tsx index f37e15b..cdf3a43 100644 --- a/src/components/DownloadDialog/DownloadJobCard.tsx +++ b/src/components/DownloadDialog/DownloadJobCard.tsx @@ -2,6 +2,7 @@ import React, { FC, useEffect, useMemo } from 'react'; import classnames from 'classnames'; import { DownloadJob, DownloadJobStatus } from '@store/DownloadMode/reducer'; import { numberFns } from 'helper-toolkit-ts'; +import { MAX_NUMBER_TO_TILES_PER_WAYPORT_EXPORT } from '@services/export-wayback-bundle/getTileEstimationsInOutputBundle'; type Props = { data: DownloadJob; @@ -33,10 +34,10 @@ type Props = { const ButtonLableByStatus: Record = { 'not started': 'create tile package', - pending: 'in progress...', + pending: 'in progress', finished: 'donwload', failed: 'failed', - downloaded: 'downloaded', + downloaded: 'CHECK BROWSER FOR DOWNLOAD PROGRESS', }; export const DownloadJobCard: FC = ({ @@ -90,13 +91,13 @@ export const DownloadJobCard: FC = ({ }, [tileEstimations, levels]); const getStatusIcon = () => { - if (status === 'pending') { - return ; - } + // if (status === 'pending') { + // return ; + // } - if (status === 'finished') { - return ; - } + // if (status === 'finished') { + // return ; + // } return ( = ({ style={{ cursor: 'pointer', }} + title="Cancel" onClick={removeButtonOnClick.bind(null, id)} /> ); @@ -121,12 +123,30 @@ export const DownloadJobCard: FC = ({ const getButtonLable = () => { if (status === 'finished' && outputTilePackageInfo !== undefined) { const sizeInMB = (outputTilePackageInfo.size / 1000000).toFixed(1); - return `Download - ${sizeInMB}MB`; + return `Tiles Ready to Download - ${sizeInMB}MB`; } return ButtonLableByStatus[status] || status; }; + /** + * get formatted total number of title. Use comma separated if total is less than 1 million, + * otherwise, use abbreviation instead + * @param total + * @returns + */ + const formatTotalNumOfTiles = (total: number) => { + if (!total) { + return 0; + } + + if (total < 1e6) { + return numberFns.numberWithCommas(total); + } + + return numberFns.abbreviateNumber(total); + }; + const shouldDisableActionButton = () => { if ( status === 'pending' || @@ -140,6 +160,13 @@ export const DownloadJobCard: FC = ({ return true; } + if ( + status === 'not started' && + totalTiles > MAX_NUMBER_TO_TILES_PER_WAYPORT_EXPORT + ) { + return true; + } + return false; }; @@ -147,9 +174,14 @@ export const DownloadJobCard: FC = ({ sliderRef.current.addEventListener( 'calciteSliderChange', (evt: any) => { - const userSelectedMaxZoomLevel = +evt.target.value; - - levelsOnChange(id, [levels[0], userSelectedMaxZoomLevel]); + const userSelectedMinZoomLevel = +evt.target.minValue; + const userSelectedMaxZoomLevel = +evt.target.maxValue; + // console.log(evt.target.minValue,evt.target.maxValue) + + levelsOnChange(id, [ + userSelectedMinZoomLevel, + userSelectedMaxZoomLevel, + ]); } ); }, []); @@ -160,7 +192,7 @@ export const DownloadJobCard: FC = ({ return (
-
+
{getStatusIcon()}
@@ -182,14 +214,16 @@ export const DownloadJobCard: FC = ({ : maxZoomLevel } min={minZoomLevel} - value={levels[1]} + // value={levels[1]} + min-value={levels[0]} + max-value={levels[1]} step="1" ticks="1" {...sliderProp} >
-
+
Level {levels[0]} - {levels[1]} @@ -197,22 +231,23 @@ export const DownloadJobCard: FC = ({
- - ~{numberFns.numberWithCommas(totalTiles)} tiles - + ~{formatTotalNumOfTiles(totalTiles)} tiles
+ {status === 'pending' && ( + + )} {getButtonLable()}
diff --git a/src/components/DownloadDialog/DownloadJobPlaceholder.tsx b/src/components/DownloadDialog/DownloadJobPlaceholder.tsx new file mode 100644 index 0000000..95a5e40 --- /dev/null +++ b/src/components/DownloadDialog/DownloadJobPlaceholder.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export const DownloadJobPlaceholder = () => { + return ( +
+
+ +
+
+ ); +}; diff --git a/src/components/Gutter/GutterContainer.tsx b/src/components/Gutter/GutterContainer.tsx index b298909..3314456 100644 --- a/src/components/Gutter/GutterContainer.tsx +++ b/src/components/Gutter/GutterContainer.tsx @@ -1,6 +1,6 @@ import React, { useContext, useMemo } from 'react'; -import Gutter from './index'; +import { Gutter } from './index'; import { useSelector, useDispatch } from 'react-redux'; @@ -14,6 +14,7 @@ import { } from '@store/UI/reducer'; import { AppContext } from '@contexts/AppContextProvider'; import { isAnimationModeOnSelector } from '@store/AnimationMode/reducer'; +import { copy2clipboard } from '@utils/snippets/copy2clipborad'; type Props = { children: React.ReactNode; @@ -31,27 +32,27 @@ const GutterContainer: React.FC = ({ children }) => { const isHide = useSelector(isGutterHideSelector); - const { isMobile, onPremises } = useContext(AppContext); + const { isMobile } = useContext(AppContext); const aboutButtonOnClick = () => { dispatch(isAboutThisAppModalOpenToggled()); }; - const shareButtonOnClick = () => { - dispatch(isShareModalOpenToggled()); - }; - const settingButtonOnClick = () => { dispatch(isSettingModalOpenToggled()); }; + const copyButtonOnClick = () => { + copy2clipboard(window.location.href); + }; + return !isHide ? ( {children} diff --git a/src/components/Gutter/index.tsx b/src/components/Gutter/index.tsx index 648054c..67049d4 100644 --- a/src/components/Gutter/index.tsx +++ b/src/components/Gutter/index.tsx @@ -1,94 +1,108 @@ import './style.css'; -import React from 'react'; +import React, { FC, useState } from 'react'; import { MOBILE_HEADER_HEIGHT } from '@constants/UI'; interface IProps { isMobile: boolean; settingsBtnDisabled: boolean; - shareBtnDisabled: boolean; + // shareBtnDisabled: boolean; // children: JSX.Element[] | JSX.Element; aboutButtonOnClick: () => void; - shareButtonOnClick: () => void; + copyButtonOnClick: () => void; settingButtonOnClick: () => void; children?: React.ReactNode; } -class Gutter extends React.PureComponent { - constructor(props: IProps) { - super(props); - } +export const Gutter: FC = ({ + isMobile, + // shareBtnDisabled, + settingsBtnDisabled, + copyButtonOnClick, + aboutButtonOnClick, + settingButtonOnClick, + children, +}) => { + const [hasCopied2Clipboard, setHasCopied2Clipboard] = useState(false); - render(): JSX.Element { - const { - isMobile, - shareBtnDisabled, - shareButtonOnClick, - aboutButtonOnClick, - settingButtonOnClick, - } = this.props; - - return ( + return ( +
+ {/* gradient effect on right side of gutter */}
- {/* gradient effect on right side of gutter */} + >
+ +
+ className="gutter-nav-btn mb-2" + // data-modal={AboutThisAppModalConfig['modal-id']} + title="About this app" + onClick={aboutButtonOnClick} + > + +
-
+ {/* {!shareBtnDisabled && (
- +
+ )} */} - {!shareBtnDisabled && ( -
- -
- )} +
{ + copyButtonOnClick(); -
- -
-
+ setHasCopied2Clipboard(true); - {/* divider with shadow effect */} -
{ + setHasCopied2Clipboard(false); + }, 3000); }} - >
+ > + +
- {this.props.children} +
+ +
- ); - } -} -export default Gutter; + {/* divider with shadow effect */} +
+ + {children} +
+ ); +}; diff --git a/src/components/ListView/Card.tsx b/src/components/ListView/Card.tsx index 89c340c..e4903fa 100644 --- a/src/components/ListView/Card.tsx +++ b/src/components/ListView/Card.tsx @@ -13,7 +13,10 @@ interface IProps { * if true, download button should be disabled */ shouldDownloadButtonBeDisabled?: boolean; - + /** + * tooltip text for download button + */ + downloadButtonTooltipText: string; toggleSelect?: (releaseNum: number) => void; onClick?: (releaseNum: number) => void; downloadButtonOnClick: (releaseNum: number) => void; @@ -73,6 +76,7 @@ class ListViewCard extends React.PureComponent { isSelected, isHighlighted, shouldDownloadButtonBeDisabled, + downloadButtonTooltipText, onClick, onMouseEnter, onMouseOut, @@ -122,7 +126,7 @@ class ListViewCard extends React.PureComponent {
- {/*
{ downloadButtonOnClick(data.releaseNum); }} - title={ - shouldDownloadButtonBeDisabled - ? 'Reached the maximum limit for download jobs' - : 'Download a local copy of imagery tiles' - } + title={downloadButtonTooltipText} > -
*/} +
{ const mapExtent = useSelector(mapExtentSelector); + const downloadButtonTooltipText = useMemo(() => { + const text = + 'Export an imagery tile package for the current map extent'; + + if (zoom < 12) { + return text + ` (zoom in to enable)`; + } + + if (hasReachedLimitOfConcurrentDownloadJobs) { + return 'Reached the maximum limit of 5 concurrent export jobs'; + } + + return text; + }, [zoom, hasReachedLimitOfConcurrentDownloadJobs]); + return ( ; /** * if ture, the The donwload button will be disabled. - * the user can only have limited of number of download jobs in the list. + * The download button should only be enabled if + * - number of download jobs has not reached to the limit + * - map zoom level is 12+ */ - hasReachedLimitOfConcurrentDownloadJobs: boolean; - + shouldDownloadButtonBeDisabled: boolean; + /** + * tooltip text for download button + */ + downloadButtonTooltipText: string; toggleSelect?: (releaseNum: number) => void; onClick?: (releaseNum: number) => void; downloadButtonOnClick: (releaseNum: number) => void; @@ -66,7 +71,8 @@ class ListView extends React.PureComponent { rNum4SelectedWaybackItems, rNum4WaybackItemsWithLocalChanges, shouldOnlyShowItemsWithLocalChange, - hasReachedLimitOfConcurrentDownloadJobs, + shouldDownloadButtonBeDisabled, + downloadButtonTooltipText, toggleSelect, onClick, onMouseEnter, @@ -104,8 +110,9 @@ class ListView extends React.PureComponent { isHighlighted={isHighlighted} toggleSelect={toggleSelect} shouldDownloadButtonBeDisabled={ - hasReachedLimitOfConcurrentDownloadJobs + shouldDownloadButtonBeDisabled } + downloadButtonTooltipText={downloadButtonTooltipText} onClick={onClick} onMouseEnter={onMouseEnter} onMouseOut={onMouseOut} diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index 0c4626b..94fee98 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -1,4 +1,3 @@ -import '@arcgis/core/assets/esri/themes/dark/main.css'; import React, { useEffect, useRef } from 'react'; import MapView from '@arcgis/core/views/MapView'; @@ -79,11 +78,11 @@ const MapViewComponent: React.FC = ({ setMapView(view); view.when(() => { - initWatchUtils(view); + initEventHandlers(view); }); }; - const initWatchUtils = async (view: MapView) => { + const initEventHandlers = async (view: MapView) => { // whenTrue(mapView, 'stationary', mapViewUpdateEndHandler); when( () => view.stationary === true, @@ -140,12 +139,6 @@ const MapViewComponent: React.FC = ({ initMapView(); }, []); - // useEffect(() => { - // if (mapView) { - // initWatchUtils(); - // } - // }, [mapView]); - return ( <>
= ({ queryMetadata(evt.mapPoint); }); - watch(mapView, 'zoom', () => { - // console.log('view zoom is on updating, should hide the popup', zoom); - metadataOnChange(null); - }); - - watch(mapView, 'center', () => { - // // console.log('view center is on updating, should update the popup position'); - // // need to update the screen point for popup anchor since the map center has changed - // updateScreenPoint4PopupAnchor(); - metadataOnChange(null); - }); - - // try { - // type Modules = [typeof IWatchUtils]; - - // const [watchUtils] = await (loadModules([ - // 'esri/core/watchUtils', - // ]) as Promise); - - // mapView.on('click', (evt) => { - // console.log('view on click, should show popup', evt.mapPoint); - // queryMetadata(evt.mapPoint); - // }); - - // watch(mapView, 'zoom', () => { - // // console.log('view zoom is on updating, should hide the popup', zoom); - // metadataOnChange(null); - // }); - - // watch(mapView, 'center', () => { - // // // console.log('view center is on updating, should update the popup position'); - // // // need to update the screen point for popup anchor since the map center has changed - // // updateScreenPoint4PopupAnchor(); - // metadataOnChange(null); - // }); - // } catch (err) { - // console.error(err); - // } + // watch(mapView, 'zoom', () => { + // // console.log('view zoom is on updating, should hide the popup', zoom); + // metadataOnChange(null); + // }); + + watch( + () => mapView.zoom, + () => { + metadataOnChange(null); + } + ); + + // watch(mapView, 'center', () => { + // // // console.log('view center is on updating, should update the popup position'); + // // // need to update the screen point for popup anchor since the map center has changed + // // updateScreenPoint4PopupAnchor(); + // metadataOnChange(null); + // }); + + watch( + () => mapView.center, + () => { + metadataOnChange(null); + } + ); }; React.useEffect(() => { diff --git a/src/components/OpenDownloadPanelBtn/OpenDownloadPanelBtn.tsx b/src/components/OpenDownloadPanelBtn/OpenDownloadPanelBtn.tsx index a60c836..1acf223 100644 --- a/src/components/OpenDownloadPanelBtn/OpenDownloadPanelBtn.tsx +++ b/src/components/OpenDownloadPanelBtn/OpenDownloadPanelBtn.tsx @@ -55,9 +55,7 @@ export const OpenDownloadPanelBtn = () => { disabled: numOfJobs === 0, } )} - title={ - 'download local copies of imagery tiles via the release row item' - } + title={'Choose a version from the list to export a tile package'} onClick={() => { dispatch(isDownloadDialogOpenToggled()); }} diff --git a/src/components/SaveAsWebmapBtn/SaveAsWebmapBtnContainer.tsx b/src/components/SaveAsWebmapBtn/SaveAsWebmapBtnContainer.tsx index b20b2a9..3b3e4d4 100644 --- a/src/components/SaveAsWebmapBtn/SaveAsWebmapBtnContainer.tsx +++ b/src/components/SaveAsWebmapBtn/SaveAsWebmapBtnContainer.tsx @@ -35,8 +35,12 @@ const SaveAsWebmapBtnContainer = () => { const isAnimationModeOn: boolean = useSelector(isAnimationModeOnSelector); const isDisabled = useMemo(() => { - return isSwipeWidgetOpen || isAnimationModeOn; - }, [isSwipeWidgetOpen, isAnimationModeOn]); + return ( + isSwipeWidgetOpen || + isAnimationModeOn || + rNum4SelectedWaybackItems?.length === 0 + ); + }, [isSwipeWidgetOpen, isAnimationModeOn, rNum4SelectedWaybackItems]); const clearAllBtnOnClick = () => { dispatch(releaseNum4SelectedItemsCleaned()); diff --git a/src/components/SwipeWidget/SwipeWidget.tsx b/src/components/SwipeWidget/SwipeWidget.tsx index 25c7ee7..ac449ef 100644 --- a/src/components/SwipeWidget/SwipeWidget.tsx +++ b/src/components/SwipeWidget/SwipeWidget.tsx @@ -1,16 +1,11 @@ import React, { useRef, useEffect } from 'react'; -// import { loadModules } from 'esri-loader'; -// import IMapView from 'esri/views/MapView'; -// import ISwipe from 'esri/widgets/Swipe'; -// import IWebTileLayer from 'esri/layers/WebTileLayer'; -// import IWatchUtils from 'esri/core/watchUtils'; import { IWaybackItem } from '@typings/index'; import MapView from '@arcgis/core/views/MapView'; import Swipe from '@arcgis/core/widgets/Swipe'; import WebTileLayer from '@arcgis/core/layers/WebTileLayer'; -import { watch } from '@arcgis/core/core/watchUtils'; +import { watch } from '@arcgis/core/core/reactiveUtils'; import { getWaybackLayer } from '../WaybackLayer/getWaybackLayer'; @@ -64,71 +59,15 @@ const SwipeWidget: React.FC = ({ // onLoaded(); } - - // type Modules = [ - // typeof ISwipe, - // ]; - - // try { - // const [ Swipe ] = await (loadModules([ - // 'esri/widgets/Swipe', - // ]) as Promise); - - // if(swipeWidgetRef.current){ - // show(); - // } else { - - // const leadingLayer = await getWaybackLayer(waybackItem4LeadingLayer); - // const trailingLayer = await getWaybackLayer(waybackItem4TrailingLayer); - - // layersRef.current = [leadingLayer, trailingLayer]; - - // mapView.map.addMany(layersRef.current, 1); - - // const swipe = new Swipe({ - // view: mapView, - // leadingLayers: [leadingLayer], - // trailingLayers: [trailingLayer], - // direction: "horizontal", - // position: 50 // position set to middle of the view (50%) - // }); - - // swipeWidgetRef.current = swipe; - - // mapView.ui.add(swipe); - - // addEventHandlers(swipe); - - // // onLoaded(); - // } - - // } catch(err){ - // console.error(err); - // init(); - // } }; const addEventHandlers = (swipeWidget: Swipe) => { - // try { - // type Modules = [typeof IWatchUtils]; - - // const [watchUtils] = await (loadModules([ - // 'esri/core/watchUtils', - // ]) as Promise); - - // watch(swipeWidget, 'position', (position:number) => { - // // console.log('position changes for swipe widget', position); - // positionOnChange(position); - // }); - - // } catch (err) { - // console.error(err); - // } - - watch(swipeWidget, 'position', (position: number) => { - // console.log('position changes for swipe widget', position); - positionOnChange(position); - }); + watch( + () => swipeWidget.position, + (position: number) => { + positionOnChange(position); + } + ); }; const show = () => { diff --git a/src/hooks/useCurrenPageBecomesVisible.tsx b/src/hooks/useCurrenPageBecomesVisible.tsx new file mode 100644 index 0000000..9039ad1 --- /dev/null +++ b/src/hooks/useCurrenPageBecomesVisible.tsx @@ -0,0 +1,26 @@ +import React, { useMemo } from 'react'; +import { usePrevious } from './usePrevious'; +import useVisibilityState from './useVisibilityState'; + +/** + * Return true when current page becomes visible again + */ +const useCurrenPageBecomesVisible = () => { + /** + * If true, the tab of current page is visible + */ + const isPageVisible = useVisibilityState(); + + /** + * previous value of the visibility state + */ + const isPageVisiblePrevState = usePrevious(isPageVisible); + + const currentPageIsVisibleAgain = useMemo(() => { + return isPageVisiblePrevState === false && isPageVisible === true; + }, [isPageVisible, isPageVisiblePrevState]); + + return currentPageIsVisibleAgain; +}; + +export default useCurrenPageBecomesVisible; diff --git a/src/hooks/useVisibilityState.tsx b/src/hooks/useVisibilityState.tsx new file mode 100644 index 0000000..91a24ee --- /dev/null +++ b/src/hooks/useVisibilityState.tsx @@ -0,0 +1,24 @@ +import React, { useEffect, useState } from 'react'; + +/** + * Use Page Visibility API to check and see if the current page is visible. + * + * When the user minimizes the window or switches to another tab, + * the API sends a visibilitychange event to let listeners know the state of the page has changed. + * @returns `boolean` if true, the current page is visible + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API + */ +const useVisibilityState = () => { + const [isPageVisible, setIsPageVisible] = useState(true); + + useEffect(() => { + document.addEventListener('visibilitychange', (event) => { + setIsPageVisible(document.visibilityState == 'visible'); + }); + }, []); + + return isPageVisible; +}; + +export default useVisibilityState; diff --git a/src/index.tsx b/src/index.tsx index bf6899e..51d9af0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,3 +1,4 @@ +import '@arcgis/core/assets/esri/themes/dark/main.css'; import './style/index.css'; import React from 'react'; @@ -7,7 +8,7 @@ import configureAppStore, { getPreloadedState } from '@store/configureStore'; import AppContextProvider from './contexts/AppContextProvider'; import WaybackManager from './services/wayback'; import { AppLayout } from '@components/index'; -import { initEsriOAuth, isAnonymouns, signIn } from '@utils/Esri-OAuth'; +import { initEsriOAuth } from '@utils/Esri-OAuth'; import config from './app-config'; import { getCustomPortalUrl } from '@utils/LocalStorage'; import { getServiceUrl } from '@utils/Tier'; diff --git a/src/services/export-wayback-bundle/getTileEstimationsInOutputBundle.ts b/src/services/export-wayback-bundle/getTileEstimationsInOutputBundle.ts index bdcdd55..f75f583 100644 --- a/src/services/export-wayback-bundle/getTileEstimationsInOutputBundle.ts +++ b/src/services/export-wayback-bundle/getTileEstimationsInOutputBundle.ts @@ -19,7 +19,7 @@ const WaybackImageBaseURL = getServiceUrl('wayback-imagery-base'); /** * maximum number of tiles allowed by the service */ -const MAX_NUM_TILES = 150000; +export const MAX_NUMBER_TO_TILES_PER_WAYPORT_EXPORT = 150000; /** * Get estimations of tiles that can be included in the output bundle. @@ -40,15 +40,15 @@ export const getTileEstimationsInOutputBundle = async ( ): Promise => { const tileEstimations: TileEstimation[] = []; - const UpperLimit = MAX_NUM_TILES * 1.1; + // const UpperLimit = MAX_NUM_TILES * 1.1; /** * a helper function to get estimation by zoom level recursively * @param zoomLevel * @returns void */ - const helper = (zoomLevel: number, total = 0) => { - if (total >= UpperLimit || zoomLevel > 23) { + const helper = (zoomLevel: number) => { + if (zoomLevel > 23) { return; } @@ -64,14 +64,19 @@ export const getTileEstimationsInOutputBundle = async ( const cols = Math.abs(tileColMax - tileColMin) + 1; const count = rows * cols; - if (total + count <= UpperLimit) { - tileEstimations.push({ - level: zoomLevel, - count, - }); - } + // if (total + count <= UpperLimit) { + // tileEstimations.push({ + // level: zoomLevel, + // count, + // }); + // } + + tileEstimations.push({ + level: zoomLevel, + count, + }); - helper(zoomLevel + 1, total + count); + helper(zoomLevel + 1); }; helper(minZoomLevel); @@ -88,6 +93,8 @@ export const getTileEstimationsInOutputBundle = async ( if (shouldBeIncluded) { output.push(tileEstimation); + } else { + break; } } diff --git a/src/services/export-wayback-bundle/wayportGPService.ts b/src/services/export-wayback-bundle/wayportGPService.ts index b5da839..0eeb572 100644 --- a/src/services/export-wayback-bundle/wayportGPService.ts +++ b/src/services/export-wayback-bundle/wayportGPService.ts @@ -3,6 +3,7 @@ import { getServiceUrl } from '@utils/Tier'; import { geographicToWebMercator } from '@arcgis/core/geometry/support/webMercatorUtils'; import Extent from '@arcgis/core/geometry/Extent'; import axios from 'axios'; +import { getToken } from '@utils/Esri-OAuth'; type GPJobStatus = | 'esriJobSubmitted' @@ -98,6 +99,7 @@ export const submitJob = async ({ const params = new URLSearchParams({ f: 'json', + token: getToken(), clump: layerIdentifier, levels: `${minZoom}-${maxZoom}`, extent: `${xmin} ${ymin} ${xmax} ${ymax} PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",0.0],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]]`, @@ -109,13 +111,19 @@ export const submitJob = async ({ const data = await res.json(); + if (data.error) { + throw data.error; + } + return data as SubmitJobResponse; }; export const checkJobStatus = async ( jobId: string ): Promise => { - const res = await fetch(`${WAYPORT_GP_SERVICE_ROOT}/jobs/${jobId}?f=json`); + const res = await fetch( + `${WAYPORT_GP_SERVICE_ROOT}/jobs/${jobId}?f=json&token=${getToken()}` + ); const data = await res.json(); @@ -126,7 +134,7 @@ export const getJobOutputInfo = async ( jobId: string ): Promise => { const outputRes = await fetch( - `${WAYPORT_GP_SERVICE_ROOT}/jobs/${jobId}/results/output?f=json` + `${WAYPORT_GP_SERVICE_ROOT}/jobs/${jobId}/results/output?f=json&token=${getToken()}` ); const data = (await outputRes.json()) as GetJobOutputResponse; diff --git a/src/store/DownloadMode/reducer.ts b/src/store/DownloadMode/reducer.ts index e8d3cd1..6061b60 100644 --- a/src/store/DownloadMode/reducer.ts +++ b/src/store/DownloadMode/reducer.ts @@ -94,6 +94,14 @@ export type DownloadModeState = { ids: string[]; }; isDownloadDialogOpen: boolean; + /** + * If true, the system is currently in the process of adding a new download job. + * + * Why is this necessary? When creating a new download job, the `getTileEstimationsInOutputBundle` function will be invoked, + * and this function might take 1-2 seconds to resolve. Therefore showing a loading indicator should inform the user + * that their request has been received. + */ + isAddingNewDownloadJob: boolean; }; export const initialDownloadModeState = { @@ -102,6 +110,7 @@ export const initialDownloadModeState = { ids: [], }, isDownloadDialogOpen: false, + isAddingNewDownloadJob: false, } as DownloadModeState; const slice = createSlice({ @@ -111,6 +120,9 @@ const slice = createSlice({ isDownloadDialogOpenToggled: (state) => { state.isDownloadDialogOpen = !state.isDownloadDialogOpen; }, + isAddingNewDownloadJobToggled: (state) => { + state.isAddingNewDownloadJob = !state.isAddingNewDownloadJob; + }, downloadJobCreated: (state, action: PayloadAction) => { const { id } = action.payload; state.jobs.byId[id] = action.payload; @@ -136,6 +148,7 @@ const { reducer } = slice; export const { isDownloadDialogOpenToggled, + isAddingNewDownloadJobToggled, downloadJobCreated, downloadJobRemoved, downloadJobsUpdated, diff --git a/src/store/DownloadMode/selectors.ts b/src/store/DownloadMode/selectors.ts index ca61b1e..4abc0e0 100644 --- a/src/store/DownloadMode/selectors.ts +++ b/src/store/DownloadMode/selectors.ts @@ -6,6 +6,11 @@ export const selectIsDownloadDialogOpen = createSelector( (isDownloadDialogOpen) => isDownloadDialogOpen ); +export const selectIsAddingNewDownloadJob = createSelector( + (state: RootState) => state.DownloadMode.isAddingNewDownloadJob, + (isAddingNewDownloadJob) => isAddingNewDownloadJob +); + export const selectDownloadJobs = createSelector( (state: RootState) => state.DownloadMode.jobs, (jobs) => { diff --git a/src/store/DownloadMode/thunks.ts b/src/store/DownloadMode/thunks.ts index 8d2362c..55317a9 100644 --- a/src/store/DownloadMode/thunks.ts +++ b/src/store/DownloadMode/thunks.ts @@ -7,6 +7,7 @@ import { downloadJobCreated, downloadJobRemoved, downloadJobsUpdated, + isAddingNewDownloadJobToggled, isDownloadDialogOpenToggled, } from './reducer'; import { nanoid } from 'nanoid'; @@ -41,20 +42,32 @@ let checkDownloadJobStatusTimeout: NodeJS.Timeout; const CHECK_JOB_STATUS_DELAY_IN_SECONDS = 15; -const GP_JOB_TIME_TO_LIVE_IN_SECONDS = 3600; +const DOWNLOAD_JOB_TIME_TO_LIVE_IN_SECONDS = 3600; + +/** + * Min tile package level should always be 12 by default. + * + * @see https://github.com/vannizhang/wayback/issues/90 + */ +const DEFAULT_MIN_LEVEL = 12; export const addToDownloadList = ({ releaseNum, zoomLevel, extent }: AddToDownloadListParams) => async (dispatch: StoreDispatch, getState: StoreGetState) => { // console.log(waybackItem, zoomLevel, extent); + batch(() => { + dispatch(isAddingNewDownloadJobToggled()); + dispatch(isDownloadDialogOpenToggled()); + }); + const { WaybackItems } = getState(); const { byReleaseNumber } = WaybackItems; const tileEstimations = await getTileEstimationsInOutputBundle( extent, - zoomLevel, + DEFAULT_MIN_LEVEL, releaseNum ); @@ -62,16 +75,17 @@ export const addToDownloadList = // return total + curr.count // }, 0) + const minZoomLevel = DEFAULT_MIN_LEVEL; const maxZoomLevel = tileEstimations[tileEstimations.length - 1].level; const downloadJob: DownloadJob = { id: nanoid(), waybackItem: byReleaseNumber[releaseNum], - minZoomLevel: zoomLevel, + minZoomLevel, maxZoomLevel, tileEstimations, // totalTiles, - levels: [zoomLevel, maxZoomLevel], + levels: [minZoomLevel, maxZoomLevel], extent, status: 'not started', // createdTime: new Date().getTime(), @@ -79,7 +93,7 @@ export const addToDownloadList = batch(() => { dispatch(downloadJobCreated(downloadJob)); - dispatch(isDownloadDialogOpenToggled()); + dispatch(isAddingNewDownloadJobToggled()); }); }; @@ -128,15 +142,22 @@ export const startDownloadJob = layerIdentifier: waybackItem.layerIdentifier, }); - const updatedJobData: DownloadJob = { + const submittedJob: DownloadJob = { ...byId[id], GPJobId: res.jobId, status: 'pending', }; - dispatch(downloadJobsUpdated([updatedJobData])); + dispatch(downloadJobsUpdated([submittedJob])); } catch (err) { console.log(err); + + const failedJob: DownloadJob = { + ...byId[id], + status: 'failed', + }; + + dispatch(downloadJobsUpdated([failedJob])); } }; @@ -240,12 +261,22 @@ export const cleanUpDownloadJobs = // find jobs that were finished more than 1 hour ago const jobsToBeRemoved = jobs.filter((job) => { - const ageOfJobInSeconds = (now - job.finishTime) / 1000; - return ( - ageOfJobInSeconds > GP_JOB_TIME_TO_LIVE_IN_SECONDS || - job.status === 'downloaded' || - job.status === 'failed' - ); + // downloaded job should be removed + if (job.status === 'downloaded') { + return true; + } + + // any finished job that is 1 hour old should be removed + if (job.finishTime) { + const secondsSinceJobWasFinished = + (now - job.finishTime) / 1000; + return ( + secondsSinceJobWasFinished > + DOWNLOAD_JOB_TIME_TO_LIVE_IN_SECONDS + ); + } + + false; }); for (const job of jobsToBeRemoved) { diff --git a/src/store/getPreloadedState.ts b/src/store/getPreloadedState.ts index 50928fe..d0a5a04 100644 --- a/src/store/getPreloadedState.ts +++ b/src/store/getPreloadedState.ts @@ -145,6 +145,7 @@ const getPreloadedState4Downloadmode = ( const { isDownloadDialogOpen } = urlParams; const jobs = getDownloadJobsFromLocalStorage(); + console.log(jobs); const byId = {}; const ids = []; diff --git a/src/style/index.css b/src/style/index.css index eedcdba..d924691 100644 --- a/src/style/index.css +++ b/src/style/index.css @@ -18,6 +18,7 @@ html, body { @apply p-0 m-0 w-full h-full overflow-hidden; color: var(--default-text-color); + background: #000; } #appRootDiv { @apply relative w-full h-full; @@ -32,6 +33,10 @@ html, body { outline: 0; } +#appRootDiv .esri-view-root { + --esri-view-outline: 0; +} + video, img { height: 0; } diff --git a/src/utils/Esri-OAuth/index.ts b/src/utils/Esri-OAuth/index.ts index 3c6e077..3ecae46 100644 --- a/src/utils/Esri-OAuth/index.ts +++ b/src/utils/Esri-OAuth/index.ts @@ -178,3 +178,33 @@ export const getUserRole = (): string => { export const getCredential = (): Credential => { return credential; }; + +export const revalidateToken = async () => { + // abort if not signed-in yet + if (isAnonymouns()) { + return; + } + + const token = getToken(); + + const portalBaseUrl = getPortalBaseUrl(); + + // use portal self request to re-validate the token, + // portal self request with an invalid token would throw an error + const requestURL = `${portalBaseUrl}/sharing/rest/portals/self?f=json&token=${token}`; + + try { + const res = await fetch(requestURL); + + const data = await res.json(); + + if (data.error) { + throw data.error; + } + } catch (err) { + console.log(err); + + // sign out if user token is invalid, means user has signed out from somewhere else + signOut(); + } +}; diff --git a/src/utils/UrlSearchParam/index.ts b/src/utils/UrlSearchParam/index.ts index e9a7f3b..40fa3ea 100644 --- a/src/utils/UrlSearchParam/index.ts +++ b/src/utils/UrlSearchParam/index.ts @@ -136,7 +136,7 @@ const saveFrames2ExcludeInURLQueryParam = (rNums: number[]): void => { export const saveMapCenterToHashParams = (center: MapCenter, zoom: number) => { const { lon, lat } = center; - const value = `${lon.toFixed(3)},${lat.toFixed(3)},${zoom}`; + const value = `${lon.toFixed(5)},${lat.toFixed(5)},${zoom}`; updateHashParams('mapCenter', value); // remove ext from URL as it is no longer needed updateHashParams('ext', null); diff --git a/src/utils/snippets/copy2clipborad.ts b/src/utils/snippets/copy2clipborad.ts new file mode 100644 index 0000000..b3c0199 --- /dev/null +++ b/src/utils/snippets/copy2clipborad.ts @@ -0,0 +1,21 @@ +export const copy2clipboard = (text: string) => { + // Create a temporary textarea element + const textarea = document.createElement('textarea'); + textarea.value = text; + + // Set the position to be off-screen + textarea.style.position = 'absolute'; + textarea.style.left = '-9999px'; + + // Append the textarea to the document + document.body.appendChild(textarea); + + // Select the text in the textarea + textarea.select(); + + // Execute the copy command + document.execCommand('copy'); + + // Remove the textarea from the document + document.body.removeChild(textarea); +};