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.
@@ -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")