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.
- cesiumjs_anywidget-0.8.0/.nblink/environment.yml +14 -0
- {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/PKG-INFO +35 -11
- {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/README.md +30 -0
- cesiumjs_anywidget-0.8.0/jest.config.js +18 -0
- cesiumjs_anywidget-0.8.0/package.json +21 -0
- {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/pyproject.toml +20 -22
- cesiumjs_anywidget-0.8.0/src/cesiumjs_anywidget/__init__.py +23 -0
- cesiumjs_anywidget-0.8.0/src/cesiumjs_anywidget/geoid.py +298 -0
- {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/src/cesiumjs_anywidget/index.js +1415 -490
- cesiumjs_anywidget-0.8.0/src/cesiumjs_anywidget/logger.py +73 -0
- {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/src/cesiumjs_anywidget/styles.css +24 -1
- {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/src/cesiumjs_anywidget/widget.py +552 -42
- cesiumjs_anywidget-0.8.0/uv.lock +2118 -0
- cesiumjs_anywidget-0.6.0/DEVELOPMENT.md +0 -141
- cesiumjs_anywidget-0.6.0/package.json +0 -15
- cesiumjs_anywidget-0.6.0/src/cesiumjs_anywidget/__init__.py +0 -6
- cesiumjs_anywidget-0.6.0/uv.lock +0 -3984
- {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/.gitignore +0 -0
- {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/LICENSE +0 -0
- {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/Makefile +0 -0
- {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/TROUBLESHOOTING.md +0 -0
- {cesiumjs_anywidget-0.6.0 → cesiumjs_anywidget-0.8.0}/build.js +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
[
|
|
34
|
-
dev = [
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
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()
|