cesiumjs-anywidget 0.5.0__py3-none-any.whl → 0.7.0__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 +23 -2
- cesiumjs_anywidget/exif_utils.py +281 -0
- cesiumjs_anywidget/geoid.py +299 -0
- cesiumjs_anywidget/index.js +1569 -605
- cesiumjs_anywidget/logger.py +73 -0
- cesiumjs_anywidget/styles.css +24 -1
- cesiumjs_anywidget/widget.py +616 -42
- {cesiumjs_anywidget-0.5.0.dist-info → cesiumjs_anywidget-0.7.0.dist-info}/METADATA +45 -6
- cesiumjs_anywidget-0.7.0.dist-info/RECORD +11 -0
- cesiumjs_anywidget-0.5.0.dist-info/RECORD +0 -8
- {cesiumjs_anywidget-0.5.0.dist-info → cesiumjs_anywidget-0.7.0.dist-info}/WHEEL +0 -0
- {cesiumjs_anywidget-0.5.0.dist-info → cesiumjs_anywidget-0.7.0.dist-info}/licenses/LICENSE +0 -0
cesiumjs_anywidget/widget.py
CHANGED
|
@@ -4,6 +4,9 @@ import os
|
|
|
4
4
|
import pathlib
|
|
5
5
|
import anywidget
|
|
6
6
|
import traitlets
|
|
7
|
+
from .logger import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
class CesiumWidget(anywidget.AnyWidget):
|
|
@@ -12,6 +15,7 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
12
15
|
This widget provides an interactive 3D globe with support for:
|
|
13
16
|
- Camera position control (latitude, longitude, altitude)
|
|
14
17
|
- Terrain and imagery visualization
|
|
18
|
+
- Google Photorealistic 3D Tiles (global photorealistic base layer)
|
|
15
19
|
- Entity management (markers, shapes, models)
|
|
16
20
|
- Bidirectional state synchronization between Python and JavaScript
|
|
17
21
|
|
|
@@ -59,6 +63,16 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
59
63
|
enable_lighting = traitlets.Bool(False, help="Enable scene lighting").tag(sync=True)
|
|
60
64
|
show_timeline = traitlets.Bool(True, help="Show timeline widget").tag(sync=True)
|
|
61
65
|
show_animation = traitlets.Bool(True, help="Show animation widget").tag(sync=True)
|
|
66
|
+
|
|
67
|
+
# Photorealistic 3D Tiles options
|
|
68
|
+
enable_photorealistic_tiles = traitlets.Bool(
|
|
69
|
+
False,
|
|
70
|
+
help="Enable Google Photorealistic 3D Tiles (global photorealistic base layer)"
|
|
71
|
+
).tag(sync=True)
|
|
72
|
+
show_globe = traitlets.Bool(
|
|
73
|
+
True,
|
|
74
|
+
help="Show the base globe (set to False when using photorealistic tiles)"
|
|
75
|
+
).tag(sync=True)
|
|
62
76
|
|
|
63
77
|
# Cesium Ion access token (optional, uses default if not set)
|
|
64
78
|
ion_access_token = traitlets.Unicode("", help="Cesium Ion access token").tag(
|
|
@@ -79,6 +93,12 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
79
93
|
help="List of CZML documents to display",
|
|
80
94
|
).tag(sync=True)
|
|
81
95
|
|
|
96
|
+
# CZML entity updates for dynamic property changes
|
|
97
|
+
czml_entity_update = traitlets.Dict(
|
|
98
|
+
default_value={},
|
|
99
|
+
help="Update properties of existing CZML entities (entity_id, property_name, value)"
|
|
100
|
+
).tag(sync=True)
|
|
101
|
+
|
|
82
102
|
# Interaction event data - sent when user interaction ends
|
|
83
103
|
interaction_event = traitlets.Dict(
|
|
84
104
|
default_value={},
|
|
@@ -103,6 +123,27 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
103
123
|
help="SkyBox rendering settings (show, sources for cube map faces)"
|
|
104
124
|
).tag(sync=True)
|
|
105
125
|
|
|
126
|
+
# Clock/Timeline configuration
|
|
127
|
+
current_time = traitlets.Unicode(
|
|
128
|
+
"",
|
|
129
|
+
help="Current time in ISO 8601 format (e.g., '2023-08-01T14:30:00Z')"
|
|
130
|
+
).tag(sync=True)
|
|
131
|
+
|
|
132
|
+
clock_multiplier = traitlets.Float(
|
|
133
|
+
1.0,
|
|
134
|
+
help="Animation speed multiplier (1.0 = real-time, 60.0 = 60x speed)"
|
|
135
|
+
).tag(sync=True)
|
|
136
|
+
|
|
137
|
+
should_animate = traitlets.Bool(
|
|
138
|
+
False,
|
|
139
|
+
help="Whether the clock should animate (play/pause)"
|
|
140
|
+
).tag(sync=True)
|
|
141
|
+
|
|
142
|
+
clock_command = traitlets.Dict(
|
|
143
|
+
default_value={},
|
|
144
|
+
help="Clock control commands (setTime, play, pause, setMultiplier)"
|
|
145
|
+
).tag(sync=True)
|
|
146
|
+
|
|
106
147
|
# Camera commands (for advanced camera operations)
|
|
107
148
|
camera_command = traitlets.Dict(
|
|
108
149
|
default_value={},
|
|
@@ -124,10 +165,43 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
124
165
|
default_value={}, help="Trigger to focus on a specific measurement"
|
|
125
166
|
).tag(sync=True)
|
|
126
167
|
show_measurement_tools = traitlets.Bool(
|
|
127
|
-
default_value=
|
|
168
|
+
default_value=False, help="Show or hide measurement toolbar"
|
|
128
169
|
).tag(sync=True)
|
|
129
170
|
show_measurements_list = traitlets.Bool(
|
|
130
|
-
default_value=
|
|
171
|
+
default_value=False, help="Show or hide measurements list panel"
|
|
172
|
+
).tag(sync=True)
|
|
173
|
+
|
|
174
|
+
# Debug mode for JavaScript logging
|
|
175
|
+
debug_mode = traitlets.Bool(
|
|
176
|
+
default_value=False, help="Enable or disable JavaScript console logging"
|
|
177
|
+
).tag(sync=True)
|
|
178
|
+
|
|
179
|
+
# Camera synchronization callbacks
|
|
180
|
+
camera_sync_enabled = traitlets.Bool(
|
|
181
|
+
default_value=False,
|
|
182
|
+
help="Enable or disable camera position synchronization callbacks"
|
|
183
|
+
).tag(sync=True)
|
|
184
|
+
|
|
185
|
+
# Point picking mode for camera calibration
|
|
186
|
+
point_picking_mode = traitlets.Bool(
|
|
187
|
+
default_value=False,
|
|
188
|
+
help="Enable point picking mode for camera calibration"
|
|
189
|
+
).tag(sync=True)
|
|
190
|
+
|
|
191
|
+
point_picking_config = traitlets.Dict(
|
|
192
|
+
default_value={},
|
|
193
|
+
help="Configuration for point picking (color, label, etc.)"
|
|
194
|
+
).tag(sync=True)
|
|
195
|
+
|
|
196
|
+
picked_points = traitlets.List(
|
|
197
|
+
trait=traitlets.Dict(),
|
|
198
|
+
default_value=[],
|
|
199
|
+
help="List of picked 3D points with coordinates and metadata"
|
|
200
|
+
).tag(sync=True)
|
|
201
|
+
|
|
202
|
+
point_picking_event = traitlets.Dict(
|
|
203
|
+
default_value={},
|
|
204
|
+
help="Last picked point event (triggered on each pick)"
|
|
131
205
|
).tag(sync=True)
|
|
132
206
|
|
|
133
207
|
def __init__(self, **kwargs):
|
|
@@ -141,14 +215,12 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
141
215
|
if env_token:
|
|
142
216
|
kwargs["ion_access_token"] = env_token
|
|
143
217
|
else:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
print(" - export CESIUM_ION_TOKEN='your_token' # in your shell")
|
|
151
|
-
print(" Note: Some features may not work without a token.")
|
|
218
|
+
logger.warning("No Cesium Ion access token provided.")
|
|
219
|
+
logger.warning("Your access token can be found at: https://ion.cesium.com/tokens")
|
|
220
|
+
logger.warning("You can set it via:")
|
|
221
|
+
logger.warning(" - CesiumWidget(ion_access_token='your_token')")
|
|
222
|
+
logger.warning(" - export CESIUM_ION_TOKEN='your_token' # in your shell")
|
|
223
|
+
logger.warning("Note: Some features may not work without a token.")
|
|
152
224
|
|
|
153
225
|
super().__init__(**kwargs)
|
|
154
226
|
|
|
@@ -202,7 +274,8 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
202
274
|
|
|
203
275
|
def set_view(
|
|
204
276
|
self, latitude: float, longitude: float, altitude: float = 400,
|
|
205
|
-
heading: float = 0.0, pitch: float = -15.0, roll: float = 0.0
|
|
277
|
+
heading: float = 0.0, pitch: float = -15.0, roll: float = 0.0,
|
|
278
|
+
fov: float = None, aspect_ratio: float = None, near: float = None, far: float = None
|
|
206
279
|
):
|
|
207
280
|
"""Set the camera view instantly without animation.
|
|
208
281
|
|
|
@@ -220,11 +293,24 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
220
293
|
Camera pitch in degrees (default: -15.0)
|
|
221
294
|
roll : float, optional
|
|
222
295
|
Camera roll in degrees (default: 0.0)
|
|
296
|
+
fov : float, optional
|
|
297
|
+
Field of view in degrees. If provided, sets the camera frustum's vertical
|
|
298
|
+
field of view (for PerspectiveFrustum). Default is None (uses camera default).
|
|
299
|
+
aspect_ratio : float, optional
|
|
300
|
+
Aspect ratio (width/height) of the frustum. If provided, sets the camera
|
|
301
|
+
frustum's aspect ratio. Default is None (uses camera default).
|
|
302
|
+
near : float, optional
|
|
303
|
+
Near clipping plane distance in meters. If provided, sets the camera
|
|
304
|
+
frustum's near plane. Default is None (uses camera default).
|
|
305
|
+
far : float, optional
|
|
306
|
+
Far clipping plane distance in meters. If provided, sets the camera
|
|
307
|
+
frustum's far plane. Default is None (uses camera default).
|
|
223
308
|
|
|
224
309
|
Examples
|
|
225
310
|
--------
|
|
226
311
|
>>> widget.set_view(48.8566, 2.3522, altitude=1000)
|
|
227
312
|
>>> widget.set_view(40.7128, -74.0060, heading=90, pitch=-45)
|
|
313
|
+
>>> widget.set_view(46.3712, 4.6355, altitude=310, fov=60, aspect_ratio=1.5)
|
|
228
314
|
"""
|
|
229
315
|
import time
|
|
230
316
|
# Update traitlets for state sync
|
|
@@ -235,7 +321,7 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
235
321
|
self.pitch = pitch
|
|
236
322
|
self.roll = roll
|
|
237
323
|
# Send command for instant view change
|
|
238
|
-
|
|
324
|
+
camera_cmd = {
|
|
239
325
|
'command': 'setView',
|
|
240
326
|
'latitude': latitude,
|
|
241
327
|
'longitude': longitude,
|
|
@@ -245,6 +331,16 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
245
331
|
'roll': roll,
|
|
246
332
|
'timestamp': time.time()
|
|
247
333
|
}
|
|
334
|
+
# Add optional frustum parameters if provided
|
|
335
|
+
if fov is not None:
|
|
336
|
+
camera_cmd['fov'] = fov
|
|
337
|
+
if aspect_ratio is not None:
|
|
338
|
+
camera_cmd['aspectRatio'] = aspect_ratio
|
|
339
|
+
if near is not None:
|
|
340
|
+
camera_cmd['near'] = near
|
|
341
|
+
if far is not None:
|
|
342
|
+
camera_cmd['far'] = far
|
|
343
|
+
self.camera_command = camera_cmd
|
|
248
344
|
|
|
249
345
|
def look_at(self, target_latitude: float, target_longitude: float, target_altitude: float = 0,
|
|
250
346
|
offset_heading: float = 0.0, offset_pitch: float = -45.0, offset_range: float = 1000.0):
|
|
@@ -561,6 +657,104 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
561
657
|
if roll is not None:
|
|
562
658
|
self.roll = roll
|
|
563
659
|
|
|
660
|
+
def set_camera_from_frustum(self, frustum):
|
|
661
|
+
"""Set camera parameters from a CameraFrustum object.
|
|
662
|
+
|
|
663
|
+
This is a convenience method for setting the camera to match a frustum's
|
|
664
|
+
position and orientation.
|
|
665
|
+
|
|
666
|
+
Parameters
|
|
667
|
+
----------
|
|
668
|
+
frustum : CameraFrustum
|
|
669
|
+
A CameraFrustum object from frustum_czml module containing position
|
|
670
|
+
and orientation information.
|
|
671
|
+
|
|
672
|
+
Examples
|
|
673
|
+
--------
|
|
674
|
+
>>> from examples.photo_projection.frustum_czml import CameraFrustum, Position, Orientation
|
|
675
|
+
>>> position = Position.from_cartographic(longitude=-74.0060, latitude=40.7128, altitude=500)
|
|
676
|
+
>>> orientation = Orientation(heading=45, pitch=-30, roll=0)
|
|
677
|
+
>>> frustum = CameraFrustum(
|
|
678
|
+
... id="my_frustum",
|
|
679
|
+
... name="My Camera",
|
|
680
|
+
... position=position,
|
|
681
|
+
... orientation=orientation,
|
|
682
|
+
... fov_horizontal=60
|
|
683
|
+
... )
|
|
684
|
+
>>> widget.set_camera_from_frustum(frustum)
|
|
685
|
+
|
|
686
|
+
See Also
|
|
687
|
+
--------
|
|
688
|
+
set_camera : Set individual camera parameters
|
|
689
|
+
"""
|
|
690
|
+
# Extract position (convert to cartographic if needed)
|
|
691
|
+
lon, lat, alt = frustum.position.to_cartographic()
|
|
692
|
+
|
|
693
|
+
# Set camera with frustum's position and orientation
|
|
694
|
+
self.set_camera(
|
|
695
|
+
latitude=lat,
|
|
696
|
+
longitude=lon,
|
|
697
|
+
altitude=alt,
|
|
698
|
+
heading=frustum.orientation.heading,
|
|
699
|
+
pitch=frustum.orientation.pitch,
|
|
700
|
+
roll=frustum.orientation.roll
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
def enable_photorealistic_3d_tiles(self, enabled: bool = True):
|
|
704
|
+
"""Enable or disable Google Photorealistic 3D Tiles.
|
|
705
|
+
|
|
706
|
+
This is a convenience method that automatically configures the widget
|
|
707
|
+
with recommended settings for photorealistic tiles.
|
|
708
|
+
|
|
709
|
+
When enabled (True):
|
|
710
|
+
- Activates Google Photorealistic 3D Tiles (global photorealistic base layer)
|
|
711
|
+
- Disables the base globe (recommended to avoid rendering conflicts)
|
|
712
|
+
- Disables terrain (tiles include 3D terrain data)
|
|
713
|
+
|
|
714
|
+
When disabled (False):
|
|
715
|
+
- Deactivates photorealistic tiles
|
|
716
|
+
- Re-enables the base globe
|
|
717
|
+
- Re-enables terrain
|
|
718
|
+
|
|
719
|
+
Parameters
|
|
720
|
+
----------
|
|
721
|
+
enabled : bool, optional
|
|
722
|
+
Whether to enable (True) or disable (False) photorealistic tiles (default: True)
|
|
723
|
+
|
|
724
|
+
Examples
|
|
725
|
+
--------
|
|
726
|
+
>>> widget = CesiumWidget()
|
|
727
|
+
>>> widget.enable_photorealistic_3d_tiles(True)
|
|
728
|
+
>>> # Fly to a location to see the photorealistic tiles
|
|
729
|
+
>>> widget.fly_to(latitude=40.7128, longitude=-74.0060, altitude=2000)
|
|
730
|
+
|
|
731
|
+
>>> # Disable photorealistic tiles and return to standard globe
|
|
732
|
+
>>> widget.enable_photorealistic_3d_tiles(False)
|
|
733
|
+
|
|
734
|
+
Notes
|
|
735
|
+
-----
|
|
736
|
+
- Photorealistic 3D Tiles require a valid Cesium Ion access token
|
|
737
|
+
- Coverage is available for major cities and populated areas globally
|
|
738
|
+
- Performance may vary based on your hardware and network connection
|
|
739
|
+
- Use the geocoder (magnifying glass icon) to search for locations
|
|
740
|
+
|
|
741
|
+
See Also
|
|
742
|
+
--------
|
|
743
|
+
fly_to : Fly the camera to a specific location
|
|
744
|
+
set_view : Set the camera view instantly
|
|
745
|
+
|
|
746
|
+
References
|
|
747
|
+
----------
|
|
748
|
+
- CesiumJS Photorealistic 3D Tiles Tutorial:
|
|
749
|
+
https://cesium.com/learn/cesiumjs-learn/cesiumjs-photorealistic-3d-tiles/
|
|
750
|
+
- Google Photorealistic 3D Tiles Documentation:
|
|
751
|
+
https://cesium.com/platform/cesium-ion/content/google-photorealistic-3d-tiles/
|
|
752
|
+
"""
|
|
753
|
+
self.enable_photorealistic_tiles = enabled
|
|
754
|
+
self.show_globe = not enabled
|
|
755
|
+
if enabled:
|
|
756
|
+
self.enable_terrain = False
|
|
757
|
+
|
|
564
758
|
def load_geojson(self, geojson, append=False):
|
|
565
759
|
"""Load GeoJSON data for visualization.
|
|
566
760
|
|
|
@@ -670,6 +864,47 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
670
864
|
"""
|
|
671
865
|
self.czml_data = []
|
|
672
866
|
|
|
867
|
+
def update_czml_entity(self, entity_id: str, properties: dict):
|
|
868
|
+
"""Update properties of an existing CZML entity.
|
|
869
|
+
|
|
870
|
+
This method allows you to dynamically update properties of CZML entities
|
|
871
|
+
without reloading the entire data source. Useful for interactive sliders.
|
|
872
|
+
|
|
873
|
+
Parameters
|
|
874
|
+
----------
|
|
875
|
+
entity_id : str
|
|
876
|
+
The ID of the entity to update
|
|
877
|
+
properties : dict
|
|
878
|
+
Dictionary of properties to update. Supports:
|
|
879
|
+
- orientation: dict with heading, pitch, roll (in degrees)
|
|
880
|
+
- position: dict with latitude, longitude, altitude
|
|
881
|
+
- Any other CZML property that can be updated
|
|
882
|
+
|
|
883
|
+
Examples
|
|
884
|
+
--------
|
|
885
|
+
Update orientation:
|
|
886
|
+
>>> widget.update_czml_entity('photo1', {
|
|
887
|
+
... 'orientation': {'heading': 45, 'pitch': -30, 'roll': 0}
|
|
888
|
+
... })
|
|
889
|
+
|
|
890
|
+
Update position:
|
|
891
|
+
>>> widget.update_czml_entity('photo1', {
|
|
892
|
+
... 'position': {'latitude': 40.7128, 'longitude': -74.0060, 'altitude': 100}
|
|
893
|
+
... })
|
|
894
|
+
|
|
895
|
+
Update multiple properties:
|
|
896
|
+
>>> widget.update_czml_entity('photo1', {
|
|
897
|
+
... 'orientation': {'heading': 90, 'pitch': -15, 'roll': 5},
|
|
898
|
+
... 'position': {'latitude': 40.7128, 'longitude': -74.0060, 'altitude': 150}
|
|
899
|
+
... })
|
|
900
|
+
"""
|
|
901
|
+
import time
|
|
902
|
+
self.czml_entity_update = {
|
|
903
|
+
'entity_id': entity_id,
|
|
904
|
+
'properties': properties,
|
|
905
|
+
'timestamp': time.time()
|
|
906
|
+
}
|
|
907
|
+
|
|
673
908
|
def enable_measurement(self, mode: str = "distance"):
|
|
674
909
|
"""Enable a measurement tool.
|
|
675
910
|
|
|
@@ -774,6 +1009,59 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
774
1009
|
"""Hide the measurements list panel."""
|
|
775
1010
|
self.show_measurements_list = False
|
|
776
1011
|
|
|
1012
|
+
def enable_debug(self):
|
|
1013
|
+
"""Enable JavaScript console logging for debugging.
|
|
1014
|
+
|
|
1015
|
+
When enabled, detailed logs will be printed to the browser console
|
|
1016
|
+
showing widget initialization, data loading, camera events, etc.
|
|
1017
|
+
|
|
1018
|
+
Examples
|
|
1019
|
+
--------
|
|
1020
|
+
>>> widget.enable_debug() # Enable logging
|
|
1021
|
+
>>> # ... interact with widget, check browser console for logs
|
|
1022
|
+
>>> widget.disable_debug() # Disable logging when done
|
|
1023
|
+
"""
|
|
1024
|
+
self.debug_mode = True
|
|
1025
|
+
|
|
1026
|
+
def disable_debug(self):
|
|
1027
|
+
"""Disable JavaScript console logging.
|
|
1028
|
+
|
|
1029
|
+
Examples
|
|
1030
|
+
--------
|
|
1031
|
+
>>> widget.disable_debug()
|
|
1032
|
+
"""
|
|
1033
|
+
self.debug_mode = False
|
|
1034
|
+
|
|
1035
|
+
def enable_camera_sync(self):
|
|
1036
|
+
"""Enable camera synchronization callbacks.
|
|
1037
|
+
|
|
1038
|
+
When enabled, camera position changes in the Cesium viewer will be
|
|
1039
|
+
synchronized back to the Python model (latitude, longitude, altitude,
|
|
1040
|
+
heading, pitch, roll properties).
|
|
1041
|
+
|
|
1042
|
+
Note: This is disabled by default to avoid unnecessary updates when
|
|
1043
|
+
you don't need to track camera position in Python.
|
|
1044
|
+
|
|
1045
|
+
Examples
|
|
1046
|
+
--------
|
|
1047
|
+
>>> widget.enable_camera_sync()
|
|
1048
|
+
>>> # Move camera in the viewer...
|
|
1049
|
+
>>> print(widget.latitude, widget.longitude) # Updated values
|
|
1050
|
+
"""
|
|
1051
|
+
self.camera_sync_enabled = True
|
|
1052
|
+
|
|
1053
|
+
def disable_camera_sync(self):
|
|
1054
|
+
"""Disable camera synchronization callbacks.
|
|
1055
|
+
|
|
1056
|
+
When disabled, camera movements in the viewer will not update the
|
|
1057
|
+
Python model properties. This is the default state.
|
|
1058
|
+
|
|
1059
|
+
Examples
|
|
1060
|
+
--------
|
|
1061
|
+
>>> widget.disable_camera_sync()
|
|
1062
|
+
"""
|
|
1063
|
+
self.camera_sync_enabled = False
|
|
1064
|
+
|
|
777
1065
|
def set_atmosphere(self,
|
|
778
1066
|
brightness_shift=None,
|
|
779
1067
|
hue_shift=None,
|
|
@@ -1011,9 +1299,12 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
1011
1299
|
clicks, timeline scrubbing, etc.) with a dictionary containing:
|
|
1012
1300
|
|
|
1013
1301
|
- type: Interaction type ('camera_move', 'left_click', 'right_click', 'timeline_scrub')
|
|
1014
|
-
- timestamp: ISO 8601 timestamp when interaction occurred
|
|
1302
|
+
- timestamp: ISO 8601 timestamp when interaction occurred (system time)
|
|
1015
1303
|
- camera: Camera state (latitude, longitude, altitude, heading, pitch, roll)
|
|
1016
|
-
- clock:
|
|
1304
|
+
- clock: Cesium clock state with:
|
|
1305
|
+
- current_time: ISO 8601 timestamp from Cesium's clock (simulation time)
|
|
1306
|
+
- multiplier: Clock speed multiplier
|
|
1307
|
+
- is_animating: Whether clock is currently animating
|
|
1017
1308
|
- picked_position: Coordinates of clicked location (if applicable)
|
|
1018
1309
|
- picked_entity: Information about clicked entity (if applicable)
|
|
1019
1310
|
|
|
@@ -1027,6 +1318,8 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
1027
1318
|
>>> def handle_interaction(event):
|
|
1028
1319
|
... print(f"Interaction: {event['type']}")
|
|
1029
1320
|
... print(f"Camera at: {event['camera']['latitude']}, {event['camera']['longitude']}")
|
|
1321
|
+
... if event['clock']:
|
|
1322
|
+
... print(f"Cesium time: {event['clock']['current_time']}")
|
|
1030
1323
|
... if 'picked_position' in event:
|
|
1031
1324
|
... print(f"Clicked: {event['picked_position']}")
|
|
1032
1325
|
>>>
|
|
@@ -1045,43 +1338,324 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
1045
1338
|
|
|
1046
1339
|
This is useful for troubleshooting widget initialization issues.
|
|
1047
1340
|
"""
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1341
|
+
def emit(message: str, *args):
|
|
1342
|
+
line = message % args if args else message
|
|
1343
|
+
print(line)
|
|
1344
|
+
logger.info(message, *args)
|
|
1345
|
+
|
|
1346
|
+
emit("=== CesiumWidget Debug Info ===")
|
|
1347
|
+
emit("Widget class: %s", self.__class__.__name__)
|
|
1348
|
+
emit("Anywidget version: %s", anywidget.__version__)
|
|
1051
1349
|
|
|
1052
1350
|
# Check file paths (note: after widget instantiation, _esm and _css contain file contents)
|
|
1053
1351
|
esm_path = pathlib.Path(__file__).parent / "index.js"
|
|
1054
1352
|
css_path = pathlib.Path(__file__).parent / "styles.css"
|
|
1055
1353
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1354
|
+
emit("JavaScript file:")
|
|
1355
|
+
emit(" Path: %s", esm_path)
|
|
1356
|
+
emit(" Exists: %s", esm_path.exists())
|
|
1059
1357
|
if esm_path.exists():
|
|
1060
|
-
|
|
1358
|
+
emit(" Size: %d bytes", esm_path.stat().st_size)
|
|
1061
1359
|
elif isinstance(self._esm, str):
|
|
1062
|
-
|
|
1360
|
+
emit(" Content loaded: %d chars", len(self._esm))
|
|
1063
1361
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1362
|
+
emit("CSS file:")
|
|
1363
|
+
emit(" Path: %s", css_path)
|
|
1364
|
+
emit(" Exists: %s", css_path.exists())
|
|
1067
1365
|
if css_path.exists():
|
|
1068
|
-
|
|
1366
|
+
emit(" Size: %d bytes", css_path.stat().st_size)
|
|
1069
1367
|
elif isinstance(self._css, str):
|
|
1070
|
-
|
|
1368
|
+
emit(" Content loaded: %d chars", len(self._css))
|
|
1071
1369
|
|
|
1072
1370
|
# Show current state
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
)
|
|
1086
|
-
|
|
1087
|
-
|
|
1371
|
+
emit("Current state:")
|
|
1372
|
+
emit(" Position: (%.4f°, %.4f°)", self.latitude, self.longitude)
|
|
1373
|
+
emit(" Altitude: %.2fm", self.altitude)
|
|
1374
|
+
emit(" Height: %s", self.height)
|
|
1375
|
+
emit(" Terrain: %s", self.enable_terrain)
|
|
1376
|
+
emit(" Lighting: %s", self.enable_lighting)
|
|
1377
|
+
|
|
1378
|
+
emit("💡 Debugging tips:")
|
|
1379
|
+
emit(" 1. Open browser DevTools (F12) and check the Console tab for errors")
|
|
1380
|
+
emit(" 2. Check Network tab to see if CesiumJS CDN loads successfully")
|
|
1381
|
+
emit(" 3. Try: widget = CesiumWidget(enable_terrain=False) to avoid async terrain loading")
|
|
1382
|
+
emit(" 4. Ensure you're using JupyterLab 4.0+ or Jupyter Notebook 7.0+")
|
|
1383
|
+
emit(" 5. Check if anywidget is properly installed: pip show anywidget")
|
|
1384
|
+
|
|
1385
|
+
# ============= Point Picking Methods for Camera Calibration =============
|
|
1386
|
+
|
|
1387
|
+
def start_point_picking(
|
|
1388
|
+
self,
|
|
1389
|
+
color: tuple = (255, 0, 0, 255),
|
|
1390
|
+
label_prefix: str = "GCP",
|
|
1391
|
+
point_id_prefix: str = "",
|
|
1392
|
+
continuous: bool = True,
|
|
1393
|
+
):
|
|
1394
|
+
"""Start point picking mode for camera calibration.
|
|
1395
|
+
|
|
1396
|
+
When enabled, clicking on the 3D scene will record the 3D coordinates
|
|
1397
|
+
of the clicked point. Points are added to the `picked_points` list.
|
|
1398
|
+
|
|
1399
|
+
Parameters
|
|
1400
|
+
----------
|
|
1401
|
+
color : tuple, optional
|
|
1402
|
+
RGBA color for the point marker (0-255 each). Default is red.
|
|
1403
|
+
label_prefix : str, optional
|
|
1404
|
+
Prefix for auto-generated labels (e.g., "GCP" -> "GCP_1", "GCP_2", ...)
|
|
1405
|
+
Default is "GCP".
|
|
1406
|
+
point_id_prefix : str, optional
|
|
1407
|
+
Prefix for auto-generated point IDs. If empty, uses label_prefix.
|
|
1408
|
+
continuous : bool, optional
|
|
1409
|
+
If True (default), allows picking multiple points with auto-incrementing IDs.
|
|
1410
|
+
If False, picks only one point and then stops.
|
|
1411
|
+
|
|
1412
|
+
Examples
|
|
1413
|
+
--------
|
|
1414
|
+
Pick multiple points (default):
|
|
1415
|
+
>>> widget.start_point_picking(label_prefix="GCP")
|
|
1416
|
+
>>> # Click multiple times on the scene to pick points
|
|
1417
|
+
>>> # Points will be labeled GCP_1, GCP_2, GCP_3, etc.
|
|
1418
|
+
>>> widget.stop_point_picking()
|
|
1419
|
+
>>> print(widget.picked_points)
|
|
1420
|
+
|
|
1421
|
+
Pick a single point:
|
|
1422
|
+
>>> widget.start_point_picking(label_prefix="P1", continuous=False)
|
|
1423
|
+
>>> # Click once - picking stops automatically
|
|
1424
|
+
"""
|
|
1425
|
+
import time
|
|
1426
|
+
|
|
1427
|
+
if not point_id_prefix:
|
|
1428
|
+
point_id_prefix = label_prefix
|
|
1429
|
+
|
|
1430
|
+
self.point_picking_config = {
|
|
1431
|
+
'color': list(color),
|
|
1432
|
+
'label_prefix': label_prefix,
|
|
1433
|
+
'point_id_prefix': point_id_prefix,
|
|
1434
|
+
'continuous': continuous,
|
|
1435
|
+
'timestamp': time.time(),
|
|
1436
|
+
}
|
|
1437
|
+
self.point_picking_mode = True
|
|
1438
|
+
|
|
1439
|
+
mode_desc = "continuous" if continuous else "single-point"
|
|
1440
|
+
logger.info("🎯 Point picking mode enabled (%s). Click on the 3D scene to pick points.", mode_desc)
|
|
1441
|
+
logger.info(" Points will be labeled: %s_1, %s_2, ...", label_prefix, label_prefix)
|
|
1442
|
+
logger.info(" Call widget.stop_point_picking() when done.")
|
|
1443
|
+
|
|
1444
|
+
def stop_point_picking(self):
|
|
1445
|
+
"""Stop point picking mode.
|
|
1446
|
+
|
|
1447
|
+
Examples
|
|
1448
|
+
--------
|
|
1449
|
+
>>> widget.stop_point_picking()
|
|
1450
|
+
"""
|
|
1451
|
+
self.point_picking_mode = False
|
|
1452
|
+
self.point_picking_config = {}
|
|
1453
|
+
logger.info("🛑 Point picking mode disabled.")
|
|
1454
|
+
|
|
1455
|
+
def clear_picked_points(self):
|
|
1456
|
+
"""Clear all picked points.
|
|
1457
|
+
|
|
1458
|
+
This removes all picked points from the list and their visual markers
|
|
1459
|
+
from the 3D scene.
|
|
1460
|
+
|
|
1461
|
+
Examples
|
|
1462
|
+
--------
|
|
1463
|
+
>>> widget.clear_picked_points()
|
|
1464
|
+
"""
|
|
1465
|
+
self.picked_points = []
|
|
1466
|
+
logger.info("🗑️ Cleared all picked points.")
|
|
1467
|
+
|
|
1468
|
+
def remove_picked_point(self, point_id: str):
|
|
1469
|
+
"""Remove a specific picked point by its ID.
|
|
1470
|
+
|
|
1471
|
+
Parameters
|
|
1472
|
+
----------
|
|
1473
|
+
point_id : str
|
|
1474
|
+
The ID of the point to remove
|
|
1475
|
+
|
|
1476
|
+
Examples
|
|
1477
|
+
--------
|
|
1478
|
+
>>> widget.remove_picked_point("P1")
|
|
1479
|
+
"""
|
|
1480
|
+
current_points = list(self.picked_points)
|
|
1481
|
+
self.picked_points = [p for p in current_points if p.get('id') != point_id]
|
|
1482
|
+
logger.info("🗑️ Removed point %s.", point_id)
|
|
1483
|
+
|
|
1484
|
+
def on_point_picked(self, callback):
|
|
1485
|
+
"""Register a callback for point picking events.
|
|
1486
|
+
|
|
1487
|
+
The callback will be called each time a point is picked in the 3D scene.
|
|
1488
|
+
|
|
1489
|
+
Parameters
|
|
1490
|
+
----------
|
|
1491
|
+
callback : callable
|
|
1492
|
+
Function to call with picked point data:
|
|
1493
|
+
callback({'id': str, 'latitude': float, 'longitude': float,
|
|
1494
|
+
'altitude_wgs84': float, 'altitude_msl': float,
|
|
1495
|
+
'color': list, 'label': str})
|
|
1496
|
+
|
|
1497
|
+
Returns
|
|
1498
|
+
-------
|
|
1499
|
+
callable
|
|
1500
|
+
The wrapper function (can be used to unobserve)
|
|
1501
|
+
|
|
1502
|
+
Examples
|
|
1503
|
+
--------
|
|
1504
|
+
>>> def handle_pick(point):
|
|
1505
|
+
... print(f"Picked {point['id']} at {point['latitude']}, {point['longitude']}")
|
|
1506
|
+
>>>
|
|
1507
|
+
>>> widget.on_point_picked(handle_pick)
|
|
1508
|
+
"""
|
|
1509
|
+
def wrapper(change):
|
|
1510
|
+
event_data = change['new']
|
|
1511
|
+
if event_data and event_data.get('id'):
|
|
1512
|
+
callback(event_data)
|
|
1513
|
+
|
|
1514
|
+
self.observe(wrapper, names='point_picking_event')
|
|
1515
|
+
return wrapper
|
|
1516
|
+
|
|
1517
|
+
# ============= Clock/Timeline Control Methods =============
|
|
1518
|
+
|
|
1519
|
+
def set_time(self, time):
|
|
1520
|
+
"""Set the current time on the Cesium timeline.
|
|
1521
|
+
|
|
1522
|
+
Parameters
|
|
1523
|
+
----------
|
|
1524
|
+
time : str or datetime
|
|
1525
|
+
Time to set. Can be:
|
|
1526
|
+
- ISO 8601 string (e.g., '2023-08-01T14:30:00Z')
|
|
1527
|
+
- datetime object (will be converted to ISO 8601)
|
|
1528
|
+
|
|
1529
|
+
Examples
|
|
1530
|
+
--------
|
|
1531
|
+
>>> from datetime import datetime
|
|
1532
|
+
>>> widget.set_time('2023-08-01T14:30:00Z')
|
|
1533
|
+
>>> widget.set_time(datetime(2023, 8, 1, 14, 30))
|
|
1534
|
+
"""
|
|
1535
|
+
from datetime import datetime as dt
|
|
1536
|
+
|
|
1537
|
+
if isinstance(time, dt):
|
|
1538
|
+
time_str = time.isoformat() + 'Z'
|
|
1539
|
+
else:
|
|
1540
|
+
time_str = str(time)
|
|
1541
|
+
|
|
1542
|
+
self.current_time = time_str
|
|
1543
|
+
self.clock_command = {
|
|
1544
|
+
'command': 'setTime',
|
|
1545
|
+
'time': time_str,
|
|
1546
|
+
'timestamp': __import__('time').time()
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
def play(self):
|
|
1550
|
+
"""Start the timeline animation.
|
|
1551
|
+
|
|
1552
|
+
Examples
|
|
1553
|
+
--------
|
|
1554
|
+
>>> widget.play()
|
|
1555
|
+
"""
|
|
1556
|
+
self.should_animate = True
|
|
1557
|
+
self.clock_command = {
|
|
1558
|
+
'command': 'play',
|
|
1559
|
+
'timestamp': __import__('time').time()
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
def pause(self):
|
|
1563
|
+
"""Pause the timeline animation.
|
|
1564
|
+
|
|
1565
|
+
Examples
|
|
1566
|
+
--------
|
|
1567
|
+
>>> widget.pause()
|
|
1568
|
+
"""
|
|
1569
|
+
self.should_animate = False
|
|
1570
|
+
self.clock_command = {
|
|
1571
|
+
'command': 'pause',
|
|
1572
|
+
'timestamp': __import__('time').time()
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
def set_speed(self, multiplier: float = 1.0):
|
|
1576
|
+
"""Set the animation speed multiplier.
|
|
1577
|
+
|
|
1578
|
+
Parameters
|
|
1579
|
+
----------
|
|
1580
|
+
multiplier : float, optional
|
|
1581
|
+
Speed multiplier (default: 1.0)
|
|
1582
|
+
- 1.0 = real-time
|
|
1583
|
+
- 60.0 = 60x speed (1 second = 1 minute)
|
|
1584
|
+
- 0.5 = half speed
|
|
1585
|
+
|
|
1586
|
+
Examples
|
|
1587
|
+
--------
|
|
1588
|
+
>>> widget.set_speed(60) # 60x speed
|
|
1589
|
+
>>> widget.set_speed(0.5) # Half speed
|
|
1590
|
+
"""
|
|
1591
|
+
self.clock_multiplier = multiplier
|
|
1592
|
+
self.clock_command = {
|
|
1593
|
+
'command': 'setMultiplier',
|
|
1594
|
+
'multiplier': multiplier,
|
|
1595
|
+
'timestamp': __import__('time').time()
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
def set_clock_range(self, start_time, stop_time):
|
|
1599
|
+
"""Set the time range for the clock/timeline.
|
|
1600
|
+
|
|
1601
|
+
Parameters
|
|
1602
|
+
----------
|
|
1603
|
+
start_time : str or datetime
|
|
1604
|
+
Start time of the range
|
|
1605
|
+
stop_time : str or datetime
|
|
1606
|
+
Stop time of the range
|
|
1607
|
+
|
|
1608
|
+
Examples
|
|
1609
|
+
--------
|
|
1610
|
+
>>> from datetime import datetime
|
|
1611
|
+
>>> widget.set_clock_range(
|
|
1612
|
+
... datetime(2023, 8, 1, 0, 0),
|
|
1613
|
+
... datetime(2023, 8, 2, 0, 0)
|
|
1614
|
+
... )
|
|
1615
|
+
"""
|
|
1616
|
+
from datetime import datetime as dt
|
|
1617
|
+
|
|
1618
|
+
if isinstance(start_time, dt):
|
|
1619
|
+
start_str = start_time.isoformat() + 'Z'
|
|
1620
|
+
else:
|
|
1621
|
+
start_str = str(start_time)
|
|
1622
|
+
|
|
1623
|
+
if isinstance(stop_time, dt):
|
|
1624
|
+
stop_str = stop_time.isoformat() + 'Z'
|
|
1625
|
+
else:
|
|
1626
|
+
stop_str = str(stop_time)
|
|
1627
|
+
|
|
1628
|
+
self.clock_command = {
|
|
1629
|
+
'command': 'setRange',
|
|
1630
|
+
'startTime': start_str,
|
|
1631
|
+
'stopTime': stop_str,
|
|
1632
|
+
'timestamp': __import__('time').time()
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
def get_picked_points_as_correspondences(self):
|
|
1636
|
+
"""Convert picked points to CorrespondencePoint objects.
|
|
1637
|
+
|
|
1638
|
+
This is useful for camera calibration workflows where you need
|
|
1639
|
+
to combine 2D photo points with 3D scene points.
|
|
1640
|
+
|
|
1641
|
+
Returns
|
|
1642
|
+
-------
|
|
1643
|
+
list
|
|
1644
|
+
List of dictionaries with point data ready for camera calibration
|
|
1645
|
+
|
|
1646
|
+
Examples
|
|
1647
|
+
--------
|
|
1648
|
+
>>> points_3d = widget.get_picked_points_as_correspondences()
|
|
1649
|
+
>>> # Combine with 2D points from photo selector
|
|
1650
|
+
"""
|
|
1651
|
+
return [
|
|
1652
|
+
{
|
|
1653
|
+
'id': p.get('id', f"point_{i}"),
|
|
1654
|
+
'latitude': p.get('latitude'),
|
|
1655
|
+
'longitude': p.get('longitude'),
|
|
1656
|
+
'altitude_msl': p.get('altitude_msl'),
|
|
1657
|
+
'color': tuple(p.get('color', [255, 0, 0, 255])),
|
|
1658
|
+
'description': p.get('label', ''),
|
|
1659
|
+
}
|
|
1660
|
+
for i, p in enumerate(self.picked_points)
|
|
1661
|
+
]
|