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.
- cesiumjs_anywidget/index.js +385 -38
- cesiumjs_anywidget/widget.py +729 -18
- {cesiumjs_anywidget-0.2.4.dist-info → cesiumjs_anywidget-0.3.0.dist-info}/METADATA +1 -1
- cesiumjs_anywidget-0.3.0.dist-info/RECORD +8 -0
- cesiumjs_anywidget-0.2.4.dist-info/RECORD +0 -8
- {cesiumjs_anywidget-0.2.4.dist-info → cesiumjs_anywidget-0.3.0.dist-info}/WHEEL +0 -0
- {cesiumjs_anywidget-0.2.4.dist-info → cesiumjs_anywidget-0.3.0.dist-info}/licenses/LICENSE +0 -0
cesiumjs_anywidget/index.js
CHANGED
|
@@ -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
|
|
318
|
+
let geojsonDataSources = [];
|
|
91
319
|
model.on("change:geojson_data", async () => {
|
|
92
|
-
if (!viewer)
|
|
320
|
+
if (!viewer || !viewer.dataSources)
|
|
93
321
|
return;
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
viewer.dataSources
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
116
|
-
viewer
|
|
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
|
|
362
|
+
let czmlDataSources = [];
|
|
123
363
|
model.on("change:czml_data", async () => {
|
|
124
|
-
if (!viewer)
|
|
364
|
+
if (!viewer || !viewer.dataSources)
|
|
125
365
|
return;
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
viewer.dataSources
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
144
|
-
viewer
|
|
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,
|
cesiumjs_anywidget/widget.py
CHANGED
|
@@ -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.
|
|
70
|
-
|
|
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=
|
|
77
|
-
|
|
78
|
-
|
|
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,
|
|
125
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,,
|
|
File without changes
|
|
File without changes
|