cesiumjs-anywidget 0.2.4__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 +6 -0
- cesiumjs_anywidget/index.js +1415 -0
- cesiumjs_anywidget/styles.css +32 -0
- cesiumjs_anywidget/widget.py +376 -0
- cesiumjs_anywidget-0.2.4.dist-info/METADATA +624 -0
- cesiumjs_anywidget-0.2.4.dist-info/RECORD +8 -0
- cesiumjs_anywidget-0.2.4.dist-info/WHEEL +4 -0
- cesiumjs_anywidget-0.2.4.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CesiumJS Anywidget Styles
|
|
3
|
+
*
|
|
4
|
+
* Basic styling for the CesiumJS widget container
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* Import CesiumJS base styles from official CDN */
|
|
8
|
+
@import url("https://cesium.com/downloads/cesiumjs/releases/1.135/Build/Cesium/Widgets/widgets.css");
|
|
9
|
+
|
|
10
|
+
/* Ensure the widget container has proper dimensions and overflow handling */
|
|
11
|
+
.cesium-widget {
|
|
12
|
+
position: relative;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Ensure Cesium viewer fills the container */
|
|
17
|
+
.cesium-viewer {
|
|
18
|
+
width: 100% !important;
|
|
19
|
+
height: 100% !important;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Remove default margins from the widget */
|
|
23
|
+
.cesium-widget-container {
|
|
24
|
+
margin: 0;
|
|
25
|
+
padding: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Ensure proper canvas sizing */
|
|
29
|
+
.cesium-viewer canvas {
|
|
30
|
+
display: block;
|
|
31
|
+
}
|
|
32
|
+
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""CesiumJS widget implementation using anywidget."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pathlib
|
|
5
|
+
import anywidget
|
|
6
|
+
import traitlets
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CesiumWidget(anywidget.AnyWidget):
|
|
10
|
+
"""A Jupyter widget for CesiumJS 3D globe visualization.
|
|
11
|
+
|
|
12
|
+
This widget provides an interactive 3D globe with support for:
|
|
13
|
+
- Camera position control (latitude, longitude, altitude)
|
|
14
|
+
- Terrain and imagery visualization
|
|
15
|
+
- Entity management (markers, shapes, models)
|
|
16
|
+
- Bidirectional state synchronization between Python and JavaScript
|
|
17
|
+
|
|
18
|
+
Examples
|
|
19
|
+
--------
|
|
20
|
+
Basic usage:
|
|
21
|
+
>>> from cesiumjs_anywidget import CesiumWidget
|
|
22
|
+
>>> widget = CesiumWidget()
|
|
23
|
+
>>> widget # Display in Jupyter
|
|
24
|
+
|
|
25
|
+
Fly to a location:
|
|
26
|
+
>>> widget.latitude = 40.7128
|
|
27
|
+
>>> widget.longitude = -74.0060
|
|
28
|
+
>>> widget.altitude = 10000
|
|
29
|
+
|
|
30
|
+
Debugging:
|
|
31
|
+
>>> widget.debug_info() # Show debug information
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Load JavaScript and CSS from files
|
|
35
|
+
_esm = pathlib.Path(__file__).parent / "index.js"
|
|
36
|
+
_css = pathlib.Path(__file__).parent / "styles.css"
|
|
37
|
+
|
|
38
|
+
# Camera position properties (synced with JavaScript)
|
|
39
|
+
latitude = traitlets.Float(-122.4175, help="Camera latitude in degrees").tag(
|
|
40
|
+
sync=True
|
|
41
|
+
)
|
|
42
|
+
longitude = traitlets.Float(37.655, help="Camera longitude in degrees").tag(
|
|
43
|
+
sync=True
|
|
44
|
+
)
|
|
45
|
+
altitude = traitlets.Float(400.0, help="Camera altitude in meters").tag(sync=True)
|
|
46
|
+
|
|
47
|
+
# Camera orientation
|
|
48
|
+
heading = traitlets.Float(0.0, help="Camera heading in degrees").tag(sync=True)
|
|
49
|
+
pitch = traitlets.Float(-15.0, help="Camera pitch in degrees").tag(sync=True)
|
|
50
|
+
roll = traitlets.Float(0.0, help="Camera roll in degrees").tag(sync=True)
|
|
51
|
+
|
|
52
|
+
# Viewer configuration
|
|
53
|
+
height = traitlets.Unicode("600px", help="Widget height").tag(sync=True)
|
|
54
|
+
|
|
55
|
+
# Viewer options
|
|
56
|
+
enable_terrain = traitlets.Bool(True, help="Enable terrain visualization").tag(
|
|
57
|
+
sync=True
|
|
58
|
+
)
|
|
59
|
+
enable_lighting = traitlets.Bool(False, help="Enable scene lighting").tag(sync=True)
|
|
60
|
+
show_timeline = traitlets.Bool(True, help="Show timeline widget").tag(sync=True)
|
|
61
|
+
show_animation = traitlets.Bool(True, help="Show animation widget").tag(sync=True)
|
|
62
|
+
|
|
63
|
+
# Cesium Ion access token (optional, uses default if not set)
|
|
64
|
+
ion_access_token = traitlets.Unicode("", help="Cesium Ion access token").tag(
|
|
65
|
+
sync=True
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# GeoJSON data for visualization
|
|
69
|
+
geojson_data = traitlets.Dict(
|
|
70
|
+
default_value=None, allow_none=True, help="GeoJSON data to display"
|
|
71
|
+
).tag(sync=True)
|
|
72
|
+
|
|
73
|
+
# CZML data for visualization
|
|
74
|
+
czml_data = traitlets.List(
|
|
75
|
+
trait=traitlets.Dict(),
|
|
76
|
+
default_value=None,
|
|
77
|
+
allow_none=True,
|
|
78
|
+
help="CZML data to display",
|
|
79
|
+
).tag(sync=True)
|
|
80
|
+
|
|
81
|
+
# Measurement tools
|
|
82
|
+
measurement_mode = traitlets.Unicode(
|
|
83
|
+
"",
|
|
84
|
+
help="Active measurement mode: 'distance', 'multi-distance', 'height', or '' for none",
|
|
85
|
+
).tag(sync=True)
|
|
86
|
+
measurement_results = traitlets.List(
|
|
87
|
+
trait=traitlets.Dict(), default_value=[], help="List of measurement results"
|
|
88
|
+
).tag(sync=True)
|
|
89
|
+
load_measurements_trigger = traitlets.Dict(
|
|
90
|
+
default_value={}, help="Trigger to load measurements with visual display"
|
|
91
|
+
).tag(sync=True)
|
|
92
|
+
focus_measurement_trigger = traitlets.Dict(
|
|
93
|
+
default_value={}, help="Trigger to focus on a specific measurement"
|
|
94
|
+
).tag(sync=True)
|
|
95
|
+
show_measurement_tools = traitlets.Bool(
|
|
96
|
+
default_value=True, help="Show or hide measurement toolbar"
|
|
97
|
+
).tag(sync=True)
|
|
98
|
+
show_measurements_list = traitlets.Bool(
|
|
99
|
+
default_value=True, help="Show or hide measurements list panel"
|
|
100
|
+
).tag(sync=True)
|
|
101
|
+
|
|
102
|
+
def __init__(self, **kwargs):
|
|
103
|
+
"""Initialize the CesiumWidget.
|
|
104
|
+
|
|
105
|
+
Automatically checks for CESIUM_ION_TOKEN environment variable if no token is provided.
|
|
106
|
+
"""
|
|
107
|
+
# Check for token in environment variable if not provided
|
|
108
|
+
if "ion_access_token" not in kwargs or not kwargs["ion_access_token"]:
|
|
109
|
+
env_token = os.environ.get("CESIUM_ION_TOKEN", "")
|
|
110
|
+
if env_token:
|
|
111
|
+
kwargs["ion_access_token"] = env_token
|
|
112
|
+
else:
|
|
113
|
+
print("⚠️ No Cesium Ion access token provided.")
|
|
114
|
+
print(
|
|
115
|
+
" Your access token can be found at: https://ion.cesium.com/tokens"
|
|
116
|
+
)
|
|
117
|
+
print(" You can set it via:")
|
|
118
|
+
print(" - CesiumWidget(ion_access_token='your_token')")
|
|
119
|
+
print(" - export CESIUM_ION_TOKEN='your_token' # in your shell")
|
|
120
|
+
print(" Note: Some features may not work without a token.")
|
|
121
|
+
|
|
122
|
+
super().__init__(**kwargs)
|
|
123
|
+
|
|
124
|
+
def fly_to(self, latitude: float, longitude: float, altitude: float = 400, duration: float = 3.0):
|
|
125
|
+
"""Fly the camera to a specific location.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
latitude : float
|
|
130
|
+
Target latitude in degrees
|
|
131
|
+
longitude : float
|
|
132
|
+
Target longitude in degrees
|
|
133
|
+
altitude : float, optional
|
|
134
|
+
Target altitude in meters (default: 400)
|
|
135
|
+
duration : float, optional
|
|
136
|
+
Flight duration in seconds (default: 3.0)
|
|
137
|
+
"""
|
|
138
|
+
self.latitude = latitude
|
|
139
|
+
self.longitude = longitude
|
|
140
|
+
self.altitude = altitude
|
|
141
|
+
|
|
142
|
+
def set_view(
|
|
143
|
+
self, latitude : float , longitude: float, altitude: float = 400, heading: float = 0.0, pitch: float = -15.0, roll: float = 0.0
|
|
144
|
+
):
|
|
145
|
+
"""Set the camera view instantly without animation.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
latitude : float
|
|
150
|
+
Camera latitude in degrees
|
|
151
|
+
longitude : float
|
|
152
|
+
Camera longitude in degrees
|
|
153
|
+
altitude : float, optional
|
|
154
|
+
Camera altitude in meters (default: 400)
|
|
155
|
+
heading : float, optional
|
|
156
|
+
Camera heading in degrees (default: 0.0)
|
|
157
|
+
pitch : float, optional
|
|
158
|
+
Camera pitch in degrees (default: -15.0)
|
|
159
|
+
roll : float, optional
|
|
160
|
+
Camera roll in degrees (default: 0.0)
|
|
161
|
+
"""
|
|
162
|
+
self.latitude = latitude
|
|
163
|
+
self.longitude = longitude
|
|
164
|
+
self.altitude = altitude
|
|
165
|
+
self.heading = heading
|
|
166
|
+
self.pitch = pitch
|
|
167
|
+
self.roll = roll
|
|
168
|
+
|
|
169
|
+
def load_geojson(self, geojson):
|
|
170
|
+
"""Load GeoJSON data for visualization.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
geojson : dict
|
|
175
|
+
GeoJSON dictionary or GeoJSON object
|
|
176
|
+
"""
|
|
177
|
+
if isinstance(geojson, str):
|
|
178
|
+
import json
|
|
179
|
+
|
|
180
|
+
geojson = json.loads(geojson)
|
|
181
|
+
self.geojson_data = geojson
|
|
182
|
+
|
|
183
|
+
def load_czml(self, czml: str | list):
|
|
184
|
+
"""Load CZML data for visualization.
|
|
185
|
+
|
|
186
|
+
CZML (Cesium Language) is a JSON format for describing time-dynamic
|
|
187
|
+
graphical scenes in Cesium. It can describe points, lines, polygons,
|
|
188
|
+
models, and other graphics primitives with time-dynamic positions,
|
|
189
|
+
orientations, colors, and other properties.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
czml : str or list
|
|
194
|
+
CZML document as a JSON string or list of packet dictionaries.
|
|
195
|
+
|
|
196
|
+
Examples
|
|
197
|
+
--------
|
|
198
|
+
From JSON string:
|
|
199
|
+
>>> czml_json = '''[
|
|
200
|
+
... {"id": "document", "version": "1.0"},
|
|
201
|
+
... {"id": "point", "position": {"cartographicDegrees": [-74, 40, 0]}}
|
|
202
|
+
... ]'''
|
|
203
|
+
>>> widget.load_czml(czml_json)
|
|
204
|
+
|
|
205
|
+
From list of dicts:
|
|
206
|
+
>>> czml = [
|
|
207
|
+
... {"id": "document", "version": "1.0"},
|
|
208
|
+
... {"id": "point", "position": {"cartographicDegrees": [-74, 40, 0]}}
|
|
209
|
+
... ]
|
|
210
|
+
>>> widget.load_czml(czml)
|
|
211
|
+
"""
|
|
212
|
+
import json
|
|
213
|
+
|
|
214
|
+
# Handle string input (JSON)
|
|
215
|
+
if isinstance(czml, str):
|
|
216
|
+
czml = json.loads(czml)
|
|
217
|
+
|
|
218
|
+
# Ensure we have a list
|
|
219
|
+
if not isinstance(czml, list):
|
|
220
|
+
raise ValueError("CZML data must be a JSON string or list of packets")
|
|
221
|
+
|
|
222
|
+
# Validate basic structure - should have at least one packet
|
|
223
|
+
if len(czml) == 0:
|
|
224
|
+
raise ValueError("CZML document must contain at least one packet")
|
|
225
|
+
|
|
226
|
+
self.czml_data = czml
|
|
227
|
+
|
|
228
|
+
def enable_measurement(self, mode: str = "distance"):
|
|
229
|
+
"""Enable a measurement tool.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
mode : str, optional
|
|
234
|
+
Measurement mode to enable:
|
|
235
|
+
- 'distance': Two-point distance measurement
|
|
236
|
+
- 'multi-distance': Multi-point polyline measurement
|
|
237
|
+
- 'height': Vertical height measurement from ground
|
|
238
|
+
- 'area': Polygon area measurement
|
|
239
|
+
Default: 'distance'
|
|
240
|
+
"""
|
|
241
|
+
valid_modes = ["distance", "multi-distance", "height", "area"]
|
|
242
|
+
if mode not in valid_modes:
|
|
243
|
+
raise ValueError(f"Invalid mode '{mode}'. Must be one of {valid_modes}")
|
|
244
|
+
self.measurement_mode = mode
|
|
245
|
+
|
|
246
|
+
def disable_measurement(self):
|
|
247
|
+
"""Disable the active measurement tool and clear measurements."""
|
|
248
|
+
self.measurement_mode = ""
|
|
249
|
+
self.measurement_results = []
|
|
250
|
+
|
|
251
|
+
def get_measurements(self):
|
|
252
|
+
"""Get all measurement results.
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
list of dict
|
|
257
|
+
List of measurement results, each containing:
|
|
258
|
+
- type: measurement type ('distance', 'multi-distance', 'height', or 'area')
|
|
259
|
+
- value: measured value in meters (or square meters for area)
|
|
260
|
+
- points: list of {lat, lon, alt} coordinates
|
|
261
|
+
"""
|
|
262
|
+
return self.measurement_results
|
|
263
|
+
|
|
264
|
+
def clear_measurements(self):
|
|
265
|
+
"""Clear all measurements from the viewer."""
|
|
266
|
+
self.measurement_results = []
|
|
267
|
+
|
|
268
|
+
def load_measurements(self, measurements):
|
|
269
|
+
"""Load and display measurements on the map.
|
|
270
|
+
|
|
271
|
+
Parameters
|
|
272
|
+
----------
|
|
273
|
+
measurements : list of dict
|
|
274
|
+
List of measurements to load and display. Each measurement should contain:
|
|
275
|
+
- type: str - 'distance', 'multi-distance', 'height', or 'area'
|
|
276
|
+
- points: list of [lon, lat, alt] coordinates (GeoJSON style)
|
|
277
|
+
|
|
278
|
+
Examples
|
|
279
|
+
--------
|
|
280
|
+
>>> widget.load_measurements([
|
|
281
|
+
... {
|
|
282
|
+
... "type": "distance",
|
|
283
|
+
... "points": [[2.3522, 48.8566, 100], [2.3550, 48.8600, 105]]
|
|
284
|
+
... },
|
|
285
|
+
... {
|
|
286
|
+
... "type": "area",
|
|
287
|
+
... "points": [[2.3522, 48.8566, 100], [2.3550, 48.8600, 105], [2.3500, 48.8620, 98]]
|
|
288
|
+
... }
|
|
289
|
+
... ])
|
|
290
|
+
"""
|
|
291
|
+
import time
|
|
292
|
+
|
|
293
|
+
# Send measurements with a timestamp to trigger the change detection
|
|
294
|
+
self.load_measurements_trigger = {
|
|
295
|
+
"measurements": measurements,
|
|
296
|
+
"timestamp": time.time(),
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
def focus_on_measurement(self, index : int):
|
|
300
|
+
"""Focus the camera on a specific measurement by index.
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
index : int
|
|
305
|
+
The index of the measurement to focus on (0-based)
|
|
306
|
+
|
|
307
|
+
Examples
|
|
308
|
+
--------
|
|
309
|
+
>>> widget.focus_on_measurement(0) # Focus on first measurement
|
|
310
|
+
>>> widget.focus_on_measurement(2) # Focus on third measurement
|
|
311
|
+
"""
|
|
312
|
+
import time
|
|
313
|
+
|
|
314
|
+
self.focus_measurement_trigger = {"index": index, "timestamp": time.time()}
|
|
315
|
+
|
|
316
|
+
def show_tools(self):
|
|
317
|
+
"""Show the measurement tools toolbar."""
|
|
318
|
+
self.show_measurement_tools = True
|
|
319
|
+
|
|
320
|
+
def hide_tools(self):
|
|
321
|
+
"""Hide the measurement tools toolbar."""
|
|
322
|
+
self.show_measurement_tools = False
|
|
323
|
+
|
|
324
|
+
def show_list(self):
|
|
325
|
+
"""Show the measurements list panel."""
|
|
326
|
+
self.show_measurements_list = True
|
|
327
|
+
|
|
328
|
+
def hide_list(self):
|
|
329
|
+
"""Hide the measurements list panel."""
|
|
330
|
+
self.show_measurements_list = False
|
|
331
|
+
|
|
332
|
+
def debug_info(self):
|
|
333
|
+
"""Print debug information about the widget.
|
|
334
|
+
|
|
335
|
+
This is useful for troubleshooting widget initialization issues.
|
|
336
|
+
"""
|
|
337
|
+
print("=== CesiumWidget Debug Info ===")
|
|
338
|
+
print(f"Widget class: {self.__class__.__name__}")
|
|
339
|
+
print(f"Anywidget version: {anywidget.__version__}")
|
|
340
|
+
|
|
341
|
+
# Check file paths (note: after widget instantiation, _esm and _css contain file contents)
|
|
342
|
+
esm_path = pathlib.Path(__file__).parent / "index.js"
|
|
343
|
+
css_path = pathlib.Path(__file__).parent / "styles.css"
|
|
344
|
+
|
|
345
|
+
print("\nJavaScript file:")
|
|
346
|
+
print(f" Path: {esm_path}")
|
|
347
|
+
print(f" Exists: {esm_path.exists()}")
|
|
348
|
+
if esm_path.exists():
|
|
349
|
+
print(f" Size: {esm_path.stat().st_size} bytes")
|
|
350
|
+
elif isinstance(self._esm, str):
|
|
351
|
+
print(f" Content loaded: {len(self._esm)} chars")
|
|
352
|
+
|
|
353
|
+
print("\nCSS file:")
|
|
354
|
+
print(f" Path: {css_path}")
|
|
355
|
+
print(f" Exists: {css_path.exists()}")
|
|
356
|
+
if css_path.exists():
|
|
357
|
+
print(f" Size: {css_path.stat().st_size} bytes")
|
|
358
|
+
elif isinstance(self._css, str):
|
|
359
|
+
print(f" Content loaded: {len(self._css)} chars")
|
|
360
|
+
|
|
361
|
+
# Show current state
|
|
362
|
+
print("\nCurrent state:")
|
|
363
|
+
print(f" Position: ({self.latitude:.4f}°, {self.longitude:.4f}°)")
|
|
364
|
+
print(f" Altitude: {self.altitude:.2f}m")
|
|
365
|
+
print(f" Height: {self.height}")
|
|
366
|
+
print(f" Terrain: {self.enable_terrain}")
|
|
367
|
+
print(f" Lighting: {self.enable_lighting}")
|
|
368
|
+
|
|
369
|
+
print("\n💡 Debugging tips:")
|
|
370
|
+
print(" 1. Open browser DevTools (F12) and check the Console tab for errors")
|
|
371
|
+
print(" 2. Check Network tab to see if CesiumJS CDN loads successfully")
|
|
372
|
+
print(
|
|
373
|
+
" 3. Try: widget = CesiumWidget(enable_terrain=False) to avoid async terrain loading"
|
|
374
|
+
)
|
|
375
|
+
print(" 4. Ensure you're using JupyterLab 4.0+ or Jupyter Notebook 7.0+")
|
|
376
|
+
print(" 5. Check if anywidget is properly installed: pip show anywidget")
|