diff --git a/package-lock.json b/package-lock.json
index 51baf51..2ff3a37 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,8 +15,10 @@
"chartjs-plugin-annotation": "^3.0.1",
"chartjs-plugin-crosshair": "^2.0.0",
"date-fns": "^3.6.0",
+ "lodash": "^4.17.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-helmet": "^6.1.0",
"react-joyride": "^2.9.2",
"react-share": "^5.1.0",
"styled-components": "^6.1.12"
@@ -4250,6 +4252,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
@@ -4938,6 +4946,12 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-fast-compare": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
+ "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
+ "license": "MIT"
+ },
"node_modules/react-floater": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/react-floater/-/react-floater-0.7.9.tgz",
@@ -4977,6 +4991,21 @@
"is-lite": "^0.8.2"
}
},
+ "node_modules/react-helmet": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
+ "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.7.2",
+ "react-fast-compare": "^3.1.1",
+ "react-side-effect": "^2.1.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.3.0"
+ }
+ },
"node_modules/react-innertext": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/react-innertext/-/react-innertext-1.1.5.tgz",
@@ -5048,6 +5077,15 @@
"react": "^17 || ^18"
}
},
+ "node_modules/react-side-effect": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
+ "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.3.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
diff --git a/package.json b/package.json
index a92f78d..8537f2a 100644
--- a/package.json
+++ b/package.json
@@ -17,8 +17,10 @@
"chartjs-plugin-annotation": "^3.0.1",
"chartjs-plugin-crosshair": "^2.0.0",
"date-fns": "^3.6.0",
+ "lodash": "^4.17.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-helmet": "^6.1.0",
"react-joyride": "^2.9.2",
"react-share": "^5.1.0",
"styled-components": "^6.1.12"
diff --git a/public/temp-disclaimer.png b/public/temp-disclaimer.png
new file mode 100644
index 0000000..164c05f
Binary files /dev/null and b/public/temp-disclaimer.png differ
diff --git a/src/App.jsx b/src/App.jsx
index 9bc7a01..ceef109 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -9,6 +9,7 @@ import {
ErrorContext
} from './contexts/AppContext';
import { VideoProvider } from './contexts/VideoContext';
+import { DataFetchingProvider } from './contexts/DataFetchingContext';
import config from './config.json';
import RotateOverlay from './components/RotateOverlay';
import Tour from './components/Tour';
@@ -34,22 +35,24 @@ export default function App() {
- How hot could it get on the hottest day in a given year, under different greenhouse gas emission scenarios?
-- See estimates of annual maxima of daily maximum near-surface air temperature (TASMAX) from NASA Earth Exchange (NEX) Global Daily Downscaled Projections (GDDP) based on simulations of the Coupled Model Intercomparison Project Phase 6 (CMIP6).
-- The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude grid and temperature in major cities could be higher than what is displayed within each gridded cell.
-+ How hot could it get on the hottest day in a given year, under different greenhouse gas emission scenarios? +
++ See estimates of annual maxima of daily maximum near-surface air temperature (TASMAX) from NASA Earth Exchange (NEX) Global Daily Downscaled Projections (GDDP) based on simulations of the Coupled Model Intercomparison Project Phase 6 (CMIP6). +
++ The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude grid, which is a system of lines used to map the sphere of the Earth. + In some cases, the temperature in major cities could be higher than what’s displayed in the gridded cell because it includes a larger area than just that city. + For example, if you search for a city, such as Los Angeles, CA the average will include the temperature of Los Angeles (which could be higher than average) plus the surrounding geographical area (which could be lower than average). +
++ Learn more about the + {' '} + + NEX-GDDP-CMIP6 dataset + ! +
++ The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude grid, which is a system of lines used to map the sphere of the Earth. + In some cases, the temperature in major cities could be higher than what’s displayed in the gridded cell because it includes a larger area than just that city. + For example, if you search for a city, such as Los Angeles, CA the average will include the temperature of Los Angeles (which could be higher than average) plus the surrounding geographical area (which could be lower than average). +
+ + ); + +} + +function SwapButton({setShowPageTwo}){ + return +} diff --git a/src/components/LineChart.jsx b/src/components/LineChart.jsx index d923117..867f11e 100644 --- a/src/components/LineChart.jsx +++ b/src/components/LineChart.jsx @@ -59,7 +59,7 @@ export default function LineChart({ selectedIndex, isFahrenheit }) { borderColor: selectedIndex === 0 ? '#FFFFFFBF' - : 'rgba(239, 239, 240, 0.2)', + : 'rgba(239, 239, 240, 0.4)', borderWidth: selectedIndex === 0 ? 1 : 0.5, fill: false, pointRadius: 0 @@ -72,7 +72,7 @@ export default function LineChart({ selectedIndex, isFahrenheit }) { borderColor: selectedIndex === 1 ? '#FFFFFFBF' - : 'rgba(239, 239, 240, 0.2)', + : 'rgba(239, 239, 240, 0.4)', borderWidth: selectedIndex === 1 ? 1 : 0.5, fill: false, pointRadius: 0 @@ -85,7 +85,7 @@ export default function LineChart({ selectedIndex, isFahrenheit }) { borderColor: selectedIndex === 2 ? '#FFFFFFBF' - : 'rgba(239, 239, 240, 0.2)', + : 'rgba(239, 239, 240, 0.4)', borderWidth: selectedIndex === 2 ? 1 : 0.5, fill: false, pointRadius: 0 @@ -98,7 +98,7 @@ export default function LineChart({ selectedIndex, isFahrenheit }) { borderColor: selectedIndex === 3 ? '#FFFFFFBF' - : 'rgba(239, 239, 240, 0.2)', + : 'rgba(239, 239, 240, 0.4)', borderWidth: selectedIndex === 3 ? 1 : 0.5, fill: false, pointRadius: 0 diff --git a/src/components/Map.jsx b/src/components/Map.jsx index dce9a49..4ebd4bc 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -16,6 +16,7 @@ import SceneView from '@arcgis/core/views/SceneView'; import Search from '@arcgis/core/widgets/Search'; import Popup from '@arcgis/core/widgets/Popup'; import { VideoContext } from '../contexts/VideoContext'; +import { DataFetchingContext } from '../contexts/DataFetchingContext'; import { ChartDataContext, MapViewContext, @@ -27,14 +28,17 @@ import { FRAME_DURATION, TOTAL_FRAMES, FPS } from '../utils/constants'; import { Transition } from '@headlessui/react'; import Expand from '@arcgis/core/widgets/Expand'; import { isMobileDevice } from '../utils/helpers'; -import { - bufferSymbol, - crosshairSymbol, - createCornerAngles -} from '../utils/sceneHelpers'; +import { crosshairSymbol, bufferSymbol } from '../utils/sceneHelpers'; +import { debounce } from 'lodash'; import ShareModal from './ShareModal'; +const defaultScenePoint = new Point({ + longitude: -77.0369, + latitude: 38.9072, + spatialReference: { wkid: 4326 } +}); + const createFeatureLayer = (url) => new FeatureLayer({ url, @@ -59,6 +63,8 @@ export default function Home() { const { mapView, setMapView } = useContext(MapViewContext); const { setChartData } = useContext(ChartDataContext); const { dataSelection } = useContext(DataSelectionContext); + const { setIsLoading, setIsInvalidData } = useContext(DataFetchingContext); + const [selectedDataset, selectedVariable] = dataSelection; const selectedDatasetVariables = config.datasets[0].variables; @@ -81,7 +87,7 @@ export default function Home() { let draggingInsideBuffer = false; let initialCamera; - let lastKnownPoint; + let lastKnownPoint = defaultScenePoint; let bufferLayer; let pointLayer; @@ -93,9 +99,22 @@ export default function Home() { return { bufferLayer, pointLayer }; }; - const createBuffer = async (point, pointLayer, bufferLayer) => { - const sideLength = 10; + const createBuffer = async (point, pointLayer, bufferLayer, view) => { + const zoomLevel = view.zoom; + + const baseMiddleRadius = 300; + const scaleFactor = zoomLevel / 3; + + const middleRadius = baseMiddleRadius / scaleFactor; + + const middleBufferSymbol = { + type: 'simple-fill', + color: [150, 50, 0, 0.0], + outline: { color: [255, 255, 255, 1], width: 2, style:"dash"} + }; + + const sideLength = 0.25*scaleFactor; const squarePolygon = { type: 'polygon', rings: [ @@ -110,52 +129,43 @@ export default function Home() { spatialReference: point.spatialReference }; - const cornerAngles = createCornerAngles(point, sideLength); - - const angleSymbol = { - type: 'simple-line', - color: [255, 255, 255], - width: 1 - }; - const bufferGraphic = new Graphic({ geometry: squarePolygon, symbol: bufferSymbol }); - if (!pointLayer.graphics.length) { - pointLayer.add( - new Graphic({ geometry: point, symbol: crosshairSymbol }) - ); - bufferLayer.add(bufferGraphic); - cornerAngles.forEach((cornerGeometry) => { - bufferLayer.add( - new Graphic({ - geometry: cornerGeometry, - symbol: angleSymbol - }) - ); - }); - } else { - pointLayer.graphics.getItemAt(0).geometry = point; + const middleCircle = await geometryEngineAsync.geodesicBuffer( + point, + middleRadius, + 'kilometers' + ); - bufferLayer.graphics.getItemAt(0).geometry = squarePolygon; - bufferLayer.graphics.getItemAt(0).symbol = bufferSymbol; + const middleBufferGraphic = new Graphic({ + geometry: middleCircle, + symbol: middleBufferSymbol + }); + + const crosshairGraphic = new Graphic({ + geometry: point, + symbol: crosshairSymbol + }); - bufferLayer.removeAll(); + if (!pointLayer.graphics.length) { + pointLayer.add(crosshairGraphic); + bufferLayer.add(middleBufferGraphic); bufferLayer.add(bufferGraphic); - cornerAngles.forEach((cornerGeometry) => { - bufferLayer.add( - new Graphic({ - geometry: cornerGeometry, - symbol: angleSymbol - }) - ); - }); + } else { + pointLayer.graphics.getItemAt(0).geometry = point; + bufferLayer.graphics.getItemAt(0).geometry = middleCircle; + bufferLayer.graphics.getItemAt(0).symbol = middleBufferSymbol; + bufferLayer.graphics.getItemAt(1).geometry = squarePolygon; + bufferLayer.graphics.getItemAt(1).symbol = bufferSymbol; } }; + const debouncedCreateBuffer = debounce(createBuffer, 100); + const handleDragStart = async (event, view, bufferLayer) => { const startPoint = view.toMap({ x: event.x, y: event.y }); const bufferGraphic = bufferLayer.graphics.getItemAt(0); @@ -180,7 +190,7 @@ export default function Home() { if (updatedPoint) { event.stopPropagation(); - await createBuffer(updatedPoint, pointLayer, bufferLayer); + await createBuffer(updatedPoint, pointLayer, bufferLayer, view); lastKnownPoint = updatedPoint; } } @@ -341,7 +351,12 @@ export default function Home() { ], zoom: 1 }); - await createBuffer(initialCenterPoint, pointLayer, bufferLayer); + await createBuffer( + initialCenterPoint, + pointLayer, + bufferLayer, + view + ); await handleMapClick({ mapPoint: initialCenterPoint }); view.on('drag', (event) => { @@ -358,11 +373,22 @@ export default function Home() { const mapPoint = view.toMap(event); if (mapPoint) { - await createBuffer(mapPoint, pointLayer, bufferLayer); + await createBuffer(mapPoint, pointLayer, bufferLayer, view); lastKnownPoint = mapPoint; await handleMapClick({ mapPoint }, view); } }); + + view.watch('zoom', () => { + if (lastKnownPoint) { + debouncedCreateBuffer( + lastKnownPoint, + pointLayer, + bufferLayer, + view + ); + } + }); }).catch((error) => { if (error.name.includes('webgl')) { setHasWebGLError(true); @@ -419,7 +445,7 @@ export default function Home() { view.graphics.removeAll(); - await createBuffer(point, pointLayer, bufferLayer); + await createBuffer(point, pointLayer, bufferLayer, view); lastKnownPoint = point; await handleMapClick({ mapPoint: point }, view); @@ -453,51 +479,63 @@ export default function Home() { const handleMapClick = async (event, view) => { const [_, selectedVariable] = dataSelection; + // round event latitude and longitude to the nearest quarter + // degree to snap to grid + event.mapPoint.latitude = Math.round(event.mapPoint.latitude * 4) / 4; + event.mapPoint.longitude = Math.round(event.mapPoint.longitude * 4) / 4; + const dataIsValid = await handleImageServiceRequest( event, selectedVariable, - setChartData + setChartData, + setIsLoading, + setIsInvalidData ); if (!dataIsValid) { - const defaultScenePoint = new Point({ - longitude: -77.0369, - latitude: 38.9072, - spatialReference: { wkid: 4326 } - }); - - if ( - Math.abs( - event.mapPoint.longitude - defaultScenePoint.longitude - ) > 0.0001 || - Math.abs(event.mapPoint.latitude - defaultScenePoint.latitude) > - 0.0001 - ) { - await view.goTo({ - center: [ - defaultScenePoint.longitude, - defaultScenePoint.latitude - ], - zoom: 10 - }); - - await createBuffer(defaultScenePoint, pointLayer, bufferLayer); - - const eventForDC = { mapPoint: defaultScenePoint }; - const dataIsValidDC = await handleImageServiceRequest( - eventForDC, - selectedVariable, - setChartData - ); + lastKnownPoint = defaultScenePoint; + + setTimeout(async () => { + if ( + Math.abs( + event.mapPoint.longitude - defaultScenePoint.longitude + ) > 0.0001 || + Math.abs( + event.mapPoint.latitude - defaultScenePoint.latitude + ) > 0.0001 + ) { + await view.goTo({ + center: [ + defaultScenePoint.longitude, + defaultScenePoint.latitude + ], + zoom: 10 + }); - if (!dataIsValidDC) { + await createBuffer( + defaultScenePoint, + pointLayer, + bufferLayer, + view + ); + + const eventForDC = { mapPoint: defaultScenePoint }; + const dataIsValidDC = await handleImageServiceRequest( + eventForDC, + selectedVariable, + setChartData, + setIsLoading, + setIsInvalidData + ); + + if (!dataIsValidDC) { + console.error('Data is invalid even for Washington DC'); + } + } else { console.error('Data is invalid even for Washington DC'); setChartData([]); } - } else { - console.error('Data is invalid even for Washington DC'); - setChartData([]); - } + }, 1000); } }; diff --git a/src/components/Panel.jsx b/src/components/Panel.jsx index 323e737..3e4de3c 100644 --- a/src/components/Panel.jsx +++ b/src/components/Panel.jsx @@ -1,11 +1,12 @@ import LineChart from './LineChart'; -import { useContext, useState, useMemo } from 'react'; +import { useContext, useState, useMemo, useEffect } from 'react'; import config from '../config.json'; import { DataSelectionContext, MapViewContext, ChartDataContext } from '../contexts/AppContext'; +import { DataFetchingContext } from '../contexts/DataFetchingContext'; import { VideoContext } from '../contexts/VideoContext'; import { PlayIcon, PauseIcon } from '@heroicons/react/24/solid'; import { FPS, TOTAL_FRAMES } from '../utils/constants'; @@ -26,6 +27,9 @@ export default function Panel() { } = useContext(VideoContext); const { mapView } = useContext(MapViewContext); const { setDataSelection } = useContext(DataSelectionContext); + const { isLoading, isInvalidData } = useContext(DataFetchingContext); + const [wasInvalidShown, setWasInvalidShown] = useState(false); + const [isFahrenheit, setIsFahrenheit] = useState(true); const { chartData } = useContext(ChartDataContext); @@ -117,6 +121,16 @@ export default function Panel() { ); }; + useEffect(() => { + if (isInvalidData) { + setWasInvalidShown(true); + } else { + // Reset wasInvalidShown after a short delay or when the invalid message is gone + const timer = setTimeout(() => setWasInvalidShown(false), 2000); // 2-second delay + return () => clearTimeout(timer); // Cleanup the timer on component unmount or update + } + }, [isInvalidData]); + return ( <> {isModalOpen && ( @@ -215,85 +229,122 @@ export default function Panel() { className="flex-1" style={{ minWidth: '600px' }} > -+ Fetching temperature + information for your + selected location... +
++ This dataset does not provide + temperature information for + oceans. Moving the map marker to + the default location... +
++ The NEX-GDDP-CMIP6 data is calculated on a 0.25°x0.25° latitude and longitude grid, which is a system of lines used to map the sphere of the Earth. + In some cases, the temperature in major cities could be higher than what’s displayed in the gridded cell because it includes a larger area than just that city. + For example, if you search for a city, such as Los Angeles, CA the average will include the temperature of Los Angeles (which could be higher than average) plus the surrounding geographical area (which could be lower than average). + +
+