gri-plot 0.2.3__tar.gz → 0.2.5__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.
- {gri_plot-0.2.3 → gri_plot-0.2.5}/CONTRIBUTING.md +8 -4
- {gri_plot-0.2.3 → gri_plot-0.2.5}/PKG-INFO +7 -17
- {gri_plot-0.2.3 → gri_plot-0.2.5}/README.md +5 -15
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/__init__.py +10 -26
- gri_plot-0.2.5/gri_plot/_heatmap_image.py +132 -0
- gri_plot-0.2.5/gri_plot/basemap.py +307 -0
- gri_plot-0.2.5/gri_plot/data/natural_earth_boundaries.npz +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/figure3d.py +5 -85
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/figure_map.py +188 -17
- gri_plot-0.2.5/gri_plot/observables/__init__.py +15 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/observables/los.py +1 -1
- gri_plot-0.2.5/gri_plot/polylines.py +60 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/scatter_map.py +44 -22
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/surfaces/__init__.py +3 -12
- gri_plot-0.2.5/gri_plot/surfaces/mesh.py +133 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/pyproject.toml +35 -7
- gri_plot-0.2.5/scripts/build_basemap_data.py +241 -0
- gri_plot-0.2.5/test/test_basemap.py +169 -0
- gri_plot-0.2.5/test/test_figure3d.py +79 -0
- gri_plot-0.2.5/test/test_frames.py +136 -0
- gri_plot-0.2.5/test/test_heatmap_image.py +137 -0
- gri_plot-0.2.5/test/test_polylines.py +52 -0
- gri_plot-0.2.5/test/test_surfaces/test_mesh.py +51 -0
- gri_plot-0.2.3/.coveragerc +0 -8
- gri_plot-0.2.3/.docs_other_projects.md +0 -85
- gri_plot-0.2.3/.gitlab-ci-deps.yml +0 -7
- gri_plot-0.2.3/.gitlab-ci.yml +0 -95
- gri_plot-0.2.3/.init_venv.sh +0 -87
- gri_plot-0.2.3/.pre-commit-config.yaml +0 -40
- gri_plot-0.2.3/.pre-commit-unskipped.py +0 -38
- gri_plot-0.2.3/.python-version +0 -1
- gri_plot-0.2.3/.ruff.toml +0 -60
- gri_plot-0.2.3/.vscode/settings.json +0 -15
- gri_plot-0.2.3/gri_plot/observables/__init__.py +0 -21
- gri_plot-0.2.3/gri_plot/observables/fdoa.py +0 -256
- gri_plot-0.2.3/gri_plot/observables/tdoa.py +0 -203
- gri_plot-0.2.3/gri_plot/observables/terrain.py +0 -295
- gri_plot-0.2.3/gri_plot/surfaces/gradients/__init__.py +0 -39
- gri_plot-0.2.3/gri_plot/surfaces/gradients/axis.py +0 -76
- gri_plot-0.2.3/gri_plot/surfaces/gradients/line.py +0 -88
- gri_plot-0.2.3/gri_plot/surfaces/gradients/plane.py +0 -55
- gri_plot-0.2.3/gri_plot/surfaces/gradients/point.py +0 -42
- gri_plot-0.2.3/gri_plot/surfaces/intersection.py +0 -219
- gri_plot-0.2.3/gri_plot/surfaces/mesh.py +0 -605
- gri_plot-0.2.3/notes.txt +0 -3
- gri_plot-0.2.3/report.xml +0 -1
- gri_plot-0.2.3/test/test_figure3d.py +0 -287
- gri_plot-0.2.3/test/test_frames.py +0 -44
- gri_plot-0.2.3/test/test_observables/test_fdoa.py +0 -59
- gri_plot-0.2.3/test/test_observables/test_tdoa.py +0 -72
- gri_plot-0.2.3/test/test_observables/test_terrain.py +0 -45
- gri_plot-0.2.3/test/test_surfaces/test_intersection.py +0 -203
- gri_plot-0.2.3/test/test_surfaces/test_mesh.py +0 -239
- gri_plot-0.2.3/uv.lock +0 -765
- {gri_plot-0.2.3 → gri_plot-0.2.5}/.gitignore +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/LICENSE +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/frames.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/observables/aoa.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/observables/range_sphere.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/plot_ellipse.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/py.typed +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/scatter.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/shapes/__init__.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/shapes/cone.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/shapes/cylinder.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/shapes/ellipse.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/shapes/ellipsoid.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/shapes/meshgen.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/shapes/sphere.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/__init__.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_figure_map.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_observables/__init__.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_observables/test_aoa.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_observables/test_los.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_observables/test_range_sphere.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_plot_ellipse.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_plot_scattermap.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_scatter.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_shapes/__init__.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_shapes/test_cone.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_shapes/test_cylinder.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_shapes/test_ellipse.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_shapes/test_ellipsoid.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_shapes/test_sphere.py +0 -0
- {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_surfaces/__init__.py +0 -0
|
@@ -54,12 +54,12 @@ ruff check # lint
|
|
|
54
54
|
ruff format # format
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
###
|
|
57
|
+
### ty
|
|
58
58
|
|
|
59
|
-
[
|
|
59
|
+
[ty](https://docs.astral.sh/ty/) for static type checking:
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
|
|
62
|
+
ty check
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
### Pre-commit
|
|
@@ -110,4 +110,8 @@ by Astral and add to your `settings.json`:
|
|
|
110
110
|
}
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
For type checking in VS Code, install the
|
|
114
|
+
[ty extension](https://marketplace.visualstudio.com/items?itemName=astral-sh.ty)
|
|
115
|
+
by Astral. To avoid conflicts with Pylance, either let ty disable the Python
|
|
116
|
+
language server (the default) or keep Pylance and set
|
|
117
|
+
`"ty.disableLanguageServices": true` to use ty for diagnostics only.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gri-plot
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: 3D geolocation visualization and 2D plotting utilities built on Plotly
|
|
5
5
|
Project-URL: Homepage, https://geosolresearch.com
|
|
6
6
|
Project-URL: Repository, https://gitlab.com/geosol-foss/python/gri-plot
|
|
@@ -19,8 +19,8 @@ Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
|
19
19
|
Requires-Python: >=3.12
|
|
20
20
|
Requires-Dist: gri-utils>=0.3.3
|
|
21
21
|
Requires-Dist: numpy>=2.3.3
|
|
22
|
+
Requires-Dist: pillow>=11.0.0
|
|
22
23
|
Requires-Dist: plotly>=6.3.0
|
|
23
|
-
Requires-Dist: scikit-image>=0.25.0
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
|
|
26
26
|
[](https://geosolresearch.com)
|
|
@@ -31,7 +31,7 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
|
|
32
32
|
## Overview
|
|
33
33
|
|
|
34
|
-
This library renders
|
|
34
|
+
This library renders parametric geometric shapes (ellipsoids, spheres, cones, cylinders) and observable surfaces that inherit from them (AOA cones, line-of-sight rays, range spheres) as interactive 3D meshes. It also provides shorthand functions for 2D scatter plots and geographic maps.
|
|
35
35
|
|
|
36
36
|
## Installation
|
|
37
37
|
|
|
@@ -86,14 +86,12 @@ Scene builder for composing 3D visualizations from multiple surfaces and points:
|
|
|
86
86
|
- **`add_surface()`**: Add any `ImplicitShape` (shapes or observables)
|
|
87
87
|
- **`add_points()`**: Add labeled scatter points
|
|
88
88
|
- **`add_line()`**: Add line segments connecting points
|
|
89
|
-
- **`add_intersection()`**: Highlight where multiple surfaces intersect
|
|
90
89
|
- **`plot_surfaces()`**: Convenience function for quick multi-surface plots
|
|
91
90
|
- Method chaining, automatic color cycling from D3 palette, `plotly_dark` template
|
|
92
91
|
|
|
93
92
|
```python
|
|
94
93
|
from gri_plot import Figure3D
|
|
95
94
|
from gri_plot.shapes import Sphere
|
|
96
|
-
from gri_plot.observables import TdoaSurface
|
|
97
95
|
import numpy as np
|
|
98
96
|
|
|
99
97
|
fig = Figure3D()
|
|
@@ -113,7 +111,7 @@ Coordinate frame handling for 3D display:
|
|
|
113
111
|
|
|
114
112
|
## Shapes
|
|
115
113
|
|
|
116
|
-
Geometric primitives implementing the `ImplicitShape` ABC. Each supports parametric mesh generation (fast, exact geometry) and implicit residual evaluation
|
|
114
|
+
Geometric primitives implementing the `ImplicitShape` ABC. Each supports parametric mesh generation (fast, exact geometry) and implicit residual evaluation.
|
|
117
115
|
|
|
118
116
|
- **Ellipsoid**: From center + covariance matrix, or center + semi-axes + rotation matrix
|
|
119
117
|
- **Sphere**: From center + radius
|
|
@@ -123,31 +121,23 @@ Geometric primitives implementing the `ImplicitShape` ABC. Each supports paramet
|
|
|
123
121
|
|
|
124
122
|
## Observables
|
|
125
123
|
|
|
126
|
-
Geolocation-specific surfaces
|
|
124
|
+
Geolocation-specific surfaces that inherit from parametric shapes:
|
|
127
125
|
|
|
128
|
-
- **TdoaSurface**: TDOA hyperboloid where range_1 - range_2 = c x tdoa, from two collector positions + time difference
|
|
129
|
-
- **FdoaSurface**: FDOA iso-Doppler surface from two collector positions/velocities + carrier frequency + frequency difference
|
|
130
126
|
- **AoaSurface**: AOA cone (extends Cone) from collector position + measured direction + angular error
|
|
131
127
|
- **LosSurface**: Line of sight ray (extends Cylinder) from start point + direction + length; also provides `to_line_trace()` for simple line rendering
|
|
132
128
|
- **RangeSphere**: Range/TOA sphere (extends Sphere) from collector position + range measurement
|
|
133
|
-
- **TerrainSurface**: Terrain from XYZ vertex grid with interpolated residual evaluation; factory methods `from_xyz_grids()` and `from_elevation_grid()`
|
|
134
129
|
|
|
135
130
|
## Surfaces
|
|
136
131
|
|
|
137
|
-
Low-level infrastructure for
|
|
132
|
+
Low-level infrastructure for shape rendering:
|
|
138
133
|
|
|
139
134
|
- **ImplicitShape ABC**: Interface requiring `residual_fn()`, `get_bounds_xyz()`, `to_mesh()`, `to_trace()`, `is_volume`, `label`; also provides `contains()` for point-in-shape testing
|
|
140
|
-
- **Mesh utilities**: `
|
|
141
|
-
- **IntersectionField**: Euclidean distance in residual space for highlighting multi-surface intersections; distinguishes volumes (interior contribution) from surfaces (boundary contribution)
|
|
142
|
-
- **Gradient functions**: Intensity calculators for mesh coloring -- `distance_from_point`, `distance_from_line`, `distance_from_line_segment`, `distance_from_plane`, `distance_from_axis`, `axis_value`
|
|
135
|
+
- **Mesh utilities**: `grid_to_mesh()` (parametric grid triangulation), `vertices_to_mesh3d()` (vertices/faces to Plotly Mesh3d trace)
|
|
143
136
|
|
|
144
137
|
## Dependencies
|
|
145
138
|
|
|
146
139
|
- **plotly**: Interactive 2D and 3D visualization
|
|
147
|
-
- **matplotlib**: Static plot support
|
|
148
140
|
- **numpy**: Array operations
|
|
149
|
-
- **scipy**: Marching cubes mesh extraction
|
|
150
|
-
- **scikit-image**: Additional mesh processing
|
|
151
141
|
- **gri-utils**: Coordinate conversions and constants
|
|
152
142
|
|
|
153
143
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
8
|
|
|
9
|
-
This library renders
|
|
9
|
+
This library renders parametric geometric shapes (ellipsoids, spheres, cones, cylinders) and observable surfaces that inherit from them (AOA cones, line-of-sight rays, range spheres) as interactive 3D meshes. It also provides shorthand functions for 2D scatter plots and geographic maps.
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
@@ -61,14 +61,12 @@ Scene builder for composing 3D visualizations from multiple surfaces and points:
|
|
|
61
61
|
- **`add_surface()`**: Add any `ImplicitShape` (shapes or observables)
|
|
62
62
|
- **`add_points()`**: Add labeled scatter points
|
|
63
63
|
- **`add_line()`**: Add line segments connecting points
|
|
64
|
-
- **`add_intersection()`**: Highlight where multiple surfaces intersect
|
|
65
64
|
- **`plot_surfaces()`**: Convenience function for quick multi-surface plots
|
|
66
65
|
- Method chaining, automatic color cycling from D3 palette, `plotly_dark` template
|
|
67
66
|
|
|
68
67
|
```python
|
|
69
68
|
from gri_plot import Figure3D
|
|
70
69
|
from gri_plot.shapes import Sphere
|
|
71
|
-
from gri_plot.observables import TdoaSurface
|
|
72
70
|
import numpy as np
|
|
73
71
|
|
|
74
72
|
fig = Figure3D()
|
|
@@ -88,7 +86,7 @@ Coordinate frame handling for 3D display:
|
|
|
88
86
|
|
|
89
87
|
## Shapes
|
|
90
88
|
|
|
91
|
-
Geometric primitives implementing the `ImplicitShape` ABC. Each supports parametric mesh generation (fast, exact geometry) and implicit residual evaluation
|
|
89
|
+
Geometric primitives implementing the `ImplicitShape` ABC. Each supports parametric mesh generation (fast, exact geometry) and implicit residual evaluation.
|
|
92
90
|
|
|
93
91
|
- **Ellipsoid**: From center + covariance matrix, or center + semi-axes + rotation matrix
|
|
94
92
|
- **Sphere**: From center + radius
|
|
@@ -98,31 +96,23 @@ Geometric primitives implementing the `ImplicitShape` ABC. Each supports paramet
|
|
|
98
96
|
|
|
99
97
|
## Observables
|
|
100
98
|
|
|
101
|
-
Geolocation-specific surfaces
|
|
99
|
+
Geolocation-specific surfaces that inherit from parametric shapes:
|
|
102
100
|
|
|
103
|
-
- **TdoaSurface**: TDOA hyperboloid where range_1 - range_2 = c x tdoa, from two collector positions + time difference
|
|
104
|
-
- **FdoaSurface**: FDOA iso-Doppler surface from two collector positions/velocities + carrier frequency + frequency difference
|
|
105
101
|
- **AoaSurface**: AOA cone (extends Cone) from collector position + measured direction + angular error
|
|
106
102
|
- **LosSurface**: Line of sight ray (extends Cylinder) from start point + direction + length; also provides `to_line_trace()` for simple line rendering
|
|
107
103
|
- **RangeSphere**: Range/TOA sphere (extends Sphere) from collector position + range measurement
|
|
108
|
-
- **TerrainSurface**: Terrain from XYZ vertex grid with interpolated residual evaluation; factory methods `from_xyz_grids()` and `from_elevation_grid()`
|
|
109
104
|
|
|
110
105
|
## Surfaces
|
|
111
106
|
|
|
112
|
-
Low-level infrastructure for
|
|
107
|
+
Low-level infrastructure for shape rendering:
|
|
113
108
|
|
|
114
109
|
- **ImplicitShape ABC**: Interface requiring `residual_fn()`, `get_bounds_xyz()`, `to_mesh()`, `to_trace()`, `is_volume`, `label`; also provides `contains()` for point-in-shape testing
|
|
115
|
-
- **Mesh utilities**: `
|
|
116
|
-
- **IntersectionField**: Euclidean distance in residual space for highlighting multi-surface intersections; distinguishes volumes (interior contribution) from surfaces (boundary contribution)
|
|
117
|
-
- **Gradient functions**: Intensity calculators for mesh coloring -- `distance_from_point`, `distance_from_line`, `distance_from_line_segment`, `distance_from_plane`, `distance_from_axis`, `axis_value`
|
|
110
|
+
- **Mesh utilities**: `grid_to_mesh()` (parametric grid triangulation), `vertices_to_mesh3d()` (vertices/faces to Plotly Mesh3d trace)
|
|
118
111
|
|
|
119
112
|
## Dependencies
|
|
120
113
|
|
|
121
114
|
- **plotly**: Interactive 2D and 3D visualization
|
|
122
|
-
- **matplotlib**: Static plot support
|
|
123
115
|
- **numpy**: Array operations
|
|
124
|
-
- **scipy**: Marching cubes mesh extraction
|
|
125
|
-
- **scikit-image**: Additional mesh processing
|
|
126
116
|
- **gri-utils**: Coordinate conversions and constants
|
|
127
117
|
|
|
128
118
|
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
"""Plotting utilities for GeoSol Research.
|
|
2
2
|
|
|
3
3
|
This package provides utilities for 2D and 3D visualization using Plotly,
|
|
4
|
-
including scatter plots, geographic maps, and 3D
|
|
5
|
-
observables.
|
|
4
|
+
including scatter plots, geographic maps, and parametric 3D shapes for
|
|
5
|
+
geolocation observables.
|
|
6
6
|
|
|
7
7
|
Main components:
|
|
8
8
|
- scatter: 2D scatter plots with Plotly
|
|
9
9
|
- scatter_map: Geographic scatter maps
|
|
10
10
|
- Figure3D: 3D figure for combining surfaces
|
|
11
11
|
- shapes: Geometric shapes (Ellipsoid, Sphere, Cone, Cylinder)
|
|
12
|
-
- observables:
|
|
13
|
-
- surfaces:
|
|
12
|
+
- observables: Parametric geolocation surfaces (AOA, LOS, Range)
|
|
13
|
+
- surfaces: ImplicitShape ABC and mesh trace utilities
|
|
14
14
|
- frames: Coordinate frame handling
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
# 2D plotting
|
|
18
18
|
# 3D plotting infrastructure
|
|
19
|
+
from .basemap import BasemapStyle, add_offline_basemap
|
|
19
20
|
from .figure3d import DEFAULT_COLORS, Figure3D, plot_surfaces
|
|
20
21
|
from .figure_map import FigureMap, MapStyle
|
|
21
22
|
from .frames import Bounds, Frame, FrameTransformer
|
|
@@ -23,13 +24,11 @@ from .frames import Bounds, Frame, FrameTransformer
|
|
|
23
24
|
# Observables
|
|
24
25
|
from .observables import (
|
|
25
26
|
AoaSurface,
|
|
26
|
-
FdoaSurface,
|
|
27
27
|
LosSurface,
|
|
28
28
|
RangeSphere,
|
|
29
|
-
TdoaSurface,
|
|
30
|
-
TerrainSurface,
|
|
31
29
|
)
|
|
32
30
|
from .plot_ellipse import plot_ellipse
|
|
31
|
+
from .polylines import split_at_antimeridian
|
|
33
32
|
from .scatter import scatter
|
|
34
33
|
from .scatter_map import scatter_map
|
|
35
34
|
|
|
@@ -45,25 +44,17 @@ from .surfaces import (
|
|
|
45
44
|
expand_bounds,
|
|
46
45
|
merge_bounds,
|
|
47
46
|
)
|
|
48
|
-
from .surfaces.
|
|
49
|
-
axis_value,
|
|
50
|
-
distance_from_axis,
|
|
51
|
-
distance_from_line,
|
|
52
|
-
distance_from_line_segment,
|
|
53
|
-
distance_from_plane,
|
|
54
|
-
distance_from_point,
|
|
55
|
-
)
|
|
56
|
-
from .surfaces.mesh import field_to_mesh, grid_to_mesh, vertices_to_mesh3d
|
|
47
|
+
from .surfaces.mesh import grid_to_mesh, vertices_to_mesh3d
|
|
57
48
|
|
|
58
49
|
__all__ = [
|
|
59
50
|
"DEFAULT_COLORS",
|
|
60
51
|
"AoaSurface",
|
|
52
|
+
"BasemapStyle",
|
|
61
53
|
"Bounds",
|
|
62
54
|
"Cone",
|
|
63
55
|
"Cylinder",
|
|
64
56
|
"Ellipse",
|
|
65
57
|
"Ellipsoid",
|
|
66
|
-
"FdoaSurface",
|
|
67
58
|
"Figure3D",
|
|
68
59
|
"FigureMap",
|
|
69
60
|
"Frame",
|
|
@@ -73,19 +64,11 @@ __all__ = [
|
|
|
73
64
|
"MapStyle",
|
|
74
65
|
"RangeSphere",
|
|
75
66
|
"Sphere",
|
|
76
|
-
"
|
|
77
|
-
"TerrainSurface",
|
|
78
|
-
"axis_value",
|
|
67
|
+
"add_offline_basemap",
|
|
79
68
|
"cone_mesh",
|
|
80
69
|
"cylinder_mesh",
|
|
81
|
-
"distance_from_axis",
|
|
82
|
-
"distance_from_line",
|
|
83
|
-
"distance_from_line_segment",
|
|
84
|
-
"distance_from_plane",
|
|
85
|
-
"distance_from_point",
|
|
86
70
|
"ellipsoid_mesh",
|
|
87
71
|
"expand_bounds",
|
|
88
|
-
"field_to_mesh",
|
|
89
72
|
"grid_to_mesh",
|
|
90
73
|
"merge_bounds",
|
|
91
74
|
"plot_ellipse",
|
|
@@ -93,5 +76,6 @@ __all__ = [
|
|
|
93
76
|
"scatter",
|
|
94
77
|
"scatter_map",
|
|
95
78
|
"sphere_mesh",
|
|
79
|
+
"split_at_antimeridian",
|
|
96
80
|
"vertices_to_mesh3d",
|
|
97
81
|
]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""PNG heatmap image generation for map-figure overlays.
|
|
2
|
+
|
|
3
|
+
Plotly's ``Scattermap`` cannot host a ``go.Heatmap`` trace because heatmaps
|
|
4
|
+
live in a Cartesian plot area, not a map. The workaround this module
|
|
5
|
+
implements is to encode the scalar field as an RGBA PNG (base64 data URI)
|
|
6
|
+
and add it to ``layout.map.layers`` as an ``sourcetype="image"`` entry.
|
|
7
|
+
|
|
8
|
+
The work this module does:
|
|
9
|
+
|
|
10
|
+
* colorscale sampling into a 256-entry lookup table;
|
|
11
|
+
* a Mercator pre-resample of the lat axis so the image aligns with the
|
|
12
|
+
basemap at non-equatorial latitudes (the image-layer is stretched
|
|
13
|
+
linearly in Mercator y; our grid is linear in geodetic latitude);
|
|
14
|
+
* a Gaussian blur on the alpha channel so the footprint mask's hard
|
|
15
|
+
per-cell edges fade smoothly at the boundary.
|
|
16
|
+
|
|
17
|
+
All functions here are private helpers for ``FigureMap.add_heatmap_image``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import base64
|
|
23
|
+
import io
|
|
24
|
+
import math
|
|
25
|
+
from typing import TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
import plotly.colors as pc
|
|
29
|
+
from PIL import Image, ImageFilter
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from numpy.typing import NDArray
|
|
33
|
+
|
|
34
|
+
LUT_SIZE = 256
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _colorscale_lut(name: str, n: int = LUT_SIZE) -> NDArray[np.uint8]:
|
|
38
|
+
"""Sample a named Plotly colorscale into an (n, 3) uint8 RGB lookup table."""
|
|
39
|
+
samples = pc.sample_colorscale(name, np.linspace(0.0, 1.0, n))
|
|
40
|
+
rows = [
|
|
41
|
+
[int(c) for c in s.removeprefix("rgb(").rstrip(")").split(",")] for s in samples
|
|
42
|
+
]
|
|
43
|
+
return np.array(rows, dtype=np.uint8)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _mercator_remap(
|
|
47
|
+
values: NDArray[np.floating],
|
|
48
|
+
lat_axis: NDArray[np.floating],
|
|
49
|
+
) -> NDArray[np.floating]:
|
|
50
|
+
"""Resample a (M, N) grid so rows are linearly spaced in Mercator y.
|
|
51
|
+
|
|
52
|
+
The input ``values`` are assumed to be on a grid whose row i has
|
|
53
|
+
latitude ``lat_axis[i]`` in degrees (linear in geodetic latitude).
|
|
54
|
+
Returns an array of the same shape whose row i corresponds to a
|
|
55
|
+
latitude linearly spaced in Mercator y between the endpoints of
|
|
56
|
+
``lat_axis``. This is what the Plotly image layer expects when its
|
|
57
|
+
``coordinates`` corners are geodetic lat/lon.
|
|
58
|
+
"""
|
|
59
|
+
m = len(lat_axis)
|
|
60
|
+
y_min = math.log(math.tan(math.pi / 4.0 + math.radians(float(lat_axis[0])) / 2.0))
|
|
61
|
+
y_max = math.log(math.tan(math.pi / 4.0 + math.radians(float(lat_axis[-1])) / 2.0))
|
|
62
|
+
target_y = np.linspace(y_min, y_max, m)
|
|
63
|
+
target_lat = np.degrees(2.0 * (np.arctan(np.exp(target_y)) - math.pi / 4.0))
|
|
64
|
+
src_idx = np.interp(target_lat, lat_axis, np.arange(m, dtype=np.float64))
|
|
65
|
+
src_lo = np.floor(src_idx).astype(np.int64)
|
|
66
|
+
src_hi = np.minimum(src_lo + 1, m - 1)
|
|
67
|
+
frac = (src_idx - src_lo)[:, None]
|
|
68
|
+
return (1.0 - frac) * values[src_lo] + frac * values[src_hi]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def values_to_png_data_uri( # noqa: PLR0913
|
|
72
|
+
values: NDArray[np.floating],
|
|
73
|
+
lat_axis: NDArray[np.floating],
|
|
74
|
+
colorscale: str,
|
|
75
|
+
*,
|
|
76
|
+
alpha: float,
|
|
77
|
+
symmetric: bool,
|
|
78
|
+
blur_px: float,
|
|
79
|
+
value_range: tuple[float, float] | None,
|
|
80
|
+
) -> tuple[str, float, float]:
|
|
81
|
+
"""Render a lat/lon-gridded scalar field to a PNG data URI.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
values: Scalar field, shape (M, N). NaN cells are rendered
|
|
85
|
+
transparent.
|
|
86
|
+
lat_axis: Latitude per row in degrees, shape (M,). Used both to
|
|
87
|
+
pick the input-row colormap and to Mercator-resample before
|
|
88
|
+
encoding.
|
|
89
|
+
colorscale: Plotly colorscale name.
|
|
90
|
+
alpha: Opacity (0-1) for finite cells.
|
|
91
|
+
symmetric: When True and the values span zero, expand the color
|
|
92
|
+
range to be symmetric about zero. Useful for diverging
|
|
93
|
+
colorscales on signed fields (TDOA-dot).
|
|
94
|
+
blur_px: Gaussian blur radius (pixels) applied to the alpha
|
|
95
|
+
channel only. Softens the per-cell stair-step at the mask
|
|
96
|
+
edge without blurring the color data. Set to 0 to disable.
|
|
97
|
+
value_range: Explicit (vmin, vmax) override. Takes precedence
|
|
98
|
+
over ``symmetric`` and the implicit min/max.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Tuple of (data URI string, vmin, vmax).
|
|
102
|
+
"""
|
|
103
|
+
v = np.asarray(values, dtype=np.float64)
|
|
104
|
+
if value_range is not None:
|
|
105
|
+
vmin, vmax = value_range
|
|
106
|
+
else:
|
|
107
|
+
vmin, vmax = float(np.nanmin(v)), float(np.nanmax(v))
|
|
108
|
+
if symmetric and vmin < 0.0 < vmax:
|
|
109
|
+
vabs = max(abs(vmin), abs(vmax))
|
|
110
|
+
vmin, vmax = -vabs, vabs
|
|
111
|
+
|
|
112
|
+
v_remap = _mercator_remap(v, np.asarray(lat_axis, dtype=np.float64))
|
|
113
|
+
finite = np.isfinite(v_remap)
|
|
114
|
+
v_safe = np.where(finite, v_remap, vmin)
|
|
115
|
+
denom = vmax - vmin if vmax > vmin else 1.0
|
|
116
|
+
norm = np.clip((v_safe - vmin) / denom, 0.0, 1.0)
|
|
117
|
+
idx = (norm * (LUT_SIZE - 1)).astype(np.int32)
|
|
118
|
+
|
|
119
|
+
rgb = _colorscale_lut(colorscale)[idx]
|
|
120
|
+
a = np.where(finite, int(alpha * 255), 0).astype(np.uint8)
|
|
121
|
+
rgba = np.dstack([rgb, a])[::-1, :, :] # flip rows so lat increases upward
|
|
122
|
+
|
|
123
|
+
img = Image.fromarray(rgba, mode="RGBA")
|
|
124
|
+
if blur_px > 0:
|
|
125
|
+
r_ch, g_ch, b_ch, a_ch = img.split()
|
|
126
|
+
a_ch = a_ch.filter(ImageFilter.GaussianBlur(radius=blur_px))
|
|
127
|
+
img = Image.merge("RGBA", (r_ch, g_ch, b_ch, a_ch))
|
|
128
|
+
|
|
129
|
+
buf = io.BytesIO()
|
|
130
|
+
img.save(buf, format="PNG")
|
|
131
|
+
uri = "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode("ascii")
|
|
132
|
+
return uri, vmin, vmax
|