cesiumjs-anywidget 0.6.0__tar.gz → 0.8.0__tar.gz

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.
Files changed (23) hide show
  1. cesiumjs_anywidget-0.8.0/.nblink/environment.yml +14 -0
  2. {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/PKG-INFO +35 -11
  3. {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/README.md +30 -0
  4. cesiumjs_anywidget-0.8.0/jest.config.js +18 -0
  5. cesiumjs_anywidget-0.8.0/package.json +21 -0
  6. {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/pyproject.toml +20 -22
  7. cesiumjs_anywidget-0.8.0/src/cesiumjs_anywidget/__init__.py +23 -0
  8. cesiumjs_anywidget-0.8.0/src/cesiumjs_anywidget/geoid.py +298 -0
  9. {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/src/cesiumjs_anywidget/index.js +1415 -490
  10. cesiumjs_anywidget-0.8.0/src/cesiumjs_anywidget/logger.py +73 -0
  11. {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/src/cesiumjs_anywidget/styles.css +24 -1
  12. {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/src/cesiumjs_anywidget/widget.py +552 -42
  13. cesiumjs_anywidget-0.8.0/uv.lock +2118 -0
  14. cesiumjs_anywidget-0.6.0/DEVELOPMENT.md +0 -141
  15. cesiumjs_anywidget-0.6.0/package.json +0 -15
  16. cesiumjs_anywidget-0.6.0/src/cesiumjs_anywidget/__init__.py +0 -6
  17. cesiumjs_anywidget-0.6.0/uv.lock +0 -3984
  18. {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/.gitignore +0 -0
  19. {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/LICENSE +0 -0
  20. {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/Makefile +0 -0
  21. {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/TROUBLESHOOTING.md +0 -0
  22. {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/build.js +0 -0
  23. {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/fix_tests.py +0 -0
@@ -0,0 +1,14 @@
1
+ name: cesiumjs-anywidget-examples
2
+ channels:
3
+ - emscripten-forge
4
+ - conda-forge
5
+ dependencies:
6
+ - xeus-python # A Jupyter kernel for Python
7
+ - pyarrow
8
+ - sidecar
9
+ - numpy
10
+ - scipy
11
+ - pip: # List pure Python packages from PyPi here
12
+ - cesiumjs-anywidget
13
+ - huggingface_hub
14
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cesiumjs-anywidget
3
- Version: 0.6.0
3
+ Version: 0.8.0
4
4
  Summary: A Jupyter widget for CesiumJS 3D globe visualization using anywidget
5
5
  Project-URL: Homepage, https://github.com/Alex-PLACET/cesiumjs_anywidget
6
6
  Project-URL: Repository, https://github.com/Alex-PLACET/cesiumjs_anywidget
@@ -213,19 +213,13 @@ Classifier: Intended Audience :: Developers
213
213
  Classifier: Intended Audience :: Science/Research
214
214
  Classifier: License :: OSI Approved :: Apache Software License
215
215
  Classifier: Programming Language :: Python :: 3
216
- Classifier: Programming Language :: Python :: 3.8
217
- Classifier: Programming Language :: Python :: 3.9
218
- Classifier: Programming Language :: Python :: 3.10
219
- Classifier: Programming Language :: Python :: 3.11
220
216
  Classifier: Programming Language :: Python :: 3.12
221
- Requires-Python: >=3.8
217
+ Requires-Python: >=3.12
222
218
  Requires-Dist: anywidget>=0.9.0
219
+ Requires-Dist: numpy>=2.4.0
220
+ Requires-Dist: pygeodesy>=25.11.5
221
+ Requires-Dist: scipy>=1.17.0
223
222
  Requires-Dist: traitlets>=5.0.0
224
- Provides-Extra: dev
225
- Requires-Dist: jupyterlab>=4.0.0; extra == 'dev'
226
- Requires-Dist: notebook>=7.0.0; extra == 'dev'
227
- Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
228
- Requires-Dist: pytest>=7.0.0; extra == 'dev'
229
223
  Description-Content-Type: text/markdown
230
224
 
231
225
  # CesiumJS Anywidget
@@ -239,6 +233,7 @@ A Jupyter widget for interactive 3D globe visualization using [CesiumJS](https:/
239
233
  - 🔄 **Bidirectional Sync**: Camera state syncs between Python and JavaScript
240
234
  - 🗺️ **GeoJSON Support**: Load and visualize GeoJSON data
241
235
  - 🏔️ **Terrain & Imagery**: World terrain and satellite imagery
236
+ - 🏙️ **Photorealistic 3D Tiles**: Google's photorealistic global 3D cities and landscapes
242
237
  - 📏 **Measurement Tools**: Built-in distance, multi-point, and height measurement tools
243
238
  - ⚙️ **Highly Configurable**: Customize viewer options and UI elements
244
239
 
@@ -393,6 +388,35 @@ widget = CesiumWidget(
393
388
  )
394
389
  ```
395
390
 
391
+ ### Google Photorealistic 3D Tiles
392
+
393
+ Visualize cities and landscapes in stunning photorealistic detail using Google's Photorealistic 3D Tiles:
394
+
395
+ ```python
396
+ # Enable photorealistic tiles (recommended method)
397
+ widget = CesiumWidget()
398
+ widget.enable_photorealistic_3d_tiles(True)
399
+
400
+ # Fly to a city to see the tiles
401
+ widget.fly_to(latitude=40.7128, longitude=-74.0060, altitude=2000, pitch=-45)
402
+
403
+ # Or configure manually during creation
404
+ widget = CesiumWidget(
405
+ enable_photorealistic_tiles=True,
406
+ show_globe=False, # Disable base globe (recommended)
407
+ enable_terrain=False, # Disable terrain (tiles include terrain)
408
+ height="700px"
409
+ )
410
+ ```
411
+
412
+ **Features:**
413
+ - Global coverage of major cities and populated areas
414
+ - Photorealistic imagery with 3D buildings and structures
415
+ - Automatic terrain integration
416
+ - Works with all camera controls and measurement tools
417
+
418
+ See [PHOTOREALISTIC_TILES.md](PHOTOREALISTIC_TILES.md) for detailed documentation and [examples/photorealistic_tiles_demo.ipynb](examples/photorealistic_tiles_demo.ipynb) for interactive examples.
419
+
396
420
  ## Development
397
421
 
398
422
  Enable hot module replacement for live updates during development:
@@ -9,6 +9,7 @@ A Jupyter widget for interactive 3D globe visualization using [CesiumJS](https:/
9
9
  - 🔄 **Bidirectional Sync**: Camera state syncs between Python and JavaScript
10
10
  - 🗺️ **GeoJSON Support**: Load and visualize GeoJSON data
11
11
  - 🏔️ **Terrain & Imagery**: World terrain and satellite imagery
12
+ - 🏙️ **Photorealistic 3D Tiles**: Google's photorealistic global 3D cities and landscapes
12
13
  - 📏 **Measurement Tools**: Built-in distance, multi-point, and height measurement tools
13
14
  - ⚙️ **Highly Configurable**: Customize viewer options and UI elements
14
15
 
@@ -163,6 +164,35 @@ widget = CesiumWidget(
163
164
  )
164
165
  ```
165
166
 
167
+ ### Google Photorealistic 3D Tiles
168
+
169
+ Visualize cities and landscapes in stunning photorealistic detail using Google's Photorealistic 3D Tiles:
170
+
171
+ ```python
172
+ # Enable photorealistic tiles (recommended method)
173
+ widget = CesiumWidget()
174
+ widget.enable_photorealistic_3d_tiles(True)
175
+
176
+ # Fly to a city to see the tiles
177
+ widget.fly_to(latitude=40.7128, longitude=-74.0060, altitude=2000, pitch=-45)
178
+
179
+ # Or configure manually during creation
180
+ widget = CesiumWidget(
181
+ enable_photorealistic_tiles=True,
182
+ show_globe=False, # Disable base globe (recommended)
183
+ enable_terrain=False, # Disable terrain (tiles include terrain)
184
+ height="700px"
185
+ )
186
+ ```
187
+
188
+ **Features:**
189
+ - Global coverage of major cities and populated areas
190
+ - Photorealistic imagery with 3D buildings and structures
191
+ - Automatic terrain integration
192
+ - Works with all camera controls and measurement tools
193
+
194
+ See [PHOTOREALISTIC_TILES.md](PHOTOREALISTIC_TILES.md) for detailed documentation and [examples/photorealistic_tiles_demo.ipynb](examples/photorealistic_tiles_demo.ipynb) for interactive examples.
195
+
166
196
  ## Development
167
197
 
168
198
  Enable hot module replacement for live updates during development:
@@ -0,0 +1,18 @@
1
+ export default {
2
+ testEnvironment: 'jsdom',
3
+ testMatch: ['**/tests/js/**/*.test.js'],
4
+ transform: {},
5
+ moduleNameMapper: {
6
+ '\\.(css|less|scss|sass)$': '<rootDir>/tests/js/__mocks__/styleMock.js',
7
+ },
8
+ setupFilesAfterEnv: ['<rootDir>/tests/js/setup.js'],
9
+ collectCoverageFrom: [
10
+ 'src/cesiumjs_anywidget/js/**/*.js',
11
+ '!src/cesiumjs_anywidget/js/index.js', // Skip main entry point for now
12
+ ],
13
+ coverageDirectory: 'coverage',
14
+ coverageReporters: ['text', 'lcov', 'html'],
15
+ verbose: true,
16
+ testPathIgnorePatterns: ['/node_modules/', '/.venv/', '/htmlcov/'],
17
+ modulePathIgnorePatterns: ['<rootDir>/.venv/', '<rootDir>/htmlcov/'],
18
+ };
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "cesiumjs-anywidget",
3
+ "version": "0.1.0",
4
+ "description": "A Jupyter widget for CesiumJS 3D globe visualization",
5
+ "private": true,
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "node build.js",
9
+ "watch": "node build.js --watch",
10
+ "dev": "node build.js --watch",
11
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
12
+ "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
13
+ "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage"
14
+ },
15
+ "devDependencies": {
16
+ "@jest/globals": "^29.7.0",
17
+ "esbuild": "^0.27.1",
18
+ "jest": "^29.7.0",
19
+ "jest-environment-jsdom": "^29.7.0"
20
+ }
21
+ }
@@ -4,14 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cesiumjs-anywidget"
7
- version = "0.6.0"
7
+ version = "0.8.0"
8
8
  description = "A Jupyter widget for CesiumJS 3D globe visualization using anywidget"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
11
- authors = [
12
- { name = "Alexis Placet" }
13
- ]
14
- requires-python = ">=3.8"
11
+ authors = [{ name = "Alexis Placet" }]
12
+ requires-python = ">=3.12"
15
13
  classifiers = [
16
14
  "Development Status :: 3 - Alpha",
17
15
  "Framework :: Jupyter",
@@ -19,23 +17,28 @@ classifiers = [
19
17
  "Intended Audience :: Science/Research",
20
18
  "License :: OSI Approved :: Apache Software License",
21
19
  "Programming Language :: Python :: 3",
22
- "Programming Language :: Python :: 3.8",
23
- "Programming Language :: Python :: 3.9",
24
- "Programming Language :: Python :: 3.10",
25
- "Programming Language :: Python :: 3.11",
26
20
  "Programming Language :: Python :: 3.12",
27
21
  ]
28
22
  dependencies = [
29
23
  "anywidget>=0.9.0",
30
24
  "traitlets>=5.0.0",
25
+ "pygeodesy>=25.11.5",
26
+ "numpy>=2.4.0",
27
+ "scipy>=1.17.0",
31
28
  ]
32
29
 
33
- [project.optional-dependencies]
34
- dev = [
35
- "jupyterlab>=4.0.0",
36
- "notebook>=7.0.0",
37
- "pytest>=7.0.0",
38
- "pytest-cov>=4.0.0",
30
+ [dependency-groups]
31
+ dev = ["pytest-cov>=4.0.0", "pytest>=7.0.0"]
32
+ notebook_examples = [
33
+ "exifread>=3.5.1",
34
+ "huggingface_hub>=1.3.4",
35
+ "ipycanvas>=0.14.3",
36
+ "jupyterlab>=4.3.8",
37
+ "opencv-python>=4.11.0.86",
38
+ "piexif>=1.1.3",
39
+ "pyarrow>=23.0.0",
40
+
41
+ "sidecar>=0.8.0",
39
42
  ]
40
43
 
41
44
  [project.urls]
@@ -44,9 +47,7 @@ Repository = "https://github.com/Alex-PLACET/cesiumjs_anywidget"
44
47
 
45
48
  [tool.hatch.build.targets.wheel]
46
49
  packages = ["src/cesiumjs_anywidget"]
47
- exclude = [
48
- "src/cesiumjs_anywidget/js",
49
- ]
50
+ exclude = ["src/cesiumjs_anywidget/js"]
50
51
  force-include = { "src/cesiumjs_anywidget/index.js" = "cesiumjs_anywidget/index.js" }
51
52
 
52
53
  [tool.hatch.build.targets.sdist]
@@ -87,10 +88,7 @@ markers = [
87
88
 
88
89
  [tool.coverage.run]
89
90
  source = ["src/cesiumjs_anywidget"]
90
- omit = [
91
- "*/tests/*",
92
- "*/__pycache__/*",
93
- ]
91
+ omit = ["*/tests/*", "*/__pycache__/*"]
94
92
 
95
93
  [tool.coverage.report]
96
94
  exclude_lines = [
@@ -0,0 +1,23 @@
1
+ """CesiumJS Anywidget - A Jupyter widget for CesiumJS 3D globe visualization."""
2
+
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
12
+
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()