cesiumjs-anywidget 0.2.4__py3-none-any.whl → 0.3.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.
@@ -85,64 +85,318 @@ function setupViewerListeners(viewer, model, container, Cesium) {
85
85
  return;
86
86
  viewer.animation.container.style.visibility = model.get("show_animation") ? "visible" : "hidden";
87
87
  });
88
+ model.on("change:atmosphere_settings", () => {
89
+ if (!viewer || !viewer.scene || !viewer.scene.atmosphere)
90
+ return;
91
+ const settings = model.get("atmosphere_settings");
92
+ if (!settings || Object.keys(settings).length === 0)
93
+ return;
94
+ const atmosphere = viewer.scene.atmosphere;
95
+ if (settings.brightnessShift !== void 0) {
96
+ atmosphere.brightnessShift = settings.brightnessShift;
97
+ }
98
+ if (settings.hueShift !== void 0) {
99
+ atmosphere.hueShift = settings.hueShift;
100
+ }
101
+ if (settings.saturationShift !== void 0) {
102
+ atmosphere.saturationShift = settings.saturationShift;
103
+ }
104
+ if (settings.lightIntensity !== void 0) {
105
+ atmosphere.lightIntensity = settings.lightIntensity;
106
+ }
107
+ if (settings.rayleighCoefficient !== void 0 && Array.isArray(settings.rayleighCoefficient) && settings.rayleighCoefficient.length === 3) {
108
+ atmosphere.rayleighCoefficient = new Cesium.Cartesian3(
109
+ settings.rayleighCoefficient[0],
110
+ settings.rayleighCoefficient[1],
111
+ settings.rayleighCoefficient[2]
112
+ );
113
+ }
114
+ if (settings.rayleighScaleHeight !== void 0) {
115
+ atmosphere.rayleighScaleHeight = settings.rayleighScaleHeight;
116
+ }
117
+ if (settings.mieCoefficient !== void 0 && Array.isArray(settings.mieCoefficient) && settings.mieCoefficient.length === 3) {
118
+ atmosphere.mieCoefficient = new Cesium.Cartesian3(
119
+ settings.mieCoefficient[0],
120
+ settings.mieCoefficient[1],
121
+ settings.mieCoefficient[2]
122
+ );
123
+ }
124
+ if (settings.mieScaleHeight !== void 0) {
125
+ atmosphere.mieScaleHeight = settings.mieScaleHeight;
126
+ }
127
+ if (settings.mieAnisotropy !== void 0) {
128
+ atmosphere.mieAnisotropy = settings.mieAnisotropy;
129
+ }
130
+ });
131
+ model.on("change:sky_atmosphere_settings", () => {
132
+ if (!viewer || !viewer.scene || !viewer.scene.skyAtmosphere)
133
+ return;
134
+ const settings = model.get("sky_atmosphere_settings");
135
+ if (!settings || Object.keys(settings).length === 0)
136
+ return;
137
+ const skyAtmosphere = viewer.scene.skyAtmosphere;
138
+ if (settings.show !== void 0) {
139
+ skyAtmosphere.show = settings.show;
140
+ }
141
+ if (settings.brightnessShift !== void 0) {
142
+ skyAtmosphere.brightnessShift = settings.brightnessShift;
143
+ }
144
+ if (settings.hueShift !== void 0) {
145
+ skyAtmosphere.hueShift = settings.hueShift;
146
+ }
147
+ if (settings.saturationShift !== void 0) {
148
+ skyAtmosphere.saturationShift = settings.saturationShift;
149
+ }
150
+ if (settings.atmosphereLightIntensity !== void 0) {
151
+ skyAtmosphere.atmosphereLightIntensity = settings.atmosphereLightIntensity;
152
+ }
153
+ if (settings.atmosphereRayleighCoefficient !== void 0 && Array.isArray(settings.atmosphereRayleighCoefficient) && settings.atmosphereRayleighCoefficient.length === 3) {
154
+ skyAtmosphere.atmosphereRayleighCoefficient = new Cesium.Cartesian3(
155
+ settings.atmosphereRayleighCoefficient[0],
156
+ settings.atmosphereRayleighCoefficient[1],
157
+ settings.atmosphereRayleighCoefficient[2]
158
+ );
159
+ }
160
+ if (settings.atmosphereRayleighScaleHeight !== void 0) {
161
+ skyAtmosphere.atmosphereRayleighScaleHeight = settings.atmosphereRayleighScaleHeight;
162
+ }
163
+ if (settings.atmosphereMieCoefficient !== void 0 && Array.isArray(settings.atmosphereMieCoefficient) && settings.atmosphereMieCoefficient.length === 3) {
164
+ skyAtmosphere.atmosphereMieCoefficient = new Cesium.Cartesian3(
165
+ settings.atmosphereMieCoefficient[0],
166
+ settings.atmosphereMieCoefficient[1],
167
+ settings.atmosphereMieCoefficient[2]
168
+ );
169
+ }
170
+ if (settings.atmosphereMieScaleHeight !== void 0) {
171
+ skyAtmosphere.atmosphereMieScaleHeight = settings.atmosphereMieScaleHeight;
172
+ }
173
+ if (settings.atmosphereMieAnisotropy !== void 0) {
174
+ skyAtmosphere.atmosphereMieAnisotropy = settings.atmosphereMieAnisotropy;
175
+ }
176
+ if (settings.perFragmentAtmosphere !== void 0) {
177
+ skyAtmosphere.perFragmentAtmosphere = settings.perFragmentAtmosphere;
178
+ }
179
+ });
180
+ model.on("change:skybox_settings", () => {
181
+ if (!viewer || !viewer.scene || !viewer.scene.skyBox)
182
+ return;
183
+ const settings = model.get("skybox_settings");
184
+ if (!settings || Object.keys(settings).length === 0)
185
+ return;
186
+ const skyBox = viewer.scene.skyBox;
187
+ if (settings.show !== void 0) {
188
+ skyBox.show = settings.show;
189
+ }
190
+ if (settings.sources !== void 0 && settings.sources !== null) {
191
+ const sources = settings.sources;
192
+ if (sources.positiveX && sources.negativeX && sources.positiveY && sources.negativeY && sources.positiveZ && sources.negativeZ) {
193
+ viewer.scene.skyBox = new Cesium.SkyBox({
194
+ sources: {
195
+ positiveX: sources.positiveX,
196
+ negativeX: sources.negativeX,
197
+ positiveY: sources.positiveY,
198
+ negativeY: sources.negativeY,
199
+ positiveZ: sources.positiveZ,
200
+ negativeZ: sources.negativeZ
201
+ }
202
+ });
203
+ if (settings.show !== void 0) {
204
+ viewer.scene.skyBox.show = settings.show;
205
+ }
206
+ }
207
+ }
208
+ });
209
+ function getCameraState() {
210
+ const cartographic = viewer.camera.positionCartographic;
211
+ return {
212
+ latitude: Cesium.Math.toDegrees(cartographic.latitude),
213
+ longitude: Cesium.Math.toDegrees(cartographic.longitude),
214
+ altitude: cartographic.height,
215
+ heading: Cesium.Math.toDegrees(viewer.camera.heading),
216
+ pitch: Cesium.Math.toDegrees(viewer.camera.pitch),
217
+ roll: Cesium.Math.toDegrees(viewer.camera.roll)
218
+ };
219
+ }
220
+ function getClockState() {
221
+ if (!viewer.clock)
222
+ return null;
223
+ return {
224
+ current_time: Cesium.JulianDate.toIso8601(viewer.clock.currentTime),
225
+ multiplier: viewer.clock.multiplier,
226
+ is_animating: viewer.clock.shouldAnimate
227
+ };
228
+ }
229
+ function sendInteractionEvent(type, additionalData = {}) {
230
+ const event = {
231
+ type,
232
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
233
+ camera: getCameraState(),
234
+ clock: getClockState(),
235
+ ...additionalData
236
+ };
237
+ console.log("[CesiumWidget] Interaction event:", type, event);
238
+ model.set("interaction_event", event);
239
+ model.save_changes();
240
+ }
241
+ const camera = viewer.camera;
242
+ camera.moveEnd.addEventListener(() => {
243
+ sendInteractionEvent("camera_move");
244
+ });
245
+ const scene = viewer.scene;
246
+ const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
247
+ handler.setInputAction((click) => {
248
+ const pickedData = {};
249
+ const ray = viewer.camera.getPickRay(click.position);
250
+ const cartesian = viewer.scene.globe.pick(ray, viewer.scene);
251
+ if (cartesian) {
252
+ const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
253
+ pickedData.picked_position = {
254
+ latitude: Cesium.Math.toDegrees(cartographic.latitude),
255
+ longitude: Cesium.Math.toDegrees(cartographic.longitude),
256
+ altitude: cartographic.height
257
+ };
258
+ }
259
+ const pickedObject = viewer.scene.pick(click.position);
260
+ if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
261
+ const entity = pickedObject.id;
262
+ pickedData.picked_entity = {
263
+ id: entity.id,
264
+ name: entity.name || null
265
+ };
266
+ if (entity.properties) {
267
+ const props = {};
268
+ const propertyNames = entity.properties.propertyNames;
269
+ if (propertyNames && propertyNames.length > 0) {
270
+ propertyNames.forEach((name) => {
271
+ try {
272
+ props[name] = entity.properties[name].getValue(viewer.clock.currentTime);
273
+ } catch (e) {
274
+ }
275
+ });
276
+ if (Object.keys(props).length > 0) {
277
+ pickedData.picked_entity.properties = props;
278
+ }
279
+ }
280
+ }
281
+ }
282
+ sendInteractionEvent("left_click", pickedData);
283
+ }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
284
+ handler.setInputAction((click) => {
285
+ const pickedData = {};
286
+ const ray = viewer.camera.getPickRay(click.position);
287
+ const cartesian = viewer.scene.globe.pick(ray, viewer.scene);
288
+ if (cartesian) {
289
+ const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
290
+ pickedData.picked_position = {
291
+ latitude: Cesium.Math.toDegrees(cartographic.latitude),
292
+ longitude: Cesium.Math.toDegrees(cartographic.longitude),
293
+ altitude: cartographic.height
294
+ };
295
+ }
296
+ sendInteractionEvent("right_click", pickedData);
297
+ }, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
298
+ if (viewer.timeline) {
299
+ let timelineScrubbing = false;
300
+ let scrubTimeout = null;
301
+ viewer.clock.onTick.addEventListener(() => {
302
+ if (viewer.timeline) {
303
+ if (scrubTimeout) {
304
+ clearTimeout(scrubTimeout);
305
+ }
306
+ scrubTimeout = setTimeout(() => {
307
+ if (timelineScrubbing) {
308
+ timelineScrubbing = false;
309
+ sendInteractionEvent("timeline_scrub");
310
+ }
311
+ }, 500);
312
+ timelineScrubbing = true;
313
+ }
314
+ });
315
+ }
88
316
  }
89
317
  function setupGeoJSONLoader(viewer, model, Cesium) {
90
- let geojsonDataSource = null;
318
+ let geojsonDataSources = [];
91
319
  model.on("change:geojson_data", async () => {
92
- if (!viewer)
320
+ if (!viewer || !viewer.dataSources)
93
321
  return;
94
- const geojsonData = model.get("geojson_data");
95
- if (geojsonDataSource) {
96
- viewer.dataSources.remove(geojsonDataSource);
97
- geojsonDataSource = null;
98
- }
99
- if (geojsonData) {
100
- try {
101
- geojsonDataSource = await Cesium.GeoJsonDataSource.load(geojsonData, {
102
- stroke: Cesium.Color.HOTPINK,
103
- fill: Cesium.Color.PINK.withAlpha(0.5),
104
- strokeWidth: 3
105
- });
106
- viewer.dataSources.add(geojsonDataSource);
107
- viewer.flyTo(geojsonDataSource);
108
- } catch (error) {
109
- console.error("Error loading GeoJSON:", error);
322
+ const geojsonDataArray = model.get("geojson_data");
323
+ geojsonDataSources.forEach((dataSource) => {
324
+ if (viewer && viewer.dataSources) {
325
+ viewer.dataSources.remove(dataSource);
326
+ }
327
+ });
328
+ geojsonDataSources = [];
329
+ if (geojsonDataArray && Array.isArray(geojsonDataArray)) {
330
+ for (const geojsonData of geojsonDataArray) {
331
+ try {
332
+ const dataSource = await Cesium.GeoJsonDataSource.load(geojsonData, {
333
+ stroke: Cesium.Color.HOTPINK,
334
+ fill: Cesium.Color.PINK.withAlpha(0.5),
335
+ strokeWidth: 3
336
+ });
337
+ if (viewer && viewer.dataSources) {
338
+ viewer.dataSources.add(dataSource);
339
+ geojsonDataSources.push(dataSource);
340
+ }
341
+ } catch (error) {
342
+ console.error("Error loading GeoJSON:", error);
343
+ }
344
+ }
345
+ if (geojsonDataSources.length > 0 && viewer && viewer.flyTo) {
346
+ viewer.flyTo(geojsonDataSources[0]);
110
347
  }
111
348
  }
112
349
  });
113
350
  return {
114
351
  destroy: () => {
115
- if (geojsonDataSource && viewer) {
116
- viewer.dataSources.remove(geojsonDataSource);
117
- }
352
+ geojsonDataSources.forEach((dataSource) => {
353
+ if (viewer) {
354
+ viewer.dataSources.remove(dataSource);
355
+ }
356
+ });
357
+ geojsonDataSources = [];
118
358
  }
119
359
  };
120
360
  }
121
361
  function setupCZMLLoader(viewer, model, Cesium) {
122
- let czmlDataSource = null;
362
+ let czmlDataSources = [];
123
363
  model.on("change:czml_data", async () => {
124
- if (!viewer)
364
+ if (!viewer || !viewer.dataSources)
125
365
  return;
126
- const czmlData = model.get("czml_data");
127
- if (czmlDataSource) {
128
- viewer.dataSources.remove(czmlDataSource);
129
- czmlDataSource = null;
130
- }
131
- if (czmlData && Array.isArray(czmlData) && czmlData.length > 0) {
132
- try {
133
- czmlDataSource = await Cesium.CzmlDataSource.load(czmlData);
134
- viewer.dataSources.add(czmlDataSource);
135
- viewer.flyTo(czmlDataSource);
136
- } catch (error) {
137
- console.error("Error loading CZML:", error);
366
+ const czmlDataArray = model.get("czml_data");
367
+ czmlDataSources.forEach((dataSource) => {
368
+ if (viewer && viewer.dataSources) {
369
+ viewer.dataSources.remove(dataSource);
370
+ }
371
+ });
372
+ czmlDataSources = [];
373
+ if (czmlDataArray && Array.isArray(czmlDataArray)) {
374
+ for (const czmlData of czmlDataArray) {
375
+ if (Array.isArray(czmlData) && czmlData.length > 0) {
376
+ try {
377
+ const dataSource = await Cesium.CzmlDataSource.load(czmlData);
378
+ if (viewer && viewer.dataSources) {
379
+ viewer.dataSources.add(dataSource);
380
+ czmlDataSources.push(dataSource);
381
+ }
382
+ } catch (error) {
383
+ console.error("Error loading CZML:", error);
384
+ }
385
+ }
386
+ }
387
+ if (czmlDataSources.length > 0 && viewer && viewer.flyTo) {
388
+ viewer.flyTo(czmlDataSources[0]);
138
389
  }
139
390
  }
140
391
  });
141
392
  return {
142
393
  destroy: () => {
143
- if (czmlDataSource && viewer) {
144
- viewer.dataSources.remove(czmlDataSource);
145
- }
394
+ czmlDataSources.forEach((dataSource) => {
395
+ if (viewer) {
396
+ viewer.dataSources.remove(dataSource);
397
+ }
398
+ });
399
+ czmlDataSources = [];
146
400
  }
147
401
  };
148
402
  }
@@ -194,6 +448,99 @@ function initializeCameraSync(viewer, model) {
194
448
  model.on("change:heading", updateCameraFromModel);
195
449
  model.on("change:pitch", updateCameraFromModel);
196
450
  model.on("change:roll", updateCameraFromModel);
451
+ model.on("change:camera_command", () => {
452
+ const command = model.get("camera_command");
453
+ if (!command || !command.command || !command.timestamp)
454
+ return;
455
+ const cmd = command.command;
456
+ try {
457
+ switch (cmd) {
458
+ case "flyTo":
459
+ viewer.camera.flyTo({
460
+ destination: Cesium.Cartesian3.fromDegrees(
461
+ command.longitude,
462
+ command.latitude,
463
+ command.altitude
464
+ ),
465
+ orientation: {
466
+ heading: Cesium.Math.toRadians(command.heading || 0),
467
+ pitch: Cesium.Math.toRadians(command.pitch || -15),
468
+ roll: Cesium.Math.toRadians(command.roll || 0)
469
+ },
470
+ duration: command.duration || 3
471
+ });
472
+ break;
473
+ case "setView":
474
+ viewer.camera.setView({
475
+ destination: Cesium.Cartesian3.fromDegrees(
476
+ command.longitude,
477
+ command.latitude,
478
+ command.altitude
479
+ ),
480
+ orientation: {
481
+ heading: Cesium.Math.toRadians(command.heading || 0),
482
+ pitch: Cesium.Math.toRadians(command.pitch || -15),
483
+ roll: Cesium.Math.toRadians(command.roll || 0)
484
+ }
485
+ });
486
+ break;
487
+ case "lookAt":
488
+ const target = Cesium.Cartesian3.fromDegrees(
489
+ command.targetLongitude,
490
+ command.targetLatitude,
491
+ command.targetAltitude || 0
492
+ );
493
+ const offset = new Cesium.HeadingPitchRange(
494
+ Cesium.Math.toRadians(command.offsetHeading || 0),
495
+ Cesium.Math.toRadians(command.offsetPitch || -45),
496
+ command.offsetRange || 1e3
497
+ );
498
+ viewer.camera.lookAt(target, offset);
499
+ viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
500
+ break;
501
+ case "moveForward":
502
+ viewer.camera.moveForward(command.distance || 100);
503
+ break;
504
+ case "moveBackward":
505
+ viewer.camera.moveBackward(command.distance || 100);
506
+ break;
507
+ case "moveUp":
508
+ viewer.camera.moveUp(command.distance || 100);
509
+ break;
510
+ case "moveDown":
511
+ viewer.camera.moveDown(command.distance || 100);
512
+ break;
513
+ case "moveLeft":
514
+ viewer.camera.moveLeft(command.distance || 100);
515
+ break;
516
+ case "moveRight":
517
+ viewer.camera.moveRight(command.distance || 100);
518
+ break;
519
+ case "rotateLeft":
520
+ viewer.camera.rotateLeft(Cesium.Math.toRadians(command.angle || 15));
521
+ break;
522
+ case "rotateRight":
523
+ viewer.camera.rotateRight(Cesium.Math.toRadians(command.angle || 15));
524
+ break;
525
+ case "rotateUp":
526
+ viewer.camera.rotateUp(Cesium.Math.toRadians(command.angle || 15));
527
+ break;
528
+ case "rotateDown":
529
+ viewer.camera.rotateDown(Cesium.Math.toRadians(command.angle || 15));
530
+ break;
531
+ case "zoomIn":
532
+ viewer.camera.zoomIn(command.distance || 100);
533
+ break;
534
+ case "zoomOut":
535
+ viewer.camera.zoomOut(command.distance || 100);
536
+ break;
537
+ default:
538
+ console.warn(`Unknown camera command: ${cmd}`);
539
+ }
540
+ } catch (error) {
541
+ console.error(`Error executing camera command ${cmd}:`, error);
542
+ }
543
+ });
197
544
  return {
198
545
  updateCameraFromModel,
199
546
  updateModelFromCamera,
@@ -65,17 +65,48 @@ class CesiumWidget(anywidget.AnyWidget):
65
65
  sync=True
66
66
  )
67
67
 
68
- # GeoJSON data for visualization
69
- geojson_data = traitlets.Dict(
70
- default_value=None, allow_none=True, help="GeoJSON data to display"
68
+ # GeoJSON data for visualization (list of GeoJSON objects)
69
+ geojson_data = traitlets.List(
70
+ trait=traitlets.Dict(),
71
+ default_value=[],
72
+ help="List of GeoJSON datasets to display"
71
73
  ).tag(sync=True)
72
74
 
73
- # CZML data for visualization
75
+ # CZML data for visualization (list of CZML documents)
74
76
  czml_data = traitlets.List(
75
- trait=traitlets.Dict(),
76
- default_value=None,
77
- allow_none=True,
78
- help="CZML data to display",
77
+ trait=traitlets.List(trait=traitlets.Dict()),
78
+ default_value=[],
79
+ help="List of CZML documents to display",
80
+ ).tag(sync=True)
81
+
82
+ # Interaction event data - sent when user interaction ends
83
+ interaction_event = traitlets.Dict(
84
+ default_value={},
85
+ help="Interaction event data with camera position, time, and context"
86
+ ).tag(sync=True)
87
+
88
+ # Atmosphere configuration
89
+ atmosphere_settings = traitlets.Dict(
90
+ default_value={},
91
+ help="Atmosphere rendering settings (brightnessShift, hueShift, saturationShift, etc.)"
92
+ ).tag(sync=True)
93
+
94
+ # Sky atmosphere configuration
95
+ sky_atmosphere_settings = traitlets.Dict(
96
+ default_value={},
97
+ help="Sky atmosphere rendering settings (show, brightnessShift, hueShift, etc.)"
98
+ ).tag(sync=True)
99
+
100
+ # SkyBox configuration
101
+ skybox_settings = traitlets.Dict(
102
+ default_value={},
103
+ help="SkyBox rendering settings (show, sources for cube map faces)"
104
+ ).tag(sync=True)
105
+
106
+ # Camera commands (for advanced camera operations)
107
+ camera_command = traitlets.Dict(
108
+ default_value={},
109
+ help="Camera command trigger for flyTo, lookAt, move, rotate, zoom operations"
79
110
  ).tag(sync=True)
80
111
 
81
112
  # Measurement tools
@@ -121,8 +152,10 @@ class CesiumWidget(anywidget.AnyWidget):
121
152
 
122
153
  super().__init__(**kwargs)
123
154
 
124
- def fly_to(self, latitude: float, longitude: float, altitude: float = 400, duration: float = 3.0):
125
- """Fly the camera to a specific location.
155
+ def fly_to(self, latitude: float, longitude: float, altitude: float = 400,
156
+ heading: float = 0.0, pitch: float = -15.0, roll: float = 0.0,
157
+ duration: float = 3.0):
158
+ """Fly the camera to a specific location with animation.
126
159
 
127
160
  Parameters
128
161
  ----------
@@ -132,15 +165,44 @@ class CesiumWidget(anywidget.AnyWidget):
132
165
  Target longitude in degrees
133
166
  altitude : float, optional
134
167
  Target altitude in meters (default: 400)
168
+ heading : float, optional
169
+ Camera heading in degrees (default: 0.0)
170
+ pitch : float, optional
171
+ Camera pitch in degrees (default: -15.0)
172
+ roll : float, optional
173
+ Camera roll in degrees (default: 0.0)
135
174
  duration : float, optional
136
175
  Flight duration in seconds (default: 3.0)
176
+
177
+ Examples
178
+ --------
179
+ >>> widget.fly_to(48.8566, 2.3522, altitude=5000, duration=2.0)
180
+ >>> widget.fly_to(40.7128, -74.0060, heading=45, pitch=-30)
137
181
  """
182
+ import time
183
+ # Update traitlets for state sync
138
184
  self.latitude = latitude
139
185
  self.longitude = longitude
140
186
  self.altitude = altitude
187
+ self.heading = heading
188
+ self.pitch = pitch
189
+ self.roll = roll
190
+ # Send command for animation
191
+ self.camera_command = {
192
+ 'command': 'flyTo',
193
+ 'latitude': latitude,
194
+ 'longitude': longitude,
195
+ 'altitude': altitude,
196
+ 'heading': heading,
197
+ 'pitch': pitch,
198
+ 'roll': roll,
199
+ 'duration': duration,
200
+ 'timestamp': time.time()
201
+ }
141
202
 
142
203
  def set_view(
143
- self, latitude : float , longitude: float, altitude: float = 400, heading: float = 0.0, pitch: float = -15.0, roll: float = 0.0
204
+ self, latitude: float, longitude: float, altitude: float = 400,
205
+ heading: float = 0.0, pitch: float = -15.0, roll: float = 0.0
144
206
  ):
145
207
  """Set the camera view instantly without animation.
146
208
 
@@ -158,29 +220,389 @@ class CesiumWidget(anywidget.AnyWidget):
158
220
  Camera pitch in degrees (default: -15.0)
159
221
  roll : float, optional
160
222
  Camera roll in degrees (default: 0.0)
223
+
224
+ Examples
225
+ --------
226
+ >>> widget.set_view(48.8566, 2.3522, altitude=1000)
227
+ >>> widget.set_view(40.7128, -74.0060, heading=90, pitch=-45)
161
228
  """
229
+ import time
230
+ # Update traitlets for state sync
162
231
  self.latitude = latitude
163
232
  self.longitude = longitude
164
233
  self.altitude = altitude
165
234
  self.heading = heading
166
235
  self.pitch = pitch
167
236
  self.roll = roll
237
+ # Send command for instant view change
238
+ self.camera_command = {
239
+ 'command': 'setView',
240
+ 'latitude': latitude,
241
+ 'longitude': longitude,
242
+ 'altitude': altitude,
243
+ 'heading': heading,
244
+ 'pitch': pitch,
245
+ 'roll': roll,
246
+ 'timestamp': time.time()
247
+ }
248
+
249
+ def look_at(self, target_latitude: float, target_longitude: float, target_altitude: float = 0,
250
+ offset_heading: float = 0.0, offset_pitch: float = -45.0, offset_range: float = 1000.0):
251
+ """Point the camera at a target location from an offset position.
252
+
253
+ This is useful for looking at a specific point from a certain distance and angle.
254
+
255
+ Parameters
256
+ ----------
257
+ target_latitude : float
258
+ Target point latitude in degrees
259
+ target_longitude : float
260
+ Target point longitude in degrees
261
+ target_altitude : float, optional
262
+ Target point altitude in meters (default: 0)
263
+ offset_heading : float, optional
264
+ Heading offset from target in degrees (default: 0.0)
265
+ offset_pitch : float, optional
266
+ Pitch offset from target in degrees (default: -45.0)
267
+ offset_range : float, optional
268
+ Distance from target in meters (default: 1000.0)
269
+
270
+ Examples
271
+ --------
272
+ >>> # Look at Eiffel Tower from 500m away at 30° angle
273
+ >>> widget.look_at(48.8584, 2.2945, offset_range=500, offset_pitch=-30)
274
+
275
+ >>> # Orbit view around a location
276
+ >>> widget.look_at(40.7128, -74.0060, offset_heading=45, offset_range=2000)
277
+ """
278
+ import time
279
+ self.camera_command = {
280
+ 'command': 'lookAt',
281
+ 'targetLatitude': target_latitude,
282
+ 'targetLongitude': target_longitude,
283
+ 'targetAltitude': target_altitude,
284
+ 'offsetHeading': offset_heading,
285
+ 'offsetPitch': offset_pitch,
286
+ 'offsetRange': offset_range,
287
+ 'timestamp': time.time()
288
+ }
289
+
290
+ def move_forward(self, distance: float = 100.0):
291
+ """Move the camera forward by a specified distance.
292
+
293
+ Parameters
294
+ ----------
295
+ distance : float, optional
296
+ Distance to move in meters (default: 100.0)
297
+
298
+ Examples
299
+ --------
300
+ >>> widget.move_forward(500) # Move 500m forward
301
+ """
302
+ import time
303
+ self.camera_command = {
304
+ 'command': 'moveForward',
305
+ 'distance': distance,
306
+ 'timestamp': time.time()
307
+ }
308
+
309
+ def move_backward(self, distance: float = 100.0):
310
+ """Move the camera backward by a specified distance.
168
311
 
169
- def load_geojson(self, geojson):
312
+ Parameters
313
+ ----------
314
+ distance : float, optional
315
+ Distance to move in meters (default: 100.0)
316
+
317
+ Examples
318
+ --------
319
+ >>> widget.move_backward(500) # Move 500m backward
320
+ """
321
+ import time
322
+ self.camera_command = {
323
+ 'command': 'moveBackward',
324
+ 'distance': distance,
325
+ 'timestamp': time.time()
326
+ }
327
+
328
+ def move_up(self, distance: float = 100.0):
329
+ """Move the camera up by a specified distance.
330
+
331
+ Parameters
332
+ ----------
333
+ distance : float, optional
334
+ Distance to move in meters (default: 100.0)
335
+
336
+ Examples
337
+ --------
338
+ >>> widget.move_up(200) # Move 200m up
339
+ """
340
+ import time
341
+ self.camera_command = {
342
+ 'command': 'moveUp',
343
+ 'distance': distance,
344
+ 'timestamp': time.time()
345
+ }
346
+
347
+ def move_down(self, distance: float = 100.0):
348
+ """Move the camera down by a specified distance.
349
+
350
+ Parameters
351
+ ----------
352
+ distance : float, optional
353
+ Distance to move in meters (default: 100.0)
354
+
355
+ Examples
356
+ --------
357
+ >>> widget.move_down(200) # Move 200m down
358
+ """
359
+ import time
360
+ self.camera_command = {
361
+ 'command': 'moveDown',
362
+ 'distance': distance,
363
+ 'timestamp': time.time()
364
+ }
365
+
366
+ def move_left(self, distance: float = 100.0):
367
+ """Move the camera left by a specified distance.
368
+
369
+ Parameters
370
+ ----------
371
+ distance : float, optional
372
+ Distance to move in meters (default: 100.0)
373
+
374
+ Examples
375
+ --------
376
+ >>> widget.move_left(150) # Move 150m left
377
+ """
378
+ import time
379
+ self.camera_command = {
380
+ 'command': 'moveLeft',
381
+ 'distance': distance,
382
+ 'timestamp': time.time()
383
+ }
384
+
385
+ def move_right(self, distance: float = 100.0):
386
+ """Move the camera right by a specified distance.
387
+
388
+ Parameters
389
+ ----------
390
+ distance : float, optional
391
+ Distance to move in meters (default: 100.0)
392
+
393
+ Examples
394
+ --------
395
+ >>> widget.move_right(150) # Move 150m right
396
+ """
397
+ import time
398
+ self.camera_command = {
399
+ 'command': 'moveRight',
400
+ 'distance': distance,
401
+ 'timestamp': time.time()
402
+ }
403
+
404
+ def rotate_left(self, angle: float = 15.0):
405
+ """Rotate the camera left (counterclockwise) by a specified angle.
406
+
407
+ Parameters
408
+ ----------
409
+ angle : float, optional
410
+ Rotation angle in degrees (default: 15.0)
411
+
412
+ Examples
413
+ --------
414
+ >>> widget.rotate_left(45) # Rotate 45° left
415
+ """
416
+ import time
417
+ self.camera_command = {
418
+ 'command': 'rotateLeft',
419
+ 'angle': angle,
420
+ 'timestamp': time.time()
421
+ }
422
+
423
+ def rotate_right(self, angle: float = 15.0):
424
+ """Rotate the camera right (clockwise) by a specified angle.
425
+
426
+ Parameters
427
+ ----------
428
+ angle : float, optional
429
+ Rotation angle in degrees (default: 15.0)
430
+
431
+ Examples
432
+ --------
433
+ >>> widget.rotate_right(45) # Rotate 45° right
434
+ """
435
+ import time
436
+ self.camera_command = {
437
+ 'command': 'rotateRight',
438
+ 'angle': angle,
439
+ 'timestamp': time.time()
440
+ }
441
+
442
+ def rotate_up(self, angle: float = 15.0):
443
+ """Rotate the camera up by a specified angle.
444
+
445
+ Parameters
446
+ ----------
447
+ angle : float, optional
448
+ Rotation angle in degrees (default: 15.0)
449
+
450
+ Examples
451
+ --------
452
+ >>> widget.rotate_up(30) # Look up 30°
453
+ """
454
+ import time
455
+ self.camera_command = {
456
+ 'command': 'rotateUp',
457
+ 'angle': angle,
458
+ 'timestamp': time.time()
459
+ }
460
+
461
+ def rotate_down(self, angle: float = 15.0):
462
+ """Rotate the camera down by a specified angle.
463
+
464
+ Parameters
465
+ ----------
466
+ angle : float, optional
467
+ Rotation angle in degrees (default: 15.0)
468
+
469
+ Examples
470
+ --------
471
+ >>> widget.rotate_down(30) # Look down 30°
472
+ """
473
+ import time
474
+ self.camera_command = {
475
+ 'command': 'rotateDown',
476
+ 'angle': angle,
477
+ 'timestamp': time.time()
478
+ }
479
+
480
+ def zoom_in(self, distance: float = 100.0):
481
+ """Zoom in (move camera closer to target).
482
+
483
+ Parameters
484
+ ----------
485
+ distance : float, optional
486
+ Distance to zoom in meters (default: 100.0)
487
+
488
+ Examples
489
+ --------
490
+ >>> widget.zoom_in(500) # Zoom in 500m
491
+ """
492
+ import time
493
+ self.camera_command = {
494
+ 'command': 'zoomIn',
495
+ 'distance': distance,
496
+ 'timestamp': time.time()
497
+ }
498
+
499
+ def zoom_out(self, distance: float = 100.0):
500
+ """Zoom out (move camera away from target).
501
+
502
+ Parameters
503
+ ----------
504
+ distance : float, optional
505
+ Distance to zoom in meters (default: 100.0)
506
+
507
+ Examples
508
+ --------
509
+ >>> widget.zoom_out(500) # Zoom out 500m
510
+ """
511
+ import time
512
+ self.camera_command = {
513
+ 'command': 'zoomOut',
514
+ 'distance': distance,
515
+ 'timestamp': time.time()
516
+ }
517
+
518
+ def set_camera(
519
+ self,
520
+ latitude=None,
521
+ longitude=None,
522
+ altitude=None,
523
+ heading=None,
524
+ pitch=None,
525
+ roll=None
526
+ ):
527
+ """Set individual camera parameters without full view reset.
528
+
529
+ This allows updating only specific camera properties while keeping others unchanged.
530
+
531
+ Parameters
532
+ ----------
533
+ latitude : float, optional
534
+ Camera latitude in degrees
535
+ longitude : float, optional
536
+ Camera longitude in degrees
537
+ altitude : float, optional
538
+ Camera altitude in meters
539
+ heading : float, optional
540
+ Camera heading in degrees
541
+ pitch : float, optional
542
+ Camera pitch in degrees
543
+ roll : float, optional
544
+ Camera roll in degrees
545
+
546
+ Examples
547
+ --------
548
+ >>> widget.set_camera(pitch=-45) # Only change pitch
549
+ >>> widget.set_camera(heading=90, altitude=5000) # Change heading and altitude
550
+ """
551
+ if latitude is not None:
552
+ self.latitude = latitude
553
+ if longitude is not None:
554
+ self.longitude = longitude
555
+ if altitude is not None:
556
+ self.altitude = altitude
557
+ if heading is not None:
558
+ self.heading = heading
559
+ if pitch is not None:
560
+ self.pitch = pitch
561
+ if roll is not None:
562
+ self.roll = roll
563
+
564
+ def load_geojson(self, geojson, append=False):
170
565
  """Load GeoJSON data for visualization.
171
566
 
172
567
  Parameters
173
568
  ----------
174
- geojson : dict
175
- GeoJSON dictionary or GeoJSON object
569
+ geojson : dict or str
570
+ GeoJSON dictionary or GeoJSON string
571
+ append : bool, optional
572
+ If True, append to existing GeoJSON data. If False (default), replace existing data.
573
+
574
+ Examples
575
+ --------
576
+ Load a single GeoJSON (replaces existing):
577
+ >>> widget.load_geojson({"type": "FeatureCollection", "features": [...]})
578
+
579
+ Load multiple GeoJSON datasets:
580
+ >>> widget.load_geojson(geojson1)
581
+ >>> widget.load_geojson(geojson2, append=True) # Adds to existing data
176
582
  """
177
583
  if isinstance(geojson, str):
178
584
  import json
179
-
180
585
  geojson = json.loads(geojson)
181
- self.geojson_data = geojson
586
+
587
+ if append:
588
+ # Append to existing list
589
+ current_data = list(self.geojson_data)
590
+ current_data.append(geojson)
591
+ self.geojson_data = current_data
592
+ else:
593
+ # Replace existing data
594
+ self.geojson_data = [geojson]
595
+
596
+ def clear_geojson(self):
597
+ """Clear all GeoJSON data from the viewer.
598
+
599
+ Examples
600
+ --------
601
+ >>> widget.clear_geojson()
602
+ """
603
+ self.geojson_data = []
182
604
 
183
- def load_czml(self, czml: str | list):
605
+ def load_czml(self, czml: str | list, append=False):
184
606
  """Load CZML data for visualization.
185
607
 
186
608
  CZML (Cesium Language) is a JSON format for describing time-dynamic
@@ -192,6 +614,8 @@ class CesiumWidget(anywidget.AnyWidget):
192
614
  ----------
193
615
  czml : str or list
194
616
  CZML document as a JSON string or list of packet dictionaries.
617
+ append : bool, optional
618
+ If True, append to existing CZML data. If False (default), replace existing data.
195
619
 
196
620
  Examples
197
621
  --------
@@ -208,6 +632,11 @@ class CesiumWidget(anywidget.AnyWidget):
208
632
  ... {"id": "point", "position": {"cartographicDegrees": [-74, 40, 0]}}
209
633
  ... ]
210
634
  >>> widget.load_czml(czml)
635
+
636
+ Append multiple CZML documents:
637
+ >>> widget.load_czml(czml_doc1)
638
+ >>> widget.load_czml(czml_doc2, append=True) # Adds to existing data
639
+ >>> widget.load_czml(new_czml, clear_existing=True)
211
640
  """
212
641
  import json
213
642
 
@@ -223,7 +652,23 @@ class CesiumWidget(anywidget.AnyWidget):
223
652
  if len(czml) == 0:
224
653
  raise ValueError("CZML document must contain at least one packet")
225
654
 
226
- self.czml_data = czml
655
+ if append:
656
+ # Append to existing list
657
+ current_data = list(self.czml_data)
658
+ current_data.append(czml)
659
+ self.czml_data = current_data
660
+ else:
661
+ # Replace existing data
662
+ self.czml_data = [czml]
663
+
664
+ def clear_czml(self):
665
+ """Clear all CZML data from the viewer.
666
+
667
+ Examples
668
+ --------
669
+ >>> widget.clear_czml()
670
+ """
671
+ self.czml_data = []
227
672
 
228
673
  def enable_measurement(self, mode: str = "distance"):
229
674
  """Enable a measurement tool.
@@ -329,6 +774,272 @@ class CesiumWidget(anywidget.AnyWidget):
329
774
  """Hide the measurements list panel."""
330
775
  self.show_measurements_list = False
331
776
 
777
+ def set_atmosphere(self,
778
+ brightness_shift=None,
779
+ hue_shift=None,
780
+ saturation_shift=None,
781
+ light_intensity=None,
782
+ rayleigh_coefficient=None,
783
+ rayleigh_scale_height=None,
784
+ mie_coefficient=None,
785
+ mie_scale_height=None,
786
+ mie_anisotropy=None):
787
+ """Configure atmosphere rendering settings.
788
+
789
+ Parameters
790
+ ----------
791
+ brightness_shift : float, optional
792
+ Brightness shift to apply (-1.0 to 1.0). Default 0.0 (no shift).
793
+ -1.0 is complete darkness, letting space show through.
794
+ hue_shift : float, optional
795
+ Hue shift to apply (0.0 to 1.0). Default 0.0 (no shift).
796
+ 1.0 indicates a complete rotation of hues.
797
+ saturation_shift : float, optional
798
+ Saturation shift to apply (-1.0 to 1.0). Default 0.0 (no shift).
799
+ -1.0 is monochrome.
800
+ light_intensity : float, optional
801
+ Intensity of light for ground atmosphere color computation.
802
+ rayleigh_coefficient : tuple of 3 floats, optional
803
+ Rayleigh scattering coefficient (x, y, z components).
804
+ rayleigh_scale_height : float, optional
805
+ Rayleigh scale height in meters.
806
+ mie_coefficient : tuple of 3 floats, optional
807
+ Mie scattering coefficient (x, y, z components).
808
+ mie_scale_height : float, optional
809
+ Mie scale height in meters.
810
+ mie_anisotropy : float, optional
811
+ Anisotropy of medium for Mie scattering (-1.0 to 1.0).
812
+
813
+ Examples
814
+ --------
815
+ Make atmosphere darker:
816
+ >>> widget.set_atmosphere(brightness_shift=-0.3)
817
+
818
+ Change hue (e.g., for alien planet effect):
819
+ >>> widget.set_atmosphere(hue_shift=0.3, saturation_shift=0.2)
820
+
821
+ Desaturate atmosphere:
822
+ >>> widget.set_atmosphere(saturation_shift=-0.5)
823
+
824
+ Reset to defaults:
825
+ >>> widget.set_atmosphere(brightness_shift=0, hue_shift=0, saturation_shift=0)
826
+ """
827
+ settings = {}
828
+
829
+ if brightness_shift is not None:
830
+ settings['brightnessShift'] = brightness_shift
831
+ if hue_shift is not None:
832
+ settings['hueShift'] = hue_shift
833
+ if saturation_shift is not None:
834
+ settings['saturationShift'] = saturation_shift
835
+ if light_intensity is not None:
836
+ settings['lightIntensity'] = light_intensity
837
+ if rayleigh_coefficient is not None:
838
+ if len(rayleigh_coefficient) != 3:
839
+ raise ValueError("rayleigh_coefficient must be a tuple of 3 floats")
840
+ settings['rayleighCoefficient'] = list(rayleigh_coefficient)
841
+ if rayleigh_scale_height is not None:
842
+ settings['rayleighScaleHeight'] = rayleigh_scale_height
843
+ if mie_coefficient is not None:
844
+ if len(mie_coefficient) != 3:
845
+ raise ValueError("mie_coefficient must be a tuple of 3 floats")
846
+ settings['mieCoefficient'] = list(mie_coefficient)
847
+ if mie_scale_height is not None:
848
+ settings['mieScaleHeight'] = mie_scale_height
849
+ if mie_anisotropy is not None:
850
+ settings['mieAnisotropy'] = mie_anisotropy
851
+
852
+ self.atmosphere_settings = settings
853
+
854
+ def set_sky_atmosphere(self,
855
+ show=None,
856
+ brightness_shift=None,
857
+ hue_shift=None,
858
+ saturation_shift=None,
859
+ light_intensity=None,
860
+ rayleigh_coefficient=None,
861
+ rayleigh_scale_height=None,
862
+ mie_coefficient=None,
863
+ mie_scale_height=None,
864
+ mie_anisotropy=None,
865
+ per_fragment_atmosphere=None):
866
+ """Configure sky atmosphere rendering settings.
867
+
868
+ The sky atmosphere is drawn around the limb of the ellipsoid and is only
869
+ visible in 3D mode (fades out in 2D/Columbus view).
870
+
871
+ Parameters
872
+ ----------
873
+ show : bool, optional
874
+ Whether to show the sky atmosphere.
875
+ brightness_shift : float, optional
876
+ Brightness shift to apply (-1.0 to 1.0). Default 0.0 (no shift).
877
+ -1.0 is complete darkness, letting space show through.
878
+ hue_shift : float, optional
879
+ Hue shift to apply (0.0 to 1.0). Default 0.0 (no shift).
880
+ 1.0 indicates a complete rotation of hues.
881
+ saturation_shift : float, optional
882
+ Saturation shift to apply (-1.0 to 1.0). Default 0.0 (no shift).
883
+ -1.0 is monochrome.
884
+ light_intensity : float, optional
885
+ Intensity of light for sky atmosphere color computation.
886
+ rayleigh_coefficient : tuple of 3 floats, optional
887
+ Rayleigh scattering coefficient (x, y, z components).
888
+ rayleigh_scale_height : float, optional
889
+ Rayleigh scale height in meters.
890
+ mie_coefficient : tuple of 3 floats, optional
891
+ Mie scattering coefficient (x, y, z components).
892
+ mie_scale_height : float, optional
893
+ Mie scale height in meters.
894
+ mie_anisotropy : float, optional
895
+ Anisotropy of medium for Mie scattering (-1.0 to 1.0).
896
+ per_fragment_atmosphere : bool, optional
897
+ Compute atmosphere per-fragment (better quality, slight performance cost).
898
+
899
+ Examples
900
+ --------
901
+ Hide sky atmosphere:
902
+ >>> widget.set_sky_atmosphere(show=False)
903
+
904
+ Make sky darker:
905
+ >>> widget.set_sky_atmosphere(brightness_shift=-0.3)
906
+
907
+ Change sky color:
908
+ >>> widget.set_sky_atmosphere(hue_shift=0.2, saturation_shift=0.1)
909
+
910
+ Enable per-fragment rendering:
911
+ >>> widget.set_sky_atmosphere(per_fragment_atmosphere=True)
912
+
913
+ Reset:
914
+ >>> widget.sky_atmosphere_settings = {}
915
+ """
916
+ settings = {}
917
+
918
+ if show is not None:
919
+ settings['show'] = show
920
+ if brightness_shift is not None:
921
+ settings['brightnessShift'] = brightness_shift
922
+ if hue_shift is not None:
923
+ settings['hueShift'] = hue_shift
924
+ if saturation_shift is not None:
925
+ settings['saturationShift'] = saturation_shift
926
+ if light_intensity is not None:
927
+ settings['atmosphereLightIntensity'] = light_intensity
928
+ if rayleigh_coefficient is not None:
929
+ if len(rayleigh_coefficient) != 3:
930
+ raise ValueError("rayleigh_coefficient must be a tuple of 3 floats")
931
+ settings['atmosphereRayleighCoefficient'] = list(rayleigh_coefficient)
932
+ if rayleigh_scale_height is not None:
933
+ settings['atmosphereRayleighScaleHeight'] = rayleigh_scale_height
934
+ if mie_coefficient is not None:
935
+ if len(mie_coefficient) != 3:
936
+ raise ValueError("mie_coefficient must be a tuple of 3 floats")
937
+ settings['atmosphereMieCoefficient'] = list(mie_coefficient)
938
+ if mie_scale_height is not None:
939
+ settings['atmosphereMieScaleHeight'] = mie_scale_height
940
+ if mie_anisotropy is not None:
941
+ settings['atmosphereMieAnisotropy'] = mie_anisotropy
942
+ if per_fragment_atmosphere is not None:
943
+ settings['perFragmentAtmosphere'] = per_fragment_atmosphere
944
+
945
+ self.sky_atmosphere_settings = settings
946
+
947
+ def set_skybox(self,
948
+ show=None,
949
+ sources=None):
950
+ """Configure skybox rendering settings.
951
+
952
+ The skybox is the cube map displayed around the scene. You can show/hide it
953
+ or provide custom cube map sources.
954
+
955
+ Parameters
956
+ ----------
957
+ show : bool, optional
958
+ Whether to show the skybox.
959
+ sources : dict, optional
960
+ Custom cube map sources with keys:
961
+ - 'positiveX': URL for +X face (right)
962
+ - 'negativeX': URL for -X face (left)
963
+ - 'positiveY': URL for +Y face (top)
964
+ - 'negativeY': URL for -Y face (bottom)
965
+ - 'positiveZ': URL for +Z face (front)
966
+ - 'negativeZ': URL for -Z face (back)
967
+
968
+ Examples
969
+ --------
970
+ Hide skybox:
971
+ >>> widget.set_skybox(show=False)
972
+
973
+ Show skybox:
974
+ >>> widget.set_skybox(show=True)
975
+
976
+ Custom skybox:
977
+ >>> widget.set_skybox(sources={
978
+ ... 'positiveX': 'path/to/right.jpg',
979
+ ... 'negativeX': 'path/to/left.jpg',
980
+ ... 'positiveY': 'path/to/top.jpg',
981
+ ... 'negativeY': 'path/to/bottom.jpg',
982
+ ... 'positiveZ': 'path/to/front.jpg',
983
+ ... 'negativeZ': 'path/to/back.jpg'
984
+ ... })
985
+ """
986
+ settings = {}
987
+
988
+ if show is not None:
989
+ settings['show'] = show
990
+
991
+ if sources is not None:
992
+ if not isinstance(sources, dict):
993
+ raise ValueError("sources must be a dictionary with cube map face URLs")
994
+
995
+ # Validate that all required faces are provided if sources is given
996
+ required_faces = {'positiveX', 'negativeX', 'positiveY', 'negativeY', 'positiveZ', 'negativeZ'}
997
+ provided_faces = set(sources.keys())
998
+
999
+ if provided_faces and not required_faces.issubset(provided_faces):
1000
+ missing = required_faces - provided_faces
1001
+ raise ValueError(f"sources must include all cube map faces. Missing: {missing}")
1002
+
1003
+ settings['sources'] = sources
1004
+
1005
+ self.skybox_settings = settings
1006
+
1007
+ def on_interaction(self, callback):
1008
+ """Register a callback for user interaction events.
1009
+
1010
+ The callback will be called when any user interaction ends (camera movement,
1011
+ clicks, timeline scrubbing, etc.) with a dictionary containing:
1012
+
1013
+ - type: Interaction type ('camera_move', 'left_click', 'right_click', 'timeline_scrub')
1014
+ - timestamp: ISO 8601 timestamp when interaction occurred
1015
+ - camera: Camera state (latitude, longitude, altitude, heading, pitch, roll)
1016
+ - clock: Clock state (current_time, multiplier, is_animating) if timeline enabled
1017
+ - picked_position: Coordinates of clicked location (if applicable)
1018
+ - picked_entity: Information about clicked entity (if applicable)
1019
+
1020
+ Parameters
1021
+ ----------
1022
+ callback : callable
1023
+ Function to call with interaction event data: callback(event_data)
1024
+
1025
+ Examples
1026
+ --------
1027
+ >>> def handle_interaction(event):
1028
+ ... print(f"Interaction: {event['type']}")
1029
+ ... print(f"Camera at: {event['camera']['latitude']}, {event['camera']['longitude']}")
1030
+ ... if 'picked_position' in event:
1031
+ ... print(f"Clicked: {event['picked_position']}")
1032
+ >>>
1033
+ >>> widget.on_interaction(handle_interaction)
1034
+ """
1035
+ def wrapper(change):
1036
+ event_data = change['new']
1037
+ if event_data and event_data.get('timestamp'): # Only call if valid event
1038
+ callback(event_data)
1039
+
1040
+ self.observe(wrapper, names='interaction_event')
1041
+ return wrapper # Return so user can unobserve if needed
1042
+
332
1043
  def debug_info(self):
333
1044
  """Print debug information about the widget.
334
1045
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cesiumjs-anywidget
3
- Version: 0.2.4
3
+ Version: 0.3.0
4
4
  Summary: A Jupyter widget for CesiumJS 3D globe visualization using anywidget
5
5
  Project-URL: Homepage, https://github.com/Alex-PLACET/cesiumjs_anywidget
6
6
  Project-URL: Repository, https://github.com/Alex-PLACET/cesiumjs_anywidget
@@ -0,0 +1,8 @@
1
+ cesiumjs_anywidget/__init__.py,sha256=9WVcAtreHgk6C5clPG6sZy4m7s5AIbGU1DJ4oDpx3Is,165
2
+ cesiumjs_anywidget/styles.css,sha256=kt2i9fJuM6gaR7WkoQ2VGGoHzhqy6RpJVK2xwMKPV70,689
3
+ cesiumjs_anywidget/widget.py,sha256=_7uXX7B3KsOg_3QrMDfV0pZ8BGA-6coFnZ7_gTI3nck,39104
4
+ cesiumjs_anywidget/index.js,sha256=eSZZJhS2-3UDipN3KkXpxUMTgx4Jzs8sSHAWSjhUnHU,67449
5
+ cesiumjs_anywidget-0.3.0.dist-info/METADATA,sha256=eTd0CCg94JXQ5ezxPvZGyyvJG_ZSCIu41jkYCUav5cs,24778
6
+ cesiumjs_anywidget-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ cesiumjs_anywidget-0.3.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
8
+ cesiumjs_anywidget-0.3.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- cesiumjs_anywidget/__init__.py,sha256=9WVcAtreHgk6C5clPG6sZy4m7s5AIbGU1DJ4oDpx3Is,165
2
- cesiumjs_anywidget/styles.css,sha256=kt2i9fJuM6gaR7WkoQ2VGGoHzhqy6RpJVK2xwMKPV70,689
3
- cesiumjs_anywidget/widget.py,sha256=7RnZgQxkiKso6CjH4ciZi4JjJwBOI-cxuXRd2Ap7WGM,13646
4
- cesiumjs_anywidget/index.js,sha256=6vqb7UM8cxeJyuyDIw2IJcsaPXRjKsSRCXbWEJNTmZk,54315
5
- cesiumjs_anywidget-0.2.4.dist-info/METADATA,sha256=ToDMpPDZ9wUGJPLGjOML5ElawxeYUTtz3WTWqOBi9r8,24778
6
- cesiumjs_anywidget-0.2.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
- cesiumjs_anywidget-0.2.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
8
- cesiumjs_anywidget-0.2.4.dist-info/RECORD,,