From e3af0197b7f4292be6829f34669de250fed68dee Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sun, 3 May 2026 07:09:44 -0400 Subject: [PATCH] feat(monitor/frontend): globe centerpiece (merged-points, imperative camera, no decorative motion) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit react-globe.gl 2.37 + three 0.184 added. Globe component is React.memo'd, absolute-fills the .center grid cell, sizes itself via ResizeObserver on the wrapper div (the library defaults to window.innerWidth/Height which would blow past the 1247px cell). pointsMerge: true so hundreds of points cost one ThreeJS Mesh draw call. Layer dataset built by useGlobePoints / useGlobeRings hooks in globeLayers.ts, projecting each store point onto color/altitude/radius lookup tables keyed on the 6 GlobePointTypes: earthquake/ransomware/scan = monochrome (--fg-2/--fg-1/--fg-4) iss = --ok (live position is functional, not decorative) outage/hijack = --amber (the only globe surfaces with alarm color — earthquakes and ransomware stay monochrome because an M2.5 quake or daily victim is not user-actionable) Rings are simple white pulses (no per-ring color decoration), single pulse (ringRepeatPeriod=0). Camera panTo() in globeCamera.ts is the only motion API; the dashboard lifecycle (Task 24) will trigger it on high-mag quakes etc. Auto-rotate disabled on globe ready — motion is meaning, a globe that spins for no reason violates the rule. atmosphereAltitude=0.12, atmosphereColor=#1f2937 per spec §10.4 — a thin glow that gives the sphere silhouette without becoming "3D space ambiance." backgroundColor=rgba(0,0,0,0) so the panel cell --bg shows through. No starfield, no nebulas. No globeImageUrl — keeps the build self-contained (no CDN dep); we add a local texture later if geographic context needs to be richer. Plan 5 Task 12 Design QA: no starfield/nebula/extra glow, monochrome quake dots, amber confined to outage/hijack, no hover tooltip on globe. --- .../frontend/package.json | 2 + .../frontend/pnpm-lock.yaml | 429 +++++++++++++++++- .../src/pages/dashboard/Dashboard.tsx | 5 +- .../src/pages/globe/Globe.module.scss | 11 + .../frontend/src/pages/globe/Globe.tsx | 84 ++++ .../frontend/src/pages/globe/globeCamera.ts | 20 + .../frontend/src/pages/globe/globeLayers.ts | 78 ++++ 7 files changed, 626 insertions(+), 3 deletions(-) create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/Globe.module.scss create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/Globe.tsx create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/globeCamera.ts create mode 100644 PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/globeLayers.ts diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/package.json b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/package.json index aa631e5f..b2d6c562 100644 --- a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/package.json +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/package.json @@ -22,10 +22,12 @@ "react": "^19.2.1", "react-dom": "^19.2.0", "react-error-boundary": "^6.0.0", + "react-globe.gl": "^2.37.1", "react-icon": "^1.0.0", "react-icons": "^5.5.0", "react-router-dom": "^7.1.1", "sonner": "^2.0.7", + "three": "^0.184.0", "zod": "^4.1.13", "zustand": "^5.0.9" }, diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/pnpm-lock.yaml b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/pnpm-lock.yaml index 51c9446a..0d03bdab 100644 --- a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/pnpm-lock.yaml +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: react-error-boundary: specifier: ^6.0.0 version: 6.0.0(react@19.2.1) + react-globe.gl: + specifier: ^2.37.1 + version: 2.37.1(react@19.2.1) react-icon: specifier: ^1.0.0 version: 1.0.0(babel-runtime@5.8.38)(react@19.2.1) @@ -38,6 +41,9 @@ importers: sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + three: + specifier: ^0.184.0 + version: 0.184.0 zod: specifier: ^4.1.13 version: 4.1.13 @@ -536,6 +542,18 @@ packages: peerDependencies: react: ^18 || ^19 + '@turf/boolean-point-in-polygon@7.3.5': + resolution: {integrity: sha512-ba7+B0wzaS9GtERZOoXUZ6oW8IcIJHNQZf3c+tiD9ESjcsPO1Q/4qIJGTKl92nBLhhracHJxMWBM/U6hAVkaRg==} + + '@turf/helpers@7.3.5': + resolution: {integrity: sha512-E/NMGV5MwbjjP7AJXBtsanC3yY8N2MQ87IGdIgkB2ji5AtBpwnH4L3gEqpYN4RlCJJWbLbzO91BbKv2waUd0eg==} + + '@turf/invariant@7.3.5': + resolution: {integrity: sha512-ZVIvsBvjr8lO7WxC5zYNjRsjSDvyGvWkJMjuWaJjTU8x+1tmfNnw3gDX/TI2Sit83gcRYLYkNo23lB/udqx/Hg==} + + '@tweenjs/tween.js@25.0.0': + resolution: {integrity: sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -560,6 +578,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/node@24.10.2': resolution: {integrity: sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==} @@ -617,6 +638,10 @@ packages: '@vitest/utils@4.1.5': resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} + accessor-fn@1.5.3: + resolution: {integrity: sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==} + engines: {node: '>=12'} + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -740,6 +765,65 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo-voronoi@2.1.0: + resolution: {integrity: sha512-kqE4yYuOjPbKdBXG0xztCacPwkVSK2REF1opSNrnqqtXJmNcM++UbwQ8SxvwP6IQTj9RvIjjK4qeiVsEfj0Z2Q==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-octree@1.1.0: + resolution: {integrity: sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-tricontour@1.1.0: + resolution: {integrity: sha512-G7gHKj89n2owmkGb6WX6ixcnQ0Kf/0wpa9VIh9DGdbHu8wdrlaHU4ir3/bFNERl8N8nn4G7e7qbtBG8N9caihQ==} + engines: {node: '>=12'} + + data-bind-mapper@1.0.3: + resolution: {integrity: sha512-QmU3lyEnbENQPo0M1F9BMu4s6cqNNp8iJA+b/HP2sSb7pf3dxwF3+EP1eO69rwBfH9kFJ1apmzrtogAmVt2/Xw==} + engines: {node: '>=12'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -749,6 +833,9 @@ packages: supports-color: optional: true + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -770,6 +857,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + earcut@3.0.2: + resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==} + electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} @@ -862,6 +952,10 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + float-tooltip@1.7.5: + resolution: {integrity: sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==} + engines: {node: '>=12'} + follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -875,6 +969,9 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + frame-ticker@1.0.3: + resolution: {integrity: sha512-E0X2u2JIvbEMrqEg5+4BpTqaD22OwojJI63K7MdKHdncjtAhGRbCR8nJCr2vwEt9NWBPCPcu70X9smPviEBy8Q==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -911,6 +1008,10 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + globe.gl@2.45.3: + resolution: {integrity: sha512-oR7iZBjD60ltBcRlHFvlEzKGL8PMfk2Xl19vSfu1SN4SWXAs3NB3kfelanL+wj5XgG2rTQiNtPQncCR2O9oj+w==} + engines: {node: '>=12'} + globjoin@0.1.4: resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} @@ -921,6 +1022,10 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + h3-js@4.4.0: + resolution: {integrity: sha512-DvJh07MhGgY2KcC4OeZc8SSyA+ZXpdvoh6uCzGpoKvWtZxJB+g6VXXC1+eWYkaMIsLz7J/ErhOalHCpcs1KYog==} + engines: {node: '>=4', npm: '>=3', yarn: '>=1.3.0'} + happy-dom@20.9.0: resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==} engines: {node: '>=20.0.0'} @@ -971,9 +1076,17 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + index-array-by@1.4.2: + resolution: {integrity: sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==} + engines: {node: '>=12'} + ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -1000,6 +1113,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jerrypick@1.1.2: + resolution: {integrity: sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==} + engines: {node: '>=12'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1023,6 +1140,10 @@ packages: engines: {node: '>=6'} hasBin: true + kapsule@1.16.3: + resolution: {integrity: sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==} + engines: {node: '>=12'} + keyv@5.5.5: resolution: {integrity: sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==} @@ -1110,9 +1231,16 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lodash.truncate@4.4.2: resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1174,6 +1302,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -1203,6 +1335,13 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + point-in-polygon-hao@1.2.4: + resolution: {integrity: sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==} + + polished@4.3.1: + resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} + engines: {node: '>=10'} + postcss-media-query-parser@0.2.3: resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} @@ -1232,6 +1371,12 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + preact@10.29.1: + resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -1252,6 +1397,12 @@ packages: peerDependencies: react: '>=16.13.1' + react-globe.gl@2.37.1: + resolution: {integrity: sha512-/YvnaB+ID+WTBTx5Qa67fGkGhco2jPlKg4jbXNTNWSl22/it7GrfByZ4nksgUDy4176VWuHh4BFvQg+hziSk8Q==} + engines: {node: '>=12'} + peerDependencies: + react: '*' + react-icon@1.0.0: resolution: {integrity: sha512-VzSlpBHnLanVw79mOxyq98hWDi6DlxK9qPiZ1bAK6bLurMBCaxO/jjyYUrRx9+JGLc/NbnwOmyE/W5Qglbb2QA==} peerDependencies: @@ -1263,6 +1414,15 @@ packages: peerDependencies: react: '*' + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-kapsule@2.5.7: + resolution: {integrity: sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.13.1' + react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -1308,6 +1468,9 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + rolldown-vite@7.2.5: resolution: {integrity: sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1378,6 +1541,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simplesignal@2.1.7: + resolution: {integrity: sha512-PEo2qWpUke7IMhlqiBxrulIFvhJRLkl1ih52Rwa+bPjzhJepcd4GIjn2RiQmFSx3dQvsEAgF0/lXMwMN7vODaA==} + sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -1479,9 +1645,45 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} + three-conic-polygon-geometry@2.1.2: + resolution: {integrity: sha512-NaP3RWLJIyPGI+zyaZwd0Yj6rkoxm4FJHqAX1Enb4L64oNYLCn4bz1ESgOEYavgcUwCNYINu1AgEoUBJr1wZcA==} + engines: {node: '>=12'} + peerDependencies: + three: '>=0.72.0' + + three-geojson-geometry@2.1.1: + resolution: {integrity: sha512-dC7bF3ri1goDcihYhzACHOBQqu7YNNazYLa2bSydVIiJUb3jDFojKSy+gNj2pMkqZNSVjssSmdY9zlmnhEpr1w==} + engines: {node: '>=12'} + peerDependencies: + three: '>=0.72.0' + + three-globe@2.45.2: + resolution: {integrity: sha512-3qJE2LAdyHsUPt02mgMRc+PG3j9kGEA0fUYrwKPGIVtvMR1XjDn9hCXu31AWocdgHOFcXkrRVz7jJZzTIvR0eQ==} + engines: {node: '>=12'} + peerDependencies: + three: '>=0.154' + + three-render-objects@1.41.1: + resolution: {integrity: sha512-0H7l7yREPVKfO3HL7RjPQ67T0phHgnyMeEc4ww/OCEfK6jbsm7psEcrR0SGFqGDyS/pDQTPi4DyPbS/xlHRJKw==} + engines: {node: '>=12'} + peerDependencies: + three: '>=0.179' + + three-slippy-map-globe@1.0.6: + resolution: {integrity: sha512-PCUR+X+1kYFYtQBf8+b/ct8xBHtnkeu7FItRYBeFxyIe3ksnGuLi0H9RAxAfVSSUsZVbKIKNz9q1atEjynrrkg==} + engines: {node: '>=12'} + peerDependencies: + three: '>=0.154' + + three@0.184.0: + resolution: {integrity: sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@1.1.2: resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} engines: {node: '>=18'} @@ -2010,6 +2212,27 @@ snapshots: '@tanstack/query-core': 5.90.12 react: 19.2.1 + '@turf/boolean-point-in-polygon@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@turf/invariant': 7.3.5 + '@types/geojson': 7946.0.16 + point-in-polygon-hao: 1.2.4 + tslib: 2.8.1 + + '@turf/helpers@7.3.5': + dependencies: + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/invariant@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@tweenjs/tween.js@25.0.0': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -2045,6 +2268,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/geojson@7946.0.16': {} + '@types/node@24.10.2': dependencies: undici-types: 7.16.0 @@ -2127,6 +2352,8 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + accessor-fn@1.5.3: {} + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -2239,10 +2466,75 @@ snapshots: csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-format@3.1.2: {} + + d3-geo-voronoi@2.1.0: + dependencies: + d3-array: 3.2.4 + d3-delaunay: 6.0.4 + d3-geo: 3.1.1 + d3-tricontour: 1.1.0 + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-octree@1.1.0: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-tricontour@1.1.0: + dependencies: + d3-delaunay: 6.0.4 + d3-scale: 4.0.2 + + data-bind-mapper@1.0.3: + dependencies: + accessor-fn: 1.5.3 + debug@4.4.3: dependencies: ms: 2.1.3 + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + delayed-stream@1.0.0: {} detect-libc@1.0.3: @@ -2260,6 +2552,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + earcut@3.0.2: {} + electron-to-chromium@1.5.267: {} emoji-regex@8.0.0: {} @@ -2339,6 +2633,12 @@ snapshots: flatted@3.4.2: {} + float-tooltip@1.7.5: + dependencies: + d3-selection: 3.0.0 + kapsule: 1.16.3 + preact: 10.29.1 + follow-redirects@1.15.11: {} form-data@4.0.5: @@ -2349,6 +2649,10 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + frame-ticker@1.0.3: + dependencies: + simplesignal: 2.1.7 + fsevents@2.3.3: optional: true @@ -2397,12 +2701,23 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + globe.gl@2.45.3: + dependencies: + '@tweenjs/tween.js': 25.0.0 + accessor-fn: 1.5.3 + kapsule: 1.16.3 + three: 0.184.0 + three-globe: 2.45.2(three@0.184.0) + three-render-objects: 1.41.1(three@0.184.0) + globjoin@0.1.4: {} globrex@0.1.2: {} gopd@1.2.0: {} + h3-js@4.4.0: {} + happy-dom@20.9.0: dependencies: '@types/node': 24.10.2 @@ -2448,8 +2763,12 @@ snapshots: imurmurhash@0.1.4: {} + index-array-by@1.4.2: {} + ini@1.3.8: {} + internmap@2.0.3: {} + is-arrayish@0.2.1: {} is-extglob@2.1.1: {} @@ -2466,6 +2785,8 @@ snapshots: isexe@2.0.0: {} + jerrypick@1.1.2: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -2480,6 +2801,10 @@ snapshots: json5@2.2.3: {} + kapsule@1.16.3: + dependencies: + lodash-es: 4.18.1 + keyv@5.5.5: dependencies: '@keyv/serialize': 1.1.1 @@ -2539,8 +2864,14 @@ snapshots: lines-and-columns@1.2.4: {} + lodash-es@4.18.1: {} + lodash.truncate@4.4.2: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -2585,6 +2916,8 @@ snapshots: normalize-path@3.0.0: {} + object-assign@4.1.1: {} + obug@2.1.1: {} parent-module@1.0.1: @@ -2608,6 +2941,14 @@ snapshots: picomatch@4.0.3: {} + point-in-polygon-hao@1.2.4: + dependencies: + robust-predicates: 3.0.3 + + polished@4.3.1: + dependencies: + '@babel/runtime': 7.28.4 + postcss-media-query-parser@0.2.3: {} postcss-resolve-nested-selector@0.1.6: {} @@ -2633,6 +2974,14 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact@10.29.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + proxy-from-env@1.1.0: {} qified@0.5.3: @@ -2651,6 +3000,13 @@ snapshots: '@babel/runtime': 7.28.4 react: 19.2.1 + react-globe.gl@2.37.1(react@19.2.1): + dependencies: + globe.gl: 2.45.3 + prop-types: 15.8.1 + react: 19.2.1 + react-kapsule: 2.5.7(react@19.2.1) + react-icon@1.0.0(babel-runtime@5.8.38)(react@19.2.1): dependencies: babel-runtime: 5.8.38 @@ -2660,6 +3016,13 @@ snapshots: dependencies: react: 19.2.1 + react-is@16.13.1: {} + + react-kapsule@2.5.7(react@19.2.1): + dependencies: + jerrypick: 1.1.2 + react: 19.2.1 + react-refresh@0.18.0: {} react-router-dom@7.10.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1): @@ -2688,6 +3051,8 @@ snapshots: reusify@1.1.0: {} + robust-predicates@3.0.3: {} + rolldown-vite@7.2.5(@types/node@24.10.2)(sass@1.95.0): dependencies: '@oxc-project/runtime': 0.97.0 @@ -2744,6 +3109,8 @@ snapshots: signal-exit@4.1.0: {} + simplesignal@2.1.7: {} + sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -2885,8 +3252,67 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + three-conic-polygon-geometry@2.1.2(three@0.184.0): + dependencies: + '@turf/boolean-point-in-polygon': 7.3.5 + d3-array: 3.2.4 + d3-geo: 3.1.1 + d3-geo-voronoi: 2.1.0 + d3-scale: 4.0.2 + delaunator: 5.1.0 + earcut: 3.0.2 + three: 0.184.0 + + three-geojson-geometry@2.1.1(three@0.184.0): + dependencies: + d3-geo: 3.1.1 + d3-interpolate: 3.0.1 + earcut: 3.0.2 + three: 0.184.0 + + three-globe@2.45.2(three@0.184.0): + dependencies: + '@tweenjs/tween.js': 25.0.0 + accessor-fn: 1.5.3 + d3-array: 3.2.4 + d3-color: 3.1.0 + d3-geo: 3.1.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + data-bind-mapper: 1.0.3 + frame-ticker: 1.0.3 + h3-js: 4.4.0 + index-array-by: 1.4.2 + kapsule: 1.16.3 + three: 0.184.0 + three-conic-polygon-geometry: 2.1.2(three@0.184.0) + three-geojson-geometry: 2.1.1(three@0.184.0) + three-slippy-map-globe: 1.0.6(three@0.184.0) + tinycolor2: 1.6.0 + + three-render-objects@1.41.1(three@0.184.0): + dependencies: + '@tweenjs/tween.js': 25.0.0 + accessor-fn: 1.5.3 + float-tooltip: 1.7.5 + kapsule: 1.16.3 + polished: 4.3.1 + three: 0.184.0 + + three-slippy-map-globe@1.0.6(three@0.184.0): + dependencies: + d3-geo: 3.1.1 + d3-octree: 1.1.0 + d3-scale: 4.0.2 + three: 0.184.0 + + three@0.184.0: {} + tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@1.1.2: {} tinyglobby@0.2.15: @@ -2906,8 +3332,7 @@ snapshots: optionalDependencies: typescript: 5.9.3 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} typescript@5.9.3: {} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx index 45621d2b..e45468c5 100644 --- a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx @@ -1,6 +1,7 @@ // ©AngelaMos | 2026 // Dashboard.tsx +import { Globe } from '@/pages/globe/Globe' import { useUIStore } from '@/stores/ui' import { AlertBanner } from './AlertBanner' import { BottomTicker } from './BottomTicker' @@ -22,7 +23,9 @@ export function Dashboard(): React.ReactElement {
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/Globe.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/Globe.module.scss new file mode 100644 index 00000000..e1c9b1ad --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/Globe.module.scss @@ -0,0 +1,11 @@ +// ©AngelaMos | 2026 +// Globe.module.scss + +.globeWrap { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/Globe.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/Globe.tsx new file mode 100644 index 00000000..f308606f --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/Globe.tsx @@ -0,0 +1,84 @@ +// ©AngelaMos | 2026 +// Globe.tsx + +import { memo, useEffect, useRef, useState } from 'react' +import GlobeGL, { type GlobeMethods } from 'react-globe.gl' +import { useGlobeEvents } from '@/stores/globeEvents' +import styles from './Globe.module.scss' +import { panTo } from './globeCamera' +import { useGlobePoints, useGlobeRings } from './globeLayers' + +const ATMOSPHERE_COLOR = '#1f2937' +const ATMOSPHERE_ALTITUDE = 0.12 +const RING_COLOR = '#e5e5e5' +const RING_MAX_RADIUS = 4 +const RING_PROPAGATION_SPEED = 4 +const RING_REPEAT_PERIOD = 0 + +export const Globe = memo(function Globe(): React.ReactElement { + const wrapRef = useRef(null) + const globeRef = useRef(undefined) + const [size, setSize] = useState({ w: 0, h: 0 }) + + const points = useGlobePoints() + const rings = useGlobeRings() + const focusEvent = useGlobeEvents((s) => s.focusEvent) + + useEffect(() => { + const wrap = wrapRef.current + if (!wrap) return + const ro = new ResizeObserver((entries) => { + const entry = entries[0] + if (!entry) return + setSize({ + w: entry.contentRect.width, + h: entry.contentRect.height, + }) + }) + ro.observe(wrap) + return () => ro.disconnect() + }, []) + + useEffect(() => { + if (size.w === 0) return + const controls = globeRef.current?.controls() + if (!controls) return + controls.autoRotate = false + }, [size.w]) + + useEffect(() => { + if (!focusEvent) return + panTo(globeRef, focusEvent.lat, focusEvent.lng) + }, [focusEvent]) + + return ( +
+ {size.w > 0 && size.h > 0 && ( + RING_COLOR} + ringMaxRadius={RING_MAX_RADIUS} + ringPropagationSpeed={RING_PROPAGATION_SPEED} + ringRepeatPeriod={RING_REPEAT_PERIOD} + atmosphereColor={ATMOSPHERE_COLOR} + atmosphereAltitude={ATMOSPHERE_ALTITUDE} + backgroundColor="rgba(0,0,0,0)" + /> + )} +
+ ) +}) + +Globe.displayName = 'Globe' diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/globeCamera.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/globeCamera.ts new file mode 100644 index 00000000..1c5bb43f --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/globeCamera.ts @@ -0,0 +1,20 @@ +// ©AngelaMos | 2026 +// globeCamera.ts + +import type { MutableRefObject } from 'react' +import type { GlobeMethods } from 'react-globe.gl' + +const DEFAULT_PAN_DURATION_MS = 1200 +const DEFAULT_FOCUS_ALTITUDE = 1.8 + +export type GlobeRef = MutableRefObject + +export function panTo( + ref: GlobeRef, + lat: number, + lng: number, + durationMs: number = DEFAULT_PAN_DURATION_MS, + altitude: number = DEFAULT_FOCUS_ALTITUDE +): void { + ref.current?.pointOfView({ lat, lng, altitude }, durationMs) +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/globeLayers.ts b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/globeLayers.ts new file mode 100644 index 00000000..42241877 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/globe/globeLayers.ts @@ -0,0 +1,78 @@ +// ©AngelaMos | 2026 +// globeLayers.ts + +import { useMemo } from 'react' +import { type GlobePointType, useGlobeEvents } from '@/stores/globeEvents' + +const COLOR_BY_TYPE: Record = { + earthquake: '#a3a3a3', + ransomware: '#e5e5e5', + scan: '#404040', + iss: '#4ade80', + outage: '#f59e0b', + hijack: '#f59e0b', +} + +const ALTITUDE_BY_TYPE: Record = { + earthquake: 0.01, + ransomware: 0.01, + scan: 0.005, + iss: 0.06, + outage: 0.005, + hijack: 0.005, +} + +const RADIUS_BY_TYPE: Record = { + earthquake: 0.4, + ransomware: 0.3, + scan: 0.15, + iss: 0.5, + outage: 0.4, + hijack: 0.4, +} + +export interface GlobePointDatum { + id: string + type: GlobePointType + lat: number + lng: number + color: string + altitude: number + radius: number +} + +export interface GlobeRingDatum { + id: string + lat: number + lng: number +} + +export function useGlobePoints(): GlobePointDatum[] { + const points = useGlobeEvents((s) => s.points) + return useMemo( + () => + points.map((p) => ({ + id: p.id, + type: p.type, + lat: p.lat, + lng: p.lng, + color: COLOR_BY_TYPE[p.type], + altitude: ALTITUDE_BY_TYPE[p.type], + radius: RADIUS_BY_TYPE[p.type], + })), + [points] + ) +} + +export function useGlobeRings(): GlobeRingDatum[] { + const rings = useGlobeEvents((s) => s.rings) + return useMemo( + () => + rings.map((r) => ({ + id: r.id, + lat: r.lat, + lng: r.lng, + })), + [rings] + ) +}