cesiumjs-anywidget 0.6.0__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cesiumjs_anywidget/__init__.py +19 -2
- cesiumjs_anywidget/geoid.py +298 -0
- cesiumjs_anywidget/index.js +1415 -490
- cesiumjs_anywidget/logger.py +73 -0
- cesiumjs_anywidget/styles.css +24 -1
- cesiumjs_anywidget/widget.py +552 -42
- {cesiumjs_anywidget-0.6.0.dist-info → cesiumjs_anywidget-0.8.0.dist-info}/METADATA +35 -11
- cesiumjs_anywidget-0.8.0.dist-info/RECORD +10 -0
- cesiumjs_anywidget-0.6.0.dist-info/RECORD +0 -8
- {cesiumjs_anywidget-0.6.0.dist-info → cesiumjs_anywidget-0.8.0.dist-info}/WHEEL +0 -0
- {cesiumjs_anywidget-0.6.0.dist-info → cesiumjs_anywidget-0.8.0.dist-info}/licenses/LICENSE +0 -0
cesiumjs_anywidget/__init__.py
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
"""CesiumJS Anywidget - A Jupyter widget for CesiumJS 3D globe visualization."""
|
|
2
2
|
|
|
3
3
|
from .widget import CesiumWidget
|
|
4
|
+
from .geoid import (
|
|
5
|
+
get_geoid_undulation,
|
|
6
|
+
msl_to_wgs84,
|
|
7
|
+
wgs84_to_msl,
|
|
8
|
+
clear_geoid_cache,
|
|
9
|
+
set_geoid_data_url,
|
|
10
|
+
)
|
|
11
|
+
from .logger import get_logger, set_log_level
|
|
4
12
|
|
|
5
|
-
__version__ = "0.
|
|
6
|
-
__all__ = [
|
|
13
|
+
__version__ = "0.6.0"
|
|
14
|
+
__all__ = [
|
|
15
|
+
"CesiumWidget",
|
|
16
|
+
"get_geoid_undulation",
|
|
17
|
+
"msl_to_wgs84",
|
|
18
|
+
"wgs84_to_msl",
|
|
19
|
+
"clear_geoid_cache",
|
|
20
|
+
"set_geoid_data_url",
|
|
21
|
+
"get_logger",
|
|
22
|
+
"set_log_level",
|
|
23
|
+
]
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Geoid conversion utilities for MSL to WGS84 altitude conversion.
|
|
2
|
+
|
|
3
|
+
This module provides functions to convert between Mean Sea Level (MSL) altitudes
|
|
4
|
+
and WGS84 ellipsoid heights using the EGM96 geoid model.
|
|
5
|
+
|
|
6
|
+
The EGM96 15-minute grid data is automatically downloaded from the internet on
|
|
7
|
+
first use and cached locally. You can customize the data source URL if needed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import tarfile
|
|
11
|
+
import urllib.request
|
|
12
|
+
import zipfile
|
|
13
|
+
from functools import lru_cache
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from .logger import get_logger
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Default URL for EGM96 15-minute grid data
|
|
23
|
+
# This is the official NGA download link
|
|
24
|
+
DEFAULT_EGM96_URL = "https://earth-info.nga.mil/php/download.php?file=egm-96interpolation"
|
|
25
|
+
|
|
26
|
+
# User-configurable URL (can be changed before first use)
|
|
27
|
+
_EGM96_DATA_URL: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
# Cached geoid model
|
|
30
|
+
_geoid_model = None
|
|
31
|
+
|
|
32
|
+
# Cache directory for downloaded geoid files
|
|
33
|
+
_CACHE_DIR = Path.home() / ".cache" / "cesiumjs_anywidget" / "geoid"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def set_geoid_data_url(url: str) -> None:
|
|
37
|
+
"""Set a custom URL for downloading EGM96 geoid data.
|
|
38
|
+
|
|
39
|
+
This must be called before the first use of geoid functions.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
url : str
|
|
44
|
+
URL to download EGM96 grid data (tar.bz2 or direct grid file)
|
|
45
|
+
|
|
46
|
+
Examples
|
|
47
|
+
--------
|
|
48
|
+
>>> from cesiumjs_anywidget.geoid import set_geoid_data_url
|
|
49
|
+
>>> set_geoid_data_url("https://example.com/egm96-15.tar.bz2")
|
|
50
|
+
"""
|
|
51
|
+
global _EGM96_DATA_URL, _geoid_model
|
|
52
|
+
_EGM96_DATA_URL = url
|
|
53
|
+
# Clear cached model to force reload with new URL
|
|
54
|
+
_geoid_model = None
|
|
55
|
+
get_geoid_undulation.cache_clear()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _download_egm96_grid(url: str, cache_dir: Path) -> Path:
|
|
59
|
+
"""Download and extract the EGM96 grid file from an archive (zip or tar.bz2).
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
url : str
|
|
64
|
+
URL to download the archive from
|
|
65
|
+
cache_dir : Path
|
|
66
|
+
Directory to store the extracted grid file
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
Path
|
|
71
|
+
Path to the extracted .GRD file
|
|
72
|
+
|
|
73
|
+
Raises
|
|
74
|
+
------
|
|
75
|
+
FileNotFoundError
|
|
76
|
+
If no .GRD file found in archive
|
|
77
|
+
Exception
|
|
78
|
+
If download or extraction fails
|
|
79
|
+
"""
|
|
80
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
|
|
82
|
+
# Expected output file (WW15MGH.GRD is the standard EGM96 15-minute grid)
|
|
83
|
+
grd_file = cache_dir / "WW15MGH.GRD"
|
|
84
|
+
|
|
85
|
+
if grd_file.exists():
|
|
86
|
+
return grd_file
|
|
87
|
+
|
|
88
|
+
# Download the archive
|
|
89
|
+
archive_path = cache_dir / "egm96-data.archive"
|
|
90
|
+
|
|
91
|
+
if not archive_path.exists():
|
|
92
|
+
logger.info("Downloading EGM96 grid data from %s...", url)
|
|
93
|
+
urllib.request.urlretrieve(url, archive_path)
|
|
94
|
+
logger.info("Download complete: %s", archive_path)
|
|
95
|
+
|
|
96
|
+
# Detect archive type and extract
|
|
97
|
+
logger.info("Extracting %s...", archive_path)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
# Try ZIP first
|
|
101
|
+
with zipfile.ZipFile(archive_path, 'r') as zip_ref:
|
|
102
|
+
# Find the .GRD file in the archive
|
|
103
|
+
grd_members = [name for name in zip_ref.namelist()
|
|
104
|
+
if name.endswith('.GRD') or name.endswith('.grd')]
|
|
105
|
+
|
|
106
|
+
if not grd_members:
|
|
107
|
+
raise FileNotFoundError(f"No .GRD file found in ZIP archive {archive_path}")
|
|
108
|
+
|
|
109
|
+
# Extract the first .GRD file found
|
|
110
|
+
member_name = grd_members[0]
|
|
111
|
+
zip_ref.extract(member_name, cache_dir)
|
|
112
|
+
|
|
113
|
+
# Rename to standard name if needed
|
|
114
|
+
extracted_path = cache_dir / member_name
|
|
115
|
+
if extracted_path.name != grd_file.name:
|
|
116
|
+
extracted_path.rename(grd_file)
|
|
117
|
+
|
|
118
|
+
except zipfile.BadZipFile:
|
|
119
|
+
# Try tar.bz2
|
|
120
|
+
with tarfile.open(archive_path, "r:bz2") as tar:
|
|
121
|
+
# Find the .GRD file in the archive
|
|
122
|
+
grd_members = [m for m in tar.getmembers()
|
|
123
|
+
if m.name.endswith('.GRD') or m.name.endswith('.grd')]
|
|
124
|
+
|
|
125
|
+
if not grd_members:
|
|
126
|
+
raise FileNotFoundError(f"No .GRD file found in tar.bz2 archive {archive_path}")
|
|
127
|
+
|
|
128
|
+
# Extract the first .GRD file found
|
|
129
|
+
member = grd_members[0]
|
|
130
|
+
tar.extract(member, cache_dir)
|
|
131
|
+
|
|
132
|
+
# Rename to standard name if needed
|
|
133
|
+
extracted_path = cache_dir / member.name
|
|
134
|
+
if extracted_path.name != grd_file.name:
|
|
135
|
+
extracted_path.rename(grd_file)
|
|
136
|
+
|
|
137
|
+
logger.info("Extraction complete: %s", grd_file)
|
|
138
|
+
|
|
139
|
+
# Clean up archive if extraction successful
|
|
140
|
+
if archive_path.exists() and grd_file.exists():
|
|
141
|
+
archive_path.unlink()
|
|
142
|
+
|
|
143
|
+
return grd_file
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _get_geoid_model():
|
|
147
|
+
"""Get or create the cached EGM96 geoid model.
|
|
148
|
+
|
|
149
|
+
The model is loaded from GeoidEGM96 using a .GRD grid file.
|
|
150
|
+
The grid file is automatically downloaded on first use and cached locally.
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
GeoidEGM96
|
|
155
|
+
The cached geoid model instance
|
|
156
|
+
"""
|
|
157
|
+
global _geoid_model
|
|
158
|
+
|
|
159
|
+
if _geoid_model is None:
|
|
160
|
+
from pygeodesy.geoids import GeoidEGM96
|
|
161
|
+
from pygeodesy.datums import Datums
|
|
162
|
+
|
|
163
|
+
# Use custom URL if set, otherwise use default
|
|
164
|
+
url = _EGM96_DATA_URL if _EGM96_DATA_URL else DEFAULT_EGM96_URL
|
|
165
|
+
|
|
166
|
+
# Download and extract the grid file if needed
|
|
167
|
+
grd_file = _download_egm96_grid(url, _CACHE_DIR)
|
|
168
|
+
|
|
169
|
+
# Create the GeoidEGM96 model with the grid file path
|
|
170
|
+
_geoid_model = GeoidEGM96(str(grd_file), datum=Datums.WGS84)
|
|
171
|
+
|
|
172
|
+
return _geoid_model
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@lru_cache(maxsize=1000)
|
|
176
|
+
def get_geoid_undulation(latitude: float, longitude: float) -> float:
|
|
177
|
+
"""Calculate the geoid undulation at a given location using EGM96.
|
|
178
|
+
|
|
179
|
+
The geoid undulation (also called geoid height or geoid separation) is the
|
|
180
|
+
height of the geoid above the WGS84 reference ellipsoid. This value is
|
|
181
|
+
needed to convert between GPS/MSL altitudes and WGS84 ellipsoid heights.
|
|
182
|
+
|
|
183
|
+
On first use, the EGM96 15-minute grid data will be automatically downloaded
|
|
184
|
+
and cached locally. Subsequent calls will use the cached data.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
latitude : float
|
|
189
|
+
Latitude in degrees (-90 to 90)
|
|
190
|
+
longitude : float
|
|
191
|
+
Longitude in degrees (-180 to 180 or 0 to 360)
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
float
|
|
196
|
+
Geoid undulation in meters. Positive values indicate the geoid is
|
|
197
|
+
above the WGS84 ellipsoid at this location.
|
|
198
|
+
|
|
199
|
+
Examples
|
|
200
|
+
--------
|
|
201
|
+
>>> # France (46°N, 4°E) - geoid is about 47m above ellipsoid
|
|
202
|
+
>>> undulation = get_geoid_undulation(46.0, 4.0)
|
|
203
|
+
>>> 45 < undulation < 50
|
|
204
|
+
True
|
|
205
|
+
|
|
206
|
+
Notes
|
|
207
|
+
-----
|
|
208
|
+
The EGM96 model provides approximately ±0.5 to ±1 meter accuracy globally.
|
|
209
|
+
The grid data is downloaded automatically from GeographicLib on first use.
|
|
210
|
+
"""
|
|
211
|
+
geoid = _get_geoid_model()
|
|
212
|
+
|
|
213
|
+
# GeoidEGM96.height() accepts lat, lon directly
|
|
214
|
+
return geoid.height(latitude, longitude)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def msl_to_wgs84(altitude_msl: float, latitude: float, longitude: float) -> float:
|
|
219
|
+
"""Convert Mean Sea Level (MSL) altitude to WGS84 ellipsoid height.
|
|
220
|
+
|
|
221
|
+
GPS receivers typically report altitude above MSL (geoid), but CZML and
|
|
222
|
+
Cesium use WGS84 ellipsoid heights. This function performs the conversion:
|
|
223
|
+
|
|
224
|
+
WGS84_height = altitude_msl + geoid_undulation
|
|
225
|
+
|
|
226
|
+
Parameters
|
|
227
|
+
----------
|
|
228
|
+
altitude_msl : float
|
|
229
|
+
Altitude above Mean Sea Level in meters
|
|
230
|
+
latitude : float
|
|
231
|
+
Latitude in degrees (-90 to 90)
|
|
232
|
+
longitude : float
|
|
233
|
+
Longitude in degrees (-180 to 180 or 0 to 360)
|
|
234
|
+
|
|
235
|
+
Returns
|
|
236
|
+
-------
|
|
237
|
+
float
|
|
238
|
+
Height above WGS84 ellipsoid in meters
|
|
239
|
+
|
|
240
|
+
Examples
|
|
241
|
+
--------
|
|
242
|
+
>>> # Sea level in France would be ~47m above WGS84 ellipsoid
|
|
243
|
+
>>> wgs84_height = msl_to_wgs84(0, 46.0, 4.0)
|
|
244
|
+
>>> 45 < wgs84_height < 50
|
|
245
|
+
True
|
|
246
|
+
|
|
247
|
+
>>> # Mountain at 1000m MSL
|
|
248
|
+
>>> wgs84_height = msl_to_wgs84(1000, 46.0, 4.0)
|
|
249
|
+
>>> 1045 < wgs84_height < 1050
|
|
250
|
+
True
|
|
251
|
+
"""
|
|
252
|
+
geoid_undulation = get_geoid_undulation(latitude, longitude)
|
|
253
|
+
return altitude_msl + geoid_undulation
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def wgs84_to_msl(altitude_wgs84: float, latitude: float, longitude: float) -> float:
|
|
257
|
+
"""Convert WGS84 ellipsoid height to Mean Sea Level (MSL) altitude.
|
|
258
|
+
|
|
259
|
+
This is the inverse of msl_to_wgs84. Useful when you have WGS84 heights
|
|
260
|
+
(e.g., from Cesium) and need to display MSL altitudes to users.
|
|
261
|
+
|
|
262
|
+
altitude_msl = WGS84_height - geoid_undulation
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
altitude_wgs84 : float
|
|
267
|
+
Height above WGS84 ellipsoid in meters
|
|
268
|
+
latitude : float
|
|
269
|
+
Latitude in degrees (-90 to 90)
|
|
270
|
+
longitude : float
|
|
271
|
+
Longitude in degrees (-180 to 180 or 0 to 360)
|
|
272
|
+
|
|
273
|
+
Returns
|
|
274
|
+
-------
|
|
275
|
+
float
|
|
276
|
+
Altitude above Mean Sea Level in meters
|
|
277
|
+
|
|
278
|
+
Examples
|
|
279
|
+
--------
|
|
280
|
+
>>> # WGS84 height of ~47m in France is approximately sea level
|
|
281
|
+
>>> msl_alt = wgs84_to_msl(47, 46.0, 4.0)
|
|
282
|
+
>>> -3 < msl_alt < 3
|
|
283
|
+
True
|
|
284
|
+
"""
|
|
285
|
+
geoid_undulation = get_geoid_undulation(latitude, longitude)
|
|
286
|
+
return altitude_wgs84 - geoid_undulation
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def clear_geoid_cache() -> None:
|
|
290
|
+
"""Clear the cached geoid model and undulation values.
|
|
291
|
+
|
|
292
|
+
This function clears both the geoid model cache and the LRU cache
|
|
293
|
+
used by get_geoid_undulation. Useful for testing or to force reloading
|
|
294
|
+
the geoid data.
|
|
295
|
+
"""
|
|
296
|
+
global _geoid_model
|
|
297
|
+
_geoid_model = None
|
|
298
|
+
get_geoid_undulation.cache_clear()
|