cesiumjs-anywidget 0.6.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 +1415 -490
- cesiumjs_anywidget/logger.py +73 -0
- cesiumjs_anywidget/styles.css +24 -1
- cesiumjs_anywidget/widget.py +552 -42
- {cesiumjs_anywidget-0.6.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.6.0.dist-info/RECORD +0 -8
- {cesiumjs_anywidget-0.6.0.dist-info → cesiumjs_anywidget-0.7.0.dist-info}/WHEEL +0 -0
- {cesiumjs_anywidget-0.6.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,10 @@ 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"
|
|
131
172
|
).tag(sync=True)
|
|
132
173
|
|
|
133
174
|
# Debug mode for JavaScript logging
|
|
@@ -141,6 +182,28 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
141
182
|
help="Enable or disable camera position synchronization callbacks"
|
|
142
183
|
).tag(sync=True)
|
|
143
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)"
|
|
205
|
+
).tag(sync=True)
|
|
206
|
+
|
|
144
207
|
def __init__(self, **kwargs):
|
|
145
208
|
"""Initialize the CesiumWidget.
|
|
146
209
|
|
|
@@ -152,14 +215,12 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
152
215
|
if env_token:
|
|
153
216
|
kwargs["ion_access_token"] = env_token
|
|
154
217
|
else:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
print(" - export CESIUM_ION_TOKEN='your_token' # in your shell")
|
|
162
|
-
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.")
|
|
163
224
|
|
|
164
225
|
super().__init__(**kwargs)
|
|
165
226
|
|
|
@@ -213,7 +274,8 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
213
274
|
|
|
214
275
|
def set_view(
|
|
215
276
|
self, latitude: float, longitude: float, altitude: float = 400,
|
|
216
|
-
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
|
|
217
279
|
):
|
|
218
280
|
"""Set the camera view instantly without animation.
|
|
219
281
|
|
|
@@ -231,11 +293,24 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
231
293
|
Camera pitch in degrees (default: -15.0)
|
|
232
294
|
roll : float, optional
|
|
233
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).
|
|
234
308
|
|
|
235
309
|
Examples
|
|
236
310
|
--------
|
|
237
311
|
>>> widget.set_view(48.8566, 2.3522, altitude=1000)
|
|
238
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)
|
|
239
314
|
"""
|
|
240
315
|
import time
|
|
241
316
|
# Update traitlets for state sync
|
|
@@ -246,7 +321,7 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
246
321
|
self.pitch = pitch
|
|
247
322
|
self.roll = roll
|
|
248
323
|
# Send command for instant view change
|
|
249
|
-
|
|
324
|
+
camera_cmd = {
|
|
250
325
|
'command': 'setView',
|
|
251
326
|
'latitude': latitude,
|
|
252
327
|
'longitude': longitude,
|
|
@@ -256,6 +331,16 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
256
331
|
'roll': roll,
|
|
257
332
|
'timestamp': time.time()
|
|
258
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
|
|
259
344
|
|
|
260
345
|
def look_at(self, target_latitude: float, target_longitude: float, target_altitude: float = 0,
|
|
261
346
|
offset_heading: float = 0.0, offset_pitch: float = -45.0, offset_range: float = 1000.0):
|
|
@@ -572,6 +657,104 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
572
657
|
if roll is not None:
|
|
573
658
|
self.roll = roll
|
|
574
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
|
+
|
|
575
758
|
def load_geojson(self, geojson, append=False):
|
|
576
759
|
"""Load GeoJSON data for visualization.
|
|
577
760
|
|
|
@@ -681,6 +864,47 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
681
864
|
"""
|
|
682
865
|
self.czml_data = []
|
|
683
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
|
+
|
|
684
908
|
def enable_measurement(self, mode: str = "distance"):
|
|
685
909
|
"""Enable a measurement tool.
|
|
686
910
|
|
|
@@ -1075,9 +1299,12 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
1075
1299
|
clicks, timeline scrubbing, etc.) with a dictionary containing:
|
|
1076
1300
|
|
|
1077
1301
|
- type: Interaction type ('camera_move', 'left_click', 'right_click', 'timeline_scrub')
|
|
1078
|
-
- timestamp: ISO 8601 timestamp when interaction occurred
|
|
1302
|
+
- timestamp: ISO 8601 timestamp when interaction occurred (system time)
|
|
1079
1303
|
- camera: Camera state (latitude, longitude, altitude, heading, pitch, roll)
|
|
1080
|
-
- 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
|
|
1081
1308
|
- picked_position: Coordinates of clicked location (if applicable)
|
|
1082
1309
|
- picked_entity: Information about clicked entity (if applicable)
|
|
1083
1310
|
|
|
@@ -1091,6 +1318,8 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
1091
1318
|
>>> def handle_interaction(event):
|
|
1092
1319
|
... print(f"Interaction: {event['type']}")
|
|
1093
1320
|
... print(f"Camera at: {event['camera']['latitude']}, {event['camera']['longitude']}")
|
|
1321
|
+
... if event['clock']:
|
|
1322
|
+
... print(f"Cesium time: {event['clock']['current_time']}")
|
|
1094
1323
|
... if 'picked_position' in event:
|
|
1095
1324
|
... print(f"Clicked: {event['picked_position']}")
|
|
1096
1325
|
>>>
|
|
@@ -1109,43 +1338,324 @@ class CesiumWidget(anywidget.AnyWidget):
|
|
|
1109
1338
|
|
|
1110
1339
|
This is useful for troubleshooting widget initialization issues.
|
|
1111
1340
|
"""
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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__)
|
|
1115
1349
|
|
|
1116
1350
|
# Check file paths (note: after widget instantiation, _esm and _css contain file contents)
|
|
1117
1351
|
esm_path = pathlib.Path(__file__).parent / "index.js"
|
|
1118
1352
|
css_path = pathlib.Path(__file__).parent / "styles.css"
|
|
1119
1353
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1354
|
+
emit("JavaScript file:")
|
|
1355
|
+
emit(" Path: %s", esm_path)
|
|
1356
|
+
emit(" Exists: %s", esm_path.exists())
|
|
1123
1357
|
if esm_path.exists():
|
|
1124
|
-
|
|
1358
|
+
emit(" Size: %d bytes", esm_path.stat().st_size)
|
|
1125
1359
|
elif isinstance(self._esm, str):
|
|
1126
|
-
|
|
1360
|
+
emit(" Content loaded: %d chars", len(self._esm))
|
|
1127
1361
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1362
|
+
emit("CSS file:")
|
|
1363
|
+
emit(" Path: %s", css_path)
|
|
1364
|
+
emit(" Exists: %s", css_path.exists())
|
|
1131
1365
|
if css_path.exists():
|
|
1132
|
-
|
|
1366
|
+
emit(" Size: %d bytes", css_path.stat().st_size)
|
|
1133
1367
|
elif isinstance(self._css, str):
|
|
1134
|
-
|
|
1368
|
+
emit(" Content loaded: %d chars", len(self._css))
|
|
1135
1369
|
|
|
1136
1370
|
# Show current state
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
)
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
+
]
|