cesiumjs-anywidget 0.6.0__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,15 @@ function error(prefix, ...args) {
23
23
 
24
24
  // src/cesiumjs_anywidget/js/viewer-init.js
25
25
  var PREFIX = "ViewerInit";
26
+ var CONSTANTS = {
27
+ // CesiumJS CDN
28
+ CESIUM_CDN_VERSION: "1.137",
29
+ // Interaction Timing
30
+ TIMELINE_SCRUB_DEBOUNCE_MS: 500,
31
+ // GeoJSON Defaults
32
+ GEOJSON_STROKE_WIDTH: 3,
33
+ GEOJSON_FILL_ALPHA: 0.5
34
+ };
26
35
  async function loadCesiumJS() {
27
36
  log(PREFIX, "Loading CesiumJS...");
28
37
  if (window.Cesium) {
@@ -30,7 +39,7 @@ async function loadCesiumJS() {
30
39
  return window.Cesium;
31
40
  }
32
41
  const script = document.createElement("script");
33
- script.src = "https://cesium.com/downloads/cesiumjs/releases/1.135/Build/Cesium/Cesium.js";
42
+ script.src = `https://cesium.com/downloads/cesiumjs/releases/${CONSTANTS.CESIUM_CDN_VERSION}/Build/Cesium/Cesium.js`;
34
43
  log(PREFIX, "Loading CesiumJS from CDN...");
35
44
  await new Promise((resolve, reject) => {
36
45
  script.onload = () => {
@@ -79,14 +88,23 @@ function createViewer(container, model, Cesium) {
79
88
  shadows: false,
80
89
  shouldAnimate: false
81
90
  };
91
+ const showGlobe = model.get("show_globe");
92
+ if (showGlobe === false) {
93
+ viewerOptions.globe = false;
94
+ log(PREFIX, "Globe disabled (typically for photorealistic tiles)");
95
+ }
82
96
  log(PREFIX, "Viewer options:", viewerOptions);
83
- if (model.get("enable_terrain")) {
97
+ if (model.get("enable_terrain") && showGlobe !== false) {
84
98
  viewerOptions.terrain = Cesium.Terrain.fromWorldTerrain();
85
99
  log(PREFIX, "Terrain enabled");
86
100
  }
87
101
  const viewer = new Cesium.Viewer(container, viewerOptions);
88
- viewer.scene.globe.enableLighting = model.get("enable_lighting");
89
- log(PREFIX, "Viewer created, lighting:", model.get("enable_lighting"));
102
+ if (viewer.scene.globe) {
103
+ viewer.scene.globe.enableLighting = model.get("enable_lighting");
104
+ log(PREFIX, "Viewer created, lighting:", model.get("enable_lighting"));
105
+ } else {
106
+ log(PREFIX, "Viewer created without globe");
107
+ }
90
108
  return viewer;
91
109
  }
92
110
  function setupViewerListeners(viewer, model, container, Cesium) {
@@ -98,8 +116,7 @@ function setupViewerListeners(viewer, model, container, Cesium) {
98
116
  log(PREFIX, "Skipping enable_terrain change - destroyed");
99
117
  return;
100
118
  }
101
- if (!viewer)
102
- return;
119
+ if (!viewer) return;
103
120
  log(PREFIX, "Terrain setting changed:", model.get("enable_terrain"));
104
121
  if (model.get("enable_terrain")) {
105
122
  viewer.scene.setTerrain(Cesium.Terrain.fromWorldTerrain());
@@ -108,46 +125,35 @@ function setupViewerListeners(viewer, model, container, Cesium) {
108
125
  }
109
126
  });
110
127
  model.on("change:enable_lighting", () => {
111
- if (isDestroyed)
112
- return;
113
- if (!viewer)
114
- return;
128
+ if (isDestroyed) return;
129
+ if (!viewer) return;
115
130
  log(PREFIX, "Lighting setting changed:", model.get("enable_lighting"));
116
131
  viewer.scene.globe.enableLighting = model.get("enable_lighting");
117
132
  });
118
133
  model.on("change:height", () => {
119
- if (isDestroyed)
120
- return;
121
- if (!viewer)
122
- return;
134
+ if (isDestroyed) return;
135
+ if (!viewer) return;
123
136
  log(PREFIX, "Height changed:", model.get("height"));
124
137
  container.style.height = model.get("height");
125
138
  viewer.resize();
126
139
  });
127
140
  model.on("change:show_timeline", () => {
128
- if (isDestroyed)
129
- return;
130
- if (!viewer || !viewer.timeline)
131
- return;
141
+ if (isDestroyed) return;
142
+ if (!viewer || !viewer.timeline) return;
132
143
  log(PREFIX, "Timeline visibility changed:", model.get("show_timeline"));
133
144
  viewer.timeline.container.style.visibility = model.get("show_timeline") ? "visible" : "hidden";
134
145
  });
135
146
  model.on("change:show_animation", () => {
136
- if (isDestroyed)
137
- return;
138
- if (!viewer || !viewer.animation)
139
- return;
147
+ if (isDestroyed) return;
148
+ if (!viewer || !viewer.animation) return;
140
149
  log(PREFIX, "Animation visibility changed:", model.get("show_animation"));
141
150
  viewer.animation.container.style.visibility = model.get("show_animation") ? "visible" : "hidden";
142
151
  });
143
152
  model.on("change:atmosphere_settings", () => {
144
- if (isDestroyed)
145
- return;
146
- if (!viewer || !viewer.scene || !viewer.scene.atmosphere)
147
- return;
153
+ if (isDestroyed) return;
154
+ if (!viewer || !viewer.scene || !viewer.scene.atmosphere) return;
148
155
  const settings = model.get("atmosphere_settings");
149
- if (!settings || Object.keys(settings).length === 0)
150
- return;
156
+ if (!settings || Object.keys(settings).length === 0) return;
151
157
  log(PREFIX, "Atmosphere settings changed:", settings);
152
158
  const atmosphere = viewer.scene.atmosphere;
153
159
  if (settings.brightnessShift !== void 0) {
@@ -187,13 +193,10 @@ function setupViewerListeners(viewer, model, container, Cesium) {
187
193
  }
188
194
  });
189
195
  model.on("change:sky_atmosphere_settings", () => {
190
- if (isDestroyed)
191
- return;
192
- if (!viewer || !viewer.scene || !viewer.scene.skyAtmosphere)
193
- return;
196
+ if (isDestroyed) return;
197
+ if (!viewer || !viewer.scene || !viewer.scene.skyAtmosphere) return;
194
198
  const settings = model.get("sky_atmosphere_settings");
195
- if (!settings || Object.keys(settings).length === 0)
196
- return;
199
+ if (!settings || Object.keys(settings).length === 0) return;
197
200
  log(PREFIX, "Sky atmosphere settings changed:", settings);
198
201
  const skyAtmosphere = viewer.scene.skyAtmosphere;
199
202
  if (settings.show !== void 0) {
@@ -239,13 +242,10 @@ function setupViewerListeners(viewer, model, container, Cesium) {
239
242
  }
240
243
  });
241
244
  model.on("change:skybox_settings", () => {
242
- if (isDestroyed)
243
- return;
244
- if (!viewer || !viewer.scene || !viewer.scene.skyBox)
245
- return;
245
+ if (isDestroyed) return;
246
+ if (!viewer || !viewer.scene || !viewer.scene.skyBox) return;
246
247
  const settings = model.get("skybox_settings");
247
- if (!settings || Object.keys(settings).length === 0)
248
- return;
248
+ if (!settings || Object.keys(settings).length === 0) return;
249
249
  log(PREFIX, "SkyBox settings changed:", settings);
250
250
  const skyBox = viewer.scene.skyBox;
251
251
  if (settings.show !== void 0) {
@@ -270,6 +270,184 @@ function setupViewerListeners(viewer, model, container, Cesium) {
270
270
  }
271
271
  }
272
272
  });
273
+ const initialTime = model.get("current_time");
274
+ if (initialTime && viewer.clock) {
275
+ try {
276
+ const julianDate = Cesium.JulianDate.fromIso8601(initialTime);
277
+ viewer.clock.currentTime = julianDate;
278
+ viewer.clock.startTime = julianDate.clone();
279
+ viewer.clock.stopTime = Cesium.JulianDate.addDays(julianDate.clone(), 1, new Cesium.JulianDate());
280
+ log(PREFIX, "Clock initialized with time:", initialTime);
281
+ } catch (err) {
282
+ warn(PREFIX, "Failed to parse initial time:", initialTime, err);
283
+ }
284
+ }
285
+ const initialMultiplier = model.get("clock_multiplier");
286
+ if (initialMultiplier !== void 0 && viewer.clock) {
287
+ viewer.clock.multiplier = initialMultiplier;
288
+ log(PREFIX, "Clock multiplier initialized:", initialMultiplier);
289
+ }
290
+ const initialAnimate = model.get("should_animate");
291
+ if (initialAnimate !== void 0 && viewer.clock) {
292
+ viewer.clock.shouldAnimate = initialAnimate;
293
+ log(PREFIX, "Clock animation initialized:", initialAnimate);
294
+ }
295
+ model.on("change:current_time", () => {
296
+ if (isDestroyed) return;
297
+ if (!viewer || !viewer.clock) return;
298
+ const timeStr = model.get("current_time");
299
+ if (!timeStr) return;
300
+ try {
301
+ const julianDate = Cesium.JulianDate.fromIso8601(timeStr);
302
+ viewer.clock.currentTime = julianDate;
303
+ log(PREFIX, "Clock time updated:", timeStr);
304
+ } catch (err) {
305
+ warn(PREFIX, "Failed to parse time:", timeStr, err);
306
+ }
307
+ });
308
+ model.on("change:clock_multiplier", () => {
309
+ if (isDestroyed) return;
310
+ if (!viewer || !viewer.clock) return;
311
+ const multiplier = model.get("clock_multiplier");
312
+ if (multiplier !== void 0) {
313
+ viewer.clock.multiplier = multiplier;
314
+ log(PREFIX, "Clock multiplier updated:", multiplier);
315
+ }
316
+ });
317
+ model.on("change:should_animate", () => {
318
+ if (isDestroyed) return;
319
+ if (!viewer || !viewer.clock) return;
320
+ const shouldAnimate = model.get("should_animate");
321
+ if (shouldAnimate !== void 0) {
322
+ viewer.clock.shouldAnimate = shouldAnimate;
323
+ log(PREFIX, "Clock animation updated:", shouldAnimate);
324
+ }
325
+ });
326
+ model.on("change:clock_command", () => {
327
+ if (isDestroyed) return;
328
+ if (!viewer || !viewer.clock) return;
329
+ const command = model.get("clock_command");
330
+ if (!command || !command.command) return;
331
+ log(PREFIX, "Clock command received:", command.command);
332
+ switch (command.command) {
333
+ case "setTime":
334
+ if (command.time) {
335
+ try {
336
+ const julianDate = Cesium.JulianDate.fromIso8601(command.time);
337
+ viewer.clock.currentTime = julianDate;
338
+ log(PREFIX, "Clock time set via command:", command.time);
339
+ } catch (err) {
340
+ warn(PREFIX, "Failed to parse time in command:", command.time, err);
341
+ }
342
+ }
343
+ break;
344
+ case "play":
345
+ viewer.clock.shouldAnimate = true;
346
+ log(PREFIX, "Clock animation started");
347
+ break;
348
+ case "pause":
349
+ viewer.clock.shouldAnimate = false;
350
+ log(PREFIX, "Clock animation paused");
351
+ break;
352
+ case "setMultiplier":
353
+ if (command.multiplier !== void 0) {
354
+ viewer.clock.multiplier = command.multiplier;
355
+ log(PREFIX, "Clock multiplier set via command:", command.multiplier);
356
+ }
357
+ break;
358
+ case "setRange":
359
+ if (command.startTime && command.stopTime) {
360
+ try {
361
+ const startDate = Cesium.JulianDate.fromIso8601(command.startTime);
362
+ const stopDate = Cesium.JulianDate.fromIso8601(command.stopTime);
363
+ viewer.clock.startTime = startDate;
364
+ viewer.clock.stopTime = stopDate;
365
+ log(PREFIX, "Clock range set via command:", command.startTime, "to", command.stopTime);
366
+ } catch (err) {
367
+ warn(PREFIX, "Failed to parse time range in command:", err);
368
+ }
369
+ }
370
+ break;
371
+ default:
372
+ warn(PREFIX, "Unknown clock command:", command.command);
373
+ }
374
+ });
375
+ model.on("change:czml_entity_update", () => {
376
+ if (isDestroyed) return;
377
+ if (!viewer || !viewer.dataSources) return;
378
+ const update = model.get("czml_entity_update");
379
+ if (!update || !update.entity_id || !update.properties) return;
380
+ log(PREFIX, "CZML entity update:", update);
381
+ let entity = null;
382
+ for (let i = 0; i < viewer.dataSources.length; i++) {
383
+ const dataSource = viewer.dataSources.get(i);
384
+ if (dataSource instanceof Cesium.CzmlDataSource) {
385
+ entity = dataSource.entities.getById(update.entity_id);
386
+ if (entity) break;
387
+ }
388
+ }
389
+ if (!entity) {
390
+ warn(PREFIX, "Entity not found:", update.entity_id);
391
+ return;
392
+ }
393
+ const props = update.properties;
394
+ if (props.orientation && !props.position) {
395
+ const heading = Cesium.Math.toRadians(props.orientation.heading ?? 0);
396
+ const pitch = Cesium.Math.toRadians(props.orientation.pitch ?? 0);
397
+ const roll = Cesium.Math.toRadians(props.orientation.roll ?? 0);
398
+ const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
399
+ if (entity.position) {
400
+ const position = entity.position.getValue(viewer.clock.currentTime);
401
+ if (position) {
402
+ const orientation = Cesium.Transforms.headingPitchRollQuaternion(
403
+ position,
404
+ hpr
405
+ );
406
+ entity.orientation = new Cesium.ConstantProperty(orientation);
407
+ log(PREFIX, "Updated entity orientation:", props.orientation);
408
+ } else {
409
+ warn(PREFIX, "Cannot update orientation - no valid position");
410
+ }
411
+ } else {
412
+ warn(PREFIX, "Cannot update orientation - entity has no position");
413
+ }
414
+ }
415
+ if (props.position) {
416
+ const lat = Cesium.Math.toRadians(props.position.latitude);
417
+ const lon = Cesium.Math.toRadians(props.position.longitude);
418
+ const alt = props.position.altitude ?? 0;
419
+ const position = Cesium.Cartesian3.fromRadians(lon, lat, alt);
420
+ entity.position = new Cesium.ConstantPositionProperty(position);
421
+ log(PREFIX, "Updated entity position:", props.position);
422
+ if (props.orientation && entity.orientation) {
423
+ const heading = Cesium.Math.toRadians(props.orientation.heading ?? 0);
424
+ const pitch = Cesium.Math.toRadians(props.orientation.pitch ?? 0);
425
+ const roll = Cesium.Math.toRadians(props.orientation.roll ?? 0);
426
+ const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
427
+ const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr);
428
+ entity.orientation = new Cesium.ConstantProperty(orientation);
429
+ log(PREFIX, "Updated entity orientation after position change:", props.orientation);
430
+ }
431
+ }
432
+ for (const [key, value] of Object.entries(props)) {
433
+ if (key !== "orientation" && key !== "position" && entity[key] !== void 0) {
434
+ try {
435
+ if (typeof value === "object" && value !== null) {
436
+ for (const [subKey, subValue] of Object.entries(value)) {
437
+ if (entity[key][subKey] !== void 0) {
438
+ entity[key][subKey] = new Cesium.ConstantProperty(subValue);
439
+ }
440
+ }
441
+ } else {
442
+ entity[key] = new Cesium.ConstantProperty(value);
443
+ }
444
+ log(PREFIX, `Updated entity ${key}:`, value);
445
+ } catch (err) {
446
+ warn(PREFIX, `Failed to update property ${key}:`, err);
447
+ }
448
+ }
449
+ }
450
+ });
273
451
  function getCameraState() {
274
452
  if (!viewer || !viewer.camera || !viewer.camera.positionCartographic) {
275
453
  warn(PREFIX, "Cannot get camera state - viewer or camera not available");
@@ -291,8 +469,7 @@ function setupViewerListeners(viewer, model, container, Cesium) {
291
469
  }
292
470
  }
293
471
  function getClockState() {
294
- if (!viewer || !viewer.clock)
295
- return null;
472
+ if (!viewer || !viewer.clock) return null;
296
473
  try {
297
474
  return {
298
475
  current_time: Cesium.JulianDate.toIso8601(viewer.clock.currentTime),
@@ -304,6 +481,61 @@ function setupViewerListeners(viewer, model, container, Cesium) {
304
481
  return null;
305
482
  }
306
483
  }
484
+ function getPickedPosition(click) {
485
+ if (!viewer || !viewer.camera || !viewer.scene || !viewer.scene.globe) {
486
+ return null;
487
+ }
488
+ try {
489
+ const ray = viewer.camera.getPickRay(click.position);
490
+ if (!ray) return null;
491
+ const cartesian = viewer.scene.globe.pick(ray, viewer.scene);
492
+ if (!cartesian) return null;
493
+ const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
494
+ return {
495
+ latitude: Cesium.Math.toDegrees(cartographic.latitude),
496
+ longitude: Cesium.Math.toDegrees(cartographic.longitude),
497
+ altitude: cartographic.height
498
+ };
499
+ } catch (error2) {
500
+ warn(PREFIX, "Error picking position:", error2);
501
+ return null;
502
+ }
503
+ }
504
+ function getPickedEntity(click) {
505
+ if (!viewer || !viewer.scene || !viewer.clock) {
506
+ return null;
507
+ }
508
+ try {
509
+ const pickedObject = viewer.scene.pick(click.position);
510
+ if (!Cesium.defined(pickedObject) || !Cesium.defined(pickedObject.id)) {
511
+ return null;
512
+ }
513
+ const entity = pickedObject.id;
514
+ const entityData = {
515
+ id: entity.id,
516
+ name: entity.name || null
517
+ };
518
+ if (entity.properties) {
519
+ const props = {};
520
+ const propertyNames = entity.properties.propertyNames;
521
+ if (propertyNames && propertyNames.length > 0) {
522
+ propertyNames.forEach((name) => {
523
+ try {
524
+ props[name] = entity.properties[name].getValue(viewer.clock.currentTime);
525
+ } catch (e) {
526
+ }
527
+ });
528
+ if (Object.keys(props).length > 0) {
529
+ entityData.properties = props;
530
+ }
531
+ }
532
+ }
533
+ return entityData;
534
+ } catch (error2) {
535
+ warn(PREFIX, "Error picking entity:", error2);
536
+ return null;
537
+ }
538
+ }
307
539
  function sendInteractionEvent(type, additionalData = {}) {
308
540
  if (isDestroyed) {
309
541
  log(PREFIX, "Skipping interaction event - destroyed:", type);
@@ -331,88 +563,37 @@ function setupViewerListeners(viewer, model, container, Cesium) {
331
563
  }
332
564
  const camera = viewer.camera;
333
565
  camera.moveEnd.addEventListener(() => {
334
- if (isDestroyed || !viewer)
335
- return;
566
+ if (isDestroyed || !viewer) return;
336
567
  sendInteractionEvent("camera_move");
337
568
  });
338
569
  const scene = viewer.scene;
339
570
  const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
340
571
  handler.setInputAction((click) => {
341
- if (isDestroyed || !viewer || !viewer.scene || !viewer.camera)
342
- return;
572
+ if (isDestroyed || !viewer || !viewer.scene || !viewer.camera) return;
343
573
  const pickedData = {};
344
- try {
345
- const ray = viewer.camera.getPickRay(click.position);
346
- if (ray && viewer.scene.globe) {
347
- const cartesian = viewer.scene.globe.pick(ray, viewer.scene);
348
- if (cartesian) {
349
- const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
350
- pickedData.picked_position = {
351
- latitude: Cesium.Math.toDegrees(cartographic.latitude),
352
- longitude: Cesium.Math.toDegrees(cartographic.longitude),
353
- altitude: cartographic.height
354
- };
355
- }
356
- }
357
- } catch (error2) {
358
- warn(PREFIX, "Error picking position:", error2);
574
+ const pickedPosition = getPickedPosition(click);
575
+ if (pickedPosition) {
576
+ pickedData.picked_position = pickedPosition;
359
577
  }
360
- try {
361
- const pickedObject = viewer.scene.pick(click.position);
362
- if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
363
- const entity = pickedObject.id;
364
- pickedData.picked_entity = {
365
- id: entity.id,
366
- name: entity.name || null
367
- };
368
- if (entity.properties) {
369
- const props = {};
370
- const propertyNames = entity.properties.propertyNames;
371
- if (propertyNames && propertyNames.length > 0) {
372
- propertyNames.forEach((name) => {
373
- try {
374
- props[name] = entity.properties[name].getValue(viewer.clock.currentTime);
375
- } catch (e) {
376
- }
377
- });
378
- if (Object.keys(props).length > 0) {
379
- pickedData.picked_entity.properties = props;
380
- }
381
- }
382
- }
383
- }
384
- } catch (error2) {
385
- warn(PREFIX, "Error picking entity:", error2);
578
+ const pickedEntity = getPickedEntity(click);
579
+ if (pickedEntity) {
580
+ pickedData.picked_entity = pickedEntity;
386
581
  }
387
582
  sendInteractionEvent("left_click", pickedData);
388
583
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
389
584
  handler.setInputAction((click) => {
390
- if (isDestroyed || !viewer || !viewer.scene || !viewer.camera)
391
- return;
585
+ if (isDestroyed || !viewer || !viewer.scene || !viewer.camera) return;
392
586
  const pickedData = {};
393
- try {
394
- const ray = viewer.camera.getPickRay(click.position);
395
- if (ray && viewer.scene.globe) {
396
- const cartesian = viewer.scene.globe.pick(ray, viewer.scene);
397
- if (cartesian) {
398
- const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
399
- pickedData.picked_position = {
400
- latitude: Cesium.Math.toDegrees(cartographic.latitude),
401
- longitude: Cesium.Math.toDegrees(cartographic.longitude),
402
- altitude: cartographic.height
403
- };
404
- }
405
- }
406
- } catch (error2) {
407
- warn(PREFIX, "Error picking position:", error2);
587
+ const pickedPosition = getPickedPosition(click);
588
+ if (pickedPosition) {
589
+ pickedData.picked_position = pickedPosition;
408
590
  }
409
591
  sendInteractionEvent("right_click", pickedData);
410
592
  }, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
411
593
  if (viewer.timeline) {
412
594
  let timelineScrubbing = false;
413
595
  viewer.clock.onTick.addEventListener(() => {
414
- if (isDestroyed)
415
- return;
596
+ if (isDestroyed) return;
416
597
  if (viewer.timeline) {
417
598
  if (scrubTimeout) {
418
599
  clearTimeout(scrubTimeout);
@@ -423,12 +604,26 @@ function setupViewerListeners(viewer, model, container, Cesium) {
423
604
  timelineScrubbing = false;
424
605
  sendInteractionEvent("timeline_scrub");
425
606
  }
426
- }, 500);
607
+ }, CONSTANTS.TIMELINE_SCRUB_DEBOUNCE_MS);
427
608
  timelineScrubbing = true;
428
609
  }
429
610
  });
430
611
  }
431
612
  log(PREFIX, "Viewer listeners setup complete");
613
+ return {
614
+ destroy: () => {
615
+ log(PREFIX, "Destroying viewer listeners");
616
+ isDestroyed = true;
617
+ if (scrubTimeout) {
618
+ clearTimeout(scrubTimeout);
619
+ scrubTimeout = null;
620
+ }
621
+ if (handler) {
622
+ handler.destroy();
623
+ }
624
+ log(PREFIX, "Viewer listeners destroyed");
625
+ }
626
+ };
432
627
  }
433
628
  function setupGeoJSONLoader(viewer, model, Cesium) {
434
629
  log(PREFIX, "Setting up GeoJSON loader");
@@ -457,10 +652,10 @@ function setupGeoJSONLoader(viewer, model, Cesium) {
457
652
  log(PREFIX, "Loading GeoJSON dataset...");
458
653
  const dataSource = await Cesium.GeoJsonDataSource.load(geojsonData, {
459
654
  stroke: Cesium.Color.HOTPINK,
460
- fill: Cesium.Color.PINK.withAlpha(0.5),
461
- strokeWidth: 3
655
+ fill: Cesium.Color.PINK.withAlpha(CONSTANTS.GEOJSON_FILL_ALPHA),
656
+ strokeWidth: CONSTANTS.GEOJSON_STROKE_WIDTH
462
657
  });
463
- if (viewer && viewer.dataSources) {
658
+ if (!isDestroyed && viewer && viewer.dataSources) {
464
659
  viewer.dataSources.add(dataSource);
465
660
  geojsonDataSources.push(dataSource);
466
661
  log(PREFIX, "GeoJSON dataset loaded successfully");
@@ -498,6 +693,7 @@ function setupCZMLLoader(viewer, model, Cesium) {
498
693
  log(PREFIX, "Setting up CZML loader");
499
694
  let czmlDataSources = [];
500
695
  let isDestroyed = false;
696
+ viewer._czmlDataSources = czmlDataSources;
501
697
  async function loadCZMLData(flyToData = true) {
502
698
  if (isDestroyed) {
503
699
  log(PREFIX, "Skipping czml_data load - destroyed");
@@ -521,7 +717,7 @@ function setupCZMLLoader(viewer, model, Cesium) {
521
717
  try {
522
718
  log(PREFIX, "Loading CZML document with", czmlData.length, "packets...");
523
719
  const dataSource = await Cesium.CzmlDataSource.load(czmlData);
524
- if (viewer && viewer.dataSources) {
720
+ if (!isDestroyed && viewer && viewer.dataSources) {
525
721
  viewer.dataSources.add(dataSource);
526
722
  czmlDataSources.push(dataSource);
527
723
  log(PREFIX, "CZML document loaded successfully, entities:", dataSource.entities.values.length);
@@ -558,20 +754,109 @@ function setupCZMLLoader(viewer, model, Cesium) {
558
754
  }
559
755
  };
560
756
  }
757
+ function setupPhotorealisticTiles(viewer, model, Cesium) {
758
+ log(PREFIX, "Setting up Photorealistic 3D Tiles loader");
759
+ let photorealisticTileset = null;
760
+ let isDestroyed = false;
761
+ async function updatePhotorealisticTiles() {
762
+ if (isDestroyed) {
763
+ log(PREFIX, "Skipping photorealistic tiles update - destroyed");
764
+ return;
765
+ }
766
+ if (!viewer || !viewer.scene || !viewer.scene.primitives) {
767
+ warn(PREFIX, "Cannot update photorealistic tiles - viewer not available");
768
+ return;
769
+ }
770
+ const enabled = model.get("enable_photorealistic_tiles");
771
+ log(PREFIX, "Photorealistic tiles enabled:", enabled);
772
+ if (photorealisticTileset) {
773
+ log(PREFIX, "Removing existing photorealistic tileset");
774
+ viewer.scene.primitives.remove(photorealisticTileset);
775
+ photorealisticTileset = null;
776
+ }
777
+ if (enabled) {
778
+ try {
779
+ log(PREFIX, "Creating Google Photorealistic 3D Tileset...");
780
+ photorealisticTileset = await Cesium.createGooglePhotorealistic3DTileset();
781
+ if (viewer && viewer.scene && viewer.scene.primitives && !isDestroyed) {
782
+ viewer.scene.primitives.add(photorealisticTileset);
783
+ log(PREFIX, "Google Photorealistic 3D Tileset loaded successfully");
784
+ } else {
785
+ log(PREFIX, "Viewer destroyed during tileset load, skipping add");
786
+ photorealisticTileset = null;
787
+ }
788
+ } catch (err) {
789
+ error(PREFIX, "Failed to load Google Photorealistic 3D Tileset:", err);
790
+ photorealisticTileset = null;
791
+ }
792
+ }
793
+ }
794
+ model.on("change:enable_photorealistic_tiles", () => updatePhotorealisticTiles());
795
+ if (model.get("enable_photorealistic_tiles")) {
796
+ log(PREFIX, "Loading initial photorealistic tiles...");
797
+ updatePhotorealisticTiles();
798
+ }
799
+ return {
800
+ destroy: () => {
801
+ log(PREFIX, "Destroying photorealistic tiles loader");
802
+ isDestroyed = true;
803
+ if (photorealisticTileset && viewer && viewer.scene && viewer.scene.primitives) {
804
+ viewer.scene.primitives.remove(photorealisticTileset);
805
+ }
806
+ photorealisticTileset = null;
807
+ }
808
+ };
809
+ }
561
810
 
562
811
  // src/cesiumjs_anywidget/js/camera-sync.js
563
812
  var PREFIX2 = "CameraSync";
564
813
  function initializeCameraSync(viewer, model) {
565
814
  const Cesium = window.Cesium;
566
815
  let cameraUpdateTimeout = null;
816
+ let modelUpdateTimeout = null;
567
817
  let isDestroyed = false;
568
818
  let syncEnabled = model.get("camera_sync_enabled") || false;
569
819
  log(PREFIX2, "Initializing camera synchronization, sync enabled:", syncEnabled);
570
- model.on("change:camera_sync_enabled", () => {
820
+ if (!Cesium) {
821
+ error(PREFIX2, "Cesium global not available");
822
+ throw new Error("Cesium is not loaded");
823
+ }
824
+ const handleSyncEnabledChange = () => {
571
825
  syncEnabled = model.get("camera_sync_enabled");
572
826
  log(PREFIX2, "Camera sync enabled changed:", syncEnabled);
573
- });
574
- function updateCameraFromModel() {
827
+ };
828
+ model.on("change:camera_sync_enabled", handleSyncEnabledChange);
829
+ function validateCameraParameters(lat, lon, alt) {
830
+ if (typeof lat !== "number" || isNaN(lat) || lat < -90 || lat > 90) {
831
+ warn(PREFIX2, "Invalid latitude:", lat);
832
+ return false;
833
+ }
834
+ if (typeof lon !== "number" || isNaN(lon)) {
835
+ warn(PREFIX2, "Invalid longitude:", lon);
836
+ return false;
837
+ }
838
+ if (typeof alt !== "number" || isNaN(alt)) {
839
+ warn(PREFIX2, "Invalid altitude:", alt);
840
+ return false;
841
+ }
842
+ return true;
843
+ }
844
+ function validateOrientationAngles(heading, pitch, roll) {
845
+ if (typeof heading !== "number" || isNaN(heading)) {
846
+ warn(PREFIX2, "Invalid heading:", heading);
847
+ return false;
848
+ }
849
+ if (typeof pitch !== "number" || isNaN(pitch)) {
850
+ warn(PREFIX2, "Invalid pitch:", pitch);
851
+ return false;
852
+ }
853
+ if (typeof roll !== "number" || isNaN(roll)) {
854
+ warn(PREFIX2, "Invalid roll:", roll);
855
+ return false;
856
+ }
857
+ return true;
858
+ }
859
+ function updateCameraFromModelImmediate() {
575
860
  if (isDestroyed) {
576
861
  log(PREFIX2, "Skipping updateCameraFromModel - module destroyed");
577
862
  return;
@@ -583,14 +868,38 @@ function initializeCameraSync(viewer, model) {
583
868
  const lat = model.get("latitude");
584
869
  const lon = model.get("longitude");
585
870
  const alt = model.get("altitude");
586
- const heading = Cesium.Math.toRadians(model.get("heading"));
587
- const pitch = Cesium.Math.toRadians(model.get("pitch"));
588
- const roll = Cesium.Math.toRadians(model.get("roll"));
871
+ const heading = model.get("heading");
872
+ const pitch = model.get("pitch");
873
+ const roll = model.get("roll");
874
+ if (!validateCameraParameters(lat, lon, alt)) {
875
+ return;
876
+ }
877
+ if (!validateOrientationAngles(heading, pitch, roll)) {
878
+ return;
879
+ }
589
880
  log(PREFIX2, "Updating camera from model:", { lat, lon, alt });
590
- viewer.camera.setView({
591
- destination: Cesium.Cartesian3.fromDegrees(lon, lat, alt),
592
- orientation: { heading, pitch, roll }
593
- });
881
+ try {
882
+ viewer.camera.setView({
883
+ destination: Cesium.Cartesian3.fromDegrees(lon, lat, alt),
884
+ orientation: {
885
+ heading: Cesium.Math.toRadians(heading),
886
+ pitch: Cesium.Math.toRadians(pitch),
887
+ roll: Cesium.Math.toRadians(roll)
888
+ }
889
+ });
890
+ } catch (err) {
891
+ error(PREFIX2, "Error updating camera from model:", err);
892
+ }
893
+ }
894
+ function updateCameraFromModel() {
895
+ if (modelUpdateTimeout) {
896
+ clearTimeout(modelUpdateTimeout);
897
+ }
898
+ modelUpdateTimeout = setTimeout(() => {
899
+ if (!isDestroyed) {
900
+ updateCameraFromModelImmediate();
901
+ }
902
+ }, 50);
594
903
  }
595
904
  function updateModelFromCamera() {
596
905
  if (isDestroyed) {
@@ -639,7 +948,7 @@ function initializeCameraSync(viewer, model) {
639
948
  }
640
949
  }, 500);
641
950
  }
642
- updateCameraFromModel();
951
+ updateCameraFromModelImmediate();
643
952
  viewer.camera.changed.addEventListener(handleCameraChanged);
644
953
  model.on("change:latitude", updateCameraFromModel);
645
954
  model.on("change:longitude", updateCameraFromModel);
@@ -647,19 +956,22 @@ function initializeCameraSync(viewer, model) {
647
956
  model.on("change:heading", updateCameraFromModel);
648
957
  model.on("change:pitch", updateCameraFromModel);
649
958
  model.on("change:roll", updateCameraFromModel);
650
- model.on("change:camera_command", () => {
959
+ const handleCameraCommand = () => {
651
960
  if (isDestroyed) {
652
961
  log(PREFIX2, "Skipping camera_command - module destroyed");
653
962
  return;
654
963
  }
655
964
  const command = model.get("camera_command");
656
- if (!command || !command.command || !command.timestamp)
657
- return;
965
+ if (!command || !command.command || !command.timestamp) return;
658
966
  const cmd = command.command;
659
967
  log(PREFIX2, "Executing camera command:", cmd, command);
660
968
  try {
661
969
  switch (cmd) {
662
970
  case "flyTo":
971
+ if (!validateCameraParameters(command.latitude, command.longitude, command.altitude)) {
972
+ error(PREFIX2, "Invalid parameters for flyTo command");
973
+ return;
974
+ }
663
975
  viewer.camera.flyTo({
664
976
  destination: Cesium.Cartesian3.fromDegrees(
665
977
  command.longitude,
@@ -675,6 +987,10 @@ function initializeCameraSync(viewer, model) {
675
987
  });
676
988
  break;
677
989
  case "setView":
990
+ if (!validateCameraParameters(command.latitude, command.longitude, command.altitude)) {
991
+ error(PREFIX2, "Invalid parameters for setView command");
992
+ return;
993
+ }
678
994
  viewer.camera.setView({
679
995
  destination: Cesium.Cartesian3.fromDegrees(
680
996
  command.longitude,
@@ -687,8 +1003,35 @@ function initializeCameraSync(viewer, model) {
687
1003
  roll: Cesium.Math.toRadians(command.roll || 0)
688
1004
  }
689
1005
  });
1006
+ if (command.fov !== void 0 || command.aspectRatio !== void 0 || command.near !== void 0 || command.far !== void 0) {
1007
+ const frustum = viewer.camera.frustum;
1008
+ if (frustum instanceof Cesium.PerspectiveFrustum) {
1009
+ if (command.fov !== void 0) {
1010
+ frustum.fov = Cesium.Math.toRadians(command.fov);
1011
+ }
1012
+ if (command.aspectRatio !== void 0) {
1013
+ frustum.aspectRatio = command.aspectRatio;
1014
+ }
1015
+ if (command.near !== void 0) {
1016
+ frustum.near = command.near;
1017
+ }
1018
+ if (command.far !== void 0) {
1019
+ frustum.far = command.far;
1020
+ }
1021
+ log(PREFIX2, "Applied frustum parameters:", {
1022
+ fov: command.fov,
1023
+ aspectRatio: command.aspectRatio,
1024
+ near: command.near,
1025
+ far: command.far
1026
+ });
1027
+ }
1028
+ }
690
1029
  break;
691
1030
  case "lookAt":
1031
+ if (!validateCameraParameters(command.targetLatitude, command.targetLongitude, command.targetAltitude || 0)) {
1032
+ error(PREFIX2, "Invalid parameters for lookAt command");
1033
+ return;
1034
+ }
692
1035
  const target = Cesium.Cartesian3.fromDegrees(
693
1036
  command.targetLongitude,
694
1037
  command.targetLatitude,
@@ -744,9 +1087,11 @@ function initializeCameraSync(viewer, model) {
744
1087
  } catch (err) {
745
1088
  error(PREFIX2, `Error executing camera command ${cmd}:`, err);
746
1089
  }
747
- });
1090
+ };
1091
+ model.on("change:camera_command", handleCameraCommand);
748
1092
  return {
749
1093
  updateCameraFromModel,
1094
+ updateCameraFromModelImmediate,
750
1095
  updateModelFromCamera,
751
1096
  destroy: () => {
752
1097
  log(PREFIX2, "Destroying camera sync module");
@@ -755,7 +1100,21 @@ function initializeCameraSync(viewer, model) {
755
1100
  clearTimeout(cameraUpdateTimeout);
756
1101
  cameraUpdateTimeout = null;
757
1102
  }
758
- viewer.camera.changed.removeEventListener(handleCameraChanged);
1103
+ if (modelUpdateTimeout) {
1104
+ clearTimeout(modelUpdateTimeout);
1105
+ modelUpdateTimeout = null;
1106
+ }
1107
+ if (viewer && viewer.camera) {
1108
+ viewer.camera.changed.removeEventListener(handleCameraChanged);
1109
+ }
1110
+ model.off("change:camera_sync_enabled", handleSyncEnabledChange);
1111
+ model.off("change:latitude", updateCameraFromModel);
1112
+ model.off("change:longitude", updateCameraFromModel);
1113
+ model.off("change:altitude", updateCameraFromModel);
1114
+ model.off("change:heading", updateCameraFromModel);
1115
+ model.off("change:pitch", updateCameraFromModel);
1116
+ model.off("change:roll", updateCameraFromModel);
1117
+ model.off("change:camera_command", handleCameraCommand);
759
1118
  log(PREFIX2, "Camera sync module destroyed");
760
1119
  }
761
1120
  };
@@ -763,6 +1122,210 @@ function initializeCameraSync(viewer, model) {
763
1122
 
764
1123
  // src/cesiumjs_anywidget/js/measurement-tools.js
765
1124
  var PREFIX3 = "Measurements";
1125
+ var CONSTANTS2 = {
1126
+ // UI Dimensions
1127
+ MARKER_SIZE: 10,
1128
+ MARKER_SIZE_HOVER: 12,
1129
+ MARKER_SIZE_SELECTED: 15,
1130
+ MARKER_OUTLINE_WIDTH: 2,
1131
+ MARKER_OUTLINE_WIDTH_HOVER: 3,
1132
+ MARKER_OUTLINE_WIDTH_SELECTED: 4,
1133
+ // Panel Dimensions
1134
+ PANEL_MIN_WIDTH: 250,
1135
+ PANEL_MIN_HEIGHT: 200,
1136
+ PANEL_MAX_WIDTH: 600,
1137
+ PANEL_MAX_HEIGHT: 800,
1138
+ // Measurement Thresholds
1139
+ MAX_ADD_POINT_DISTANCE_METERS: 50,
1140
+ AREA_UNIT_THRESHOLD: 1e6,
1141
+ // m² to km² conversion
1142
+ DISTANCE_UNIT_THRESHOLD: 1e3,
1143
+ // m to km conversion
1144
+ // Visual
1145
+ POLYLINE_WIDTH: 3,
1146
+ LABEL_PIXEL_OFFSET_Y: -20,
1147
+ POLYGON_ALPHA: 0.3,
1148
+ POLYGON_OUTLINE_WIDTH: 2
1149
+ };
1150
+ function getColors() {
1151
+ const Cesium = window.Cesium;
1152
+ return {
1153
+ distance: { main: Cesium.Color.RED, button: "#e74c3c" },
1154
+ "multi-distance": { main: Cesium.Color.BLUE, button: "#3498db" },
1155
+ height: { main: Cesium.Color.GREEN, button: "#2ecc71" },
1156
+ area: { main: Cesium.Color.ORANGE, button: "#e67e22" }
1157
+ };
1158
+ }
1159
+ var TYPE_LABELS = {
1160
+ distance: "Distance",
1161
+ "multi-distance": "Multi-Distance",
1162
+ height: "Height",
1163
+ area: "Area"
1164
+ };
1165
+ function calculateGeodesicArea(positions) {
1166
+ const Cesium = window.Cesium;
1167
+ const polygonHierarchy = new Cesium.PolygonHierarchy(positions);
1168
+ const geometry = Cesium.PolygonGeometry.createGeometry(
1169
+ new Cesium.PolygonGeometry({
1170
+ polygonHierarchy,
1171
+ perPositionHeight: false,
1172
+ arcType: Cesium.ArcType.GEODESIC
1173
+ })
1174
+ );
1175
+ if (!geometry) return 0;
1176
+ const positionsArray = geometry.attributes.position.values;
1177
+ const indices = geometry.indices;
1178
+ let area = 0;
1179
+ for (let i = 0; i < indices.length; i += 3) {
1180
+ const i0 = indices[i] * 3;
1181
+ const i1 = indices[i + 1] * 3;
1182
+ const i2 = indices[i + 2] * 3;
1183
+ const v0 = new Cesium.Cartesian3(positionsArray[i0], positionsArray[i0 + 1], positionsArray[i0 + 2]);
1184
+ const v1 = new Cesium.Cartesian3(positionsArray[i1], positionsArray[i1 + 1], positionsArray[i1 + 2]);
1185
+ const v2 = new Cesium.Cartesian3(positionsArray[i2], positionsArray[i2 + 1], positionsArray[i2 + 2]);
1186
+ const edge1 = Cesium.Cartesian3.subtract(v1, v0, new Cesium.Cartesian3());
1187
+ const edge2 = Cesium.Cartesian3.subtract(v2, v0, new Cesium.Cartesian3());
1188
+ const crossProduct = Cesium.Cartesian3.cross(edge1, edge2, new Cesium.Cartesian3());
1189
+ area += Cesium.Cartesian3.magnitude(crossProduct) / 2;
1190
+ }
1191
+ return area;
1192
+ }
1193
+ function calculateCentroid(positions) {
1194
+ const Cesium = window.Cesium;
1195
+ let centroidLon = 0, centroidLat = 0;
1196
+ positions.forEach((pos) => {
1197
+ const carto = Cesium.Cartographic.fromCartesian(pos);
1198
+ centroidLon += carto.longitude;
1199
+ centroidLat += carto.latitude;
1200
+ });
1201
+ return new Cesium.Cartographic(centroidLon / positions.length, centroidLat / positions.length);
1202
+ }
1203
+ function formatValue(value, isArea = false) {
1204
+ if (isArea) {
1205
+ return value >= CONSTANTS2.AREA_UNIT_THRESHOLD ? `${(value / CONSTANTS2.AREA_UNIT_THRESHOLD).toFixed(2)} km\xB2` : `${value.toFixed(2)} m\xB2`;
1206
+ }
1207
+ return value >= CONSTANTS2.DISTANCE_UNIT_THRESHOLD ? `${(value / CONSTANTS2.DISTANCE_UNIT_THRESHOLD).toFixed(2)} km` : `${value.toFixed(2)} m`;
1208
+ }
1209
+ function createStyledButton(text, baseColor, activeColor = "#e74c3c") {
1210
+ const btn = document.createElement("button");
1211
+ btn.textContent = text;
1212
+ btn.dataset.baseColor = baseColor;
1213
+ btn.dataset.activeColor = activeColor;
1214
+ btn.style.cssText = `
1215
+ padding: 8px 12px;
1216
+ background: ${baseColor};
1217
+ color: white;
1218
+ border: none;
1219
+ border-radius: 3px;
1220
+ cursor: pointer;
1221
+ font-size: 12px;
1222
+ transition: background 0.2s;
1223
+ `;
1224
+ return btn;
1225
+ }
1226
+ function makeDraggable(panel, handle) {
1227
+ let isDragging = false;
1228
+ let startX, startY, initialLeft, initialTop;
1229
+ handle.style.cursor = "move";
1230
+ const handleMouseDown = (e) => {
1231
+ if (e.button !== 0 || e.target.tagName === "BUTTON" || e.target.tagName === "INPUT") return;
1232
+ isDragging = true;
1233
+ startX = e.clientX;
1234
+ startY = e.clientY;
1235
+ const rect = panel.getBoundingClientRect();
1236
+ const parentRect = panel.parentElement.getBoundingClientRect();
1237
+ initialLeft = rect.left - parentRect.left;
1238
+ initialTop = rect.top - parentRect.top;
1239
+ panel.style.right = "auto";
1240
+ panel.style.bottom = "auto";
1241
+ panel.style.left = initialLeft + "px";
1242
+ panel.style.top = initialTop + "px";
1243
+ e.preventDefault();
1244
+ };
1245
+ const handleMouseMove = (e) => {
1246
+ if (!isDragging) return;
1247
+ const deltaX = e.clientX - startX;
1248
+ const deltaY = e.clientY - startY;
1249
+ const parentRect = panel.parentElement.getBoundingClientRect();
1250
+ const panelRect = panel.getBoundingClientRect();
1251
+ let newLeft = initialLeft + deltaX;
1252
+ let newTop = initialTop + deltaY;
1253
+ newLeft = Math.max(0, Math.min(newLeft, parentRect.width - panelRect.width));
1254
+ newTop = Math.max(0, Math.min(newTop, parentRect.height - panelRect.height));
1255
+ panel.style.left = newLeft + "px";
1256
+ panel.style.top = newTop + "px";
1257
+ };
1258
+ const handleMouseUp = () => {
1259
+ isDragging = false;
1260
+ };
1261
+ handle.addEventListener("mousedown", handleMouseDown);
1262
+ document.addEventListener("mousemove", handleMouseMove);
1263
+ document.addEventListener("mouseup", handleMouseUp);
1264
+ return () => {
1265
+ handle.removeEventListener("mousedown", handleMouseDown);
1266
+ document.removeEventListener("mousemove", handleMouseMove);
1267
+ document.removeEventListener("mouseup", handleMouseUp);
1268
+ };
1269
+ }
1270
+ function makeResizable(panel, minWidth = CONSTANTS2.PANEL_MIN_WIDTH, minHeight = CONSTANTS2.PANEL_MIN_HEIGHT, maxWidth = CONSTANTS2.PANEL_MAX_WIDTH, maxHeight = CONSTANTS2.PANEL_MAX_HEIGHT) {
1271
+ const resizeHandle = document.createElement("div");
1272
+ resizeHandle.style.cssText = `
1273
+ position: absolute;
1274
+ bottom: 2px;
1275
+ right: 2px;
1276
+ width: 20px;
1277
+ height: 20px;
1278
+ cursor: nwse-resize;
1279
+ z-index: 1002;
1280
+ `;
1281
+ resizeHandle.innerHTML = `
1282
+ <svg width="20" height="20" viewBox="0 0 20 20" style="pointer-events: none;">
1283
+ <path d="M 20 0 L 20 20 L 0 20" fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="1"/>
1284
+ <path d="M 14 20 L 20 14" stroke="rgba(255,255,255,0.5)" stroke-width="2"/>
1285
+ <path d="M 8 20 L 20 8" stroke="rgba(255,255,255,0.5)" stroke-width="2"/>
1286
+ </svg>
1287
+ `;
1288
+ panel.appendChild(resizeHandle);
1289
+ let isResizing = false;
1290
+ let startX, startY, startWidth, startHeight;
1291
+ const handleMouseDown = (e) => {
1292
+ isResizing = true;
1293
+ startX = e.clientX;
1294
+ startY = e.clientY;
1295
+ const rect = panel.getBoundingClientRect();
1296
+ startWidth = rect.width;
1297
+ startHeight = rect.height;
1298
+ e.preventDefault();
1299
+ e.stopPropagation();
1300
+ };
1301
+ const handleMouseMove = (e) => {
1302
+ if (!isResizing) return;
1303
+ const deltaX = e.clientX - startX;
1304
+ const deltaY = e.clientY - startY;
1305
+ let newWidth = startWidth + deltaX;
1306
+ let newHeight = startHeight + deltaY;
1307
+ newWidth = Math.max(minWidth, Math.min(newWidth, maxWidth));
1308
+ newHeight = Math.max(minHeight, Math.min(newHeight, maxHeight));
1309
+ panel.style.width = newWidth + "px";
1310
+ panel.style.height = newHeight + "px";
1311
+ panel.style.maxWidth = "none";
1312
+ panel.style.maxHeight = "none";
1313
+ };
1314
+ const handleMouseUp = () => {
1315
+ isResizing = false;
1316
+ };
1317
+ resizeHandle.addEventListener("mousedown", handleMouseDown);
1318
+ document.addEventListener("mousemove", handleMouseMove);
1319
+ document.addEventListener("mouseup", handleMouseUp);
1320
+ return () => {
1321
+ resizeHandle.removeEventListener("mousedown", handleMouseDown);
1322
+ document.removeEventListener("mousemove", handleMouseMove);
1323
+ document.removeEventListener("mouseup", handleMouseUp);
1324
+ if (resizeHandle.parentNode) {
1325
+ resizeHandle.remove();
1326
+ }
1327
+ };
1328
+ }
766
1329
  function initializeMeasurementTools(viewer, model, container) {
767
1330
  log(PREFIX3, "Initializing measurement tools");
768
1331
  const Cesium = window.Cesium;
@@ -784,121 +1347,106 @@ function initializeMeasurementTools(viewer, model, container) {
784
1347
  selectedEntity: null,
785
1348
  dragging: false,
786
1349
  measurementIndex: null,
787
- pointIndex: null
1350
+ pointIndex: null,
1351
+ addPointMode: false
788
1352
  };
789
- let completedMeasurements = [];
1353
+ const cleanupFunctions = [];
790
1354
  const toolbarDiv = document.createElement("div");
791
1355
  toolbarDiv.style.cssText = `
792
1356
  position: absolute;
793
1357
  top: 10px;
794
1358
  left: 10px;
795
1359
  background: rgba(42, 42, 42, 0.9);
796
- padding: 10px;
1360
+ padding: 0;
797
1361
  border-radius: 5px;
798
1362
  z-index: 1000;
799
1363
  display: flex;
800
1364
  flex-direction: column;
801
- gap: 5px;
1365
+ gap: 0;
802
1366
  `;
803
1367
  container.appendChild(toolbarDiv);
804
- function createMeasurementButton(text, mode) {
805
- const btn = document.createElement("button");
806
- btn.textContent = text;
807
- btn.style.cssText = `
808
- padding: 8px 12px;
809
- background: #3498db;
810
- color: white;
811
- border: none;
812
- border-radius: 3px;
813
- cursor: pointer;
814
- font-size: 12px;
815
- transition: background 0.2s;
816
- `;
1368
+ const toolbarHeader = document.createElement("div");
1369
+ toolbarHeader.style.cssText = `
1370
+ padding: 8px 10px;
1371
+ background: rgba(60, 60, 60, 0.95);
1372
+ border-radius: 5px 5px 0 0;
1373
+ font-family: sans-serif;
1374
+ font-size: 11px;
1375
+ color: #aaa;
1376
+ border-bottom: 1px solid #555;
1377
+ user-select: none;
1378
+ `;
1379
+ toolbarHeader.textContent = "\u22EE\u22EE Measurement Tools";
1380
+ toolbarDiv.appendChild(toolbarHeader);
1381
+ const toolbarContent = document.createElement("div");
1382
+ toolbarContent.style.cssText = `
1383
+ padding: 10px;
1384
+ display: flex;
1385
+ flex-direction: column;
1386
+ gap: 5px;
1387
+ `;
1388
+ toolbarDiv.appendChild(toolbarContent);
1389
+ cleanupFunctions.push(makeDraggable(toolbarDiv, toolbarHeader));
1390
+ function setupButtonHover(btn, hoverColor, getActiveState = () => false) {
1391
+ const baseColor = btn.dataset.baseColor;
817
1392
  btn.onmouseover = () => {
818
- btn.style.background = "#2980b9";
1393
+ btn.style.background = hoverColor;
819
1394
  };
820
1395
  btn.onmouseout = () => {
821
- btn.style.background = measurementState.mode === mode ? "#e74c3c" : "#3498db";
1396
+ btn.style.background = getActiveState() ? btn.dataset.activeColor : baseColor;
822
1397
  };
1398
+ }
1399
+ function createMeasurementButton(text, mode) {
1400
+ const btn = createStyledButton(text, "#3498db");
1401
+ setupButtonHover(btn, "#2980b9", () => measurementState.mode === mode);
823
1402
  btn.onclick = () => {
824
- if (measurementState.mode === mode) {
825
- model.set("measurement_mode", "");
826
- model.save_changes();
827
- } else {
828
- model.set("measurement_mode", mode);
829
- model.save_changes();
830
- }
1403
+ model.set("measurement_mode", measurementState.mode === mode ? "" : mode);
1404
+ model.save_changes();
831
1405
  };
832
1406
  return btn;
833
1407
  }
834
- const distanceBtn = createMeasurementButton("\u{1F4CF} Distance", "distance");
835
- const multiDistanceBtn = createMeasurementButton("\u{1F4D0} Multi Distance", "multi-distance");
836
- const heightBtn = createMeasurementButton("\u{1F4CA} Height", "height");
837
- const areaBtn = createMeasurementButton("\u2B1B Area", "area");
838
- const clearBtn = document.createElement("button");
839
- clearBtn.textContent = "\u{1F5D1}\uFE0F Clear";
840
- clearBtn.style.cssText = `
841
- padding: 8px 12px;
842
- background: #e74c3c;
843
- color: white;
844
- border: none;
845
- border-radius: 3px;
846
- cursor: pointer;
847
- font-size: 12px;
848
- transition: background 0.2s;
849
- `;
850
- clearBtn.onmouseover = () => {
851
- clearBtn.style.background = "#c0392b";
852
- };
853
- clearBtn.onmouseout = () => {
854
- clearBtn.style.background = "#e74c3c";
1408
+ const modeButtons = {
1409
+ distance: createMeasurementButton("\u{1F4CF} Distance", "distance"),
1410
+ "multi-distance": createMeasurementButton("\u{1F4D0} Multi Distance", "multi-distance"),
1411
+ height: createMeasurementButton("\u{1F4CA} Height", "height"),
1412
+ area: createMeasurementButton("\u2B1B Area", "area")
855
1413
  };
1414
+ const clearBtn = createStyledButton("\u{1F5D1}\uFE0F Clear", "#e74c3c");
1415
+ setupButtonHover(clearBtn, "#c0392b");
856
1416
  clearBtn.onclick = () => {
857
1417
  clearAllMeasurements();
858
1418
  model.set("measurement_mode", "");
859
1419
  model.set("measurement_results", []);
860
1420
  model.save_changes();
861
1421
  };
862
- toolbarDiv.appendChild(distanceBtn);
863
- toolbarDiv.appendChild(multiDistanceBtn);
864
- toolbarDiv.appendChild(heightBtn);
865
- toolbarDiv.appendChild(areaBtn);
866
- toolbarDiv.appendChild(clearBtn);
867
- const editBtn = document.createElement("button");
868
- editBtn.textContent = "\u270F\uFE0F Edit Points";
869
- editBtn.style.cssText = `
870
- padding: 8px 12px;
871
- background: #9b59b6;
872
- color: white;
873
- border: none;
874
- border-radius: 3px;
875
- cursor: pointer;
876
- font-size: 12px;
877
- transition: background 0.2s;
878
- `;
879
- editBtn.onmouseover = () => {
880
- editBtn.style.background = "#8e44ad";
881
- };
882
- editBtn.onmouseout = () => {
883
- editBtn.style.background = editState.enabled ? "#e74c3c" : "#9b59b6";
884
- };
1422
+ Object.values(modeButtons).forEach((btn) => toolbarContent.appendChild(btn));
1423
+ toolbarContent.appendChild(clearBtn);
1424
+ const editBtn = createStyledButton("\u270F\uFE0F Edit Points", "#9b59b6");
1425
+ setupButtonHover(editBtn, "#8e44ad", () => editState.enabled);
885
1426
  editBtn.onclick = () => {
886
1427
  editState.enabled = !editState.enabled;
887
1428
  editBtn.style.background = editState.enabled ? "#e74c3c" : "#9b59b6";
888
- if (editState.enabled) {
889
- enableEditMode();
890
- } else {
891
- disableEditMode();
1429
+ editState.enabled ? enableEditMode() : disableEditMode();
1430
+ };
1431
+ toolbarContent.appendChild(editBtn);
1432
+ const addPointBtn = createStyledButton("\u2795 Add Point", "#16a085");
1433
+ addPointBtn.style.display = "none";
1434
+ setupButtonHover(addPointBtn, "#138d75", () => editState.addPointMode);
1435
+ addPointBtn.onclick = () => {
1436
+ editState.addPointMode = !editState.addPointMode;
1437
+ addPointBtn.style.background = editState.addPointMode ? "#e74c3c" : "#16a085";
1438
+ if (editState.addPointMode) {
1439
+ deselectPoint();
892
1440
  }
893
1441
  };
894
- toolbarDiv.appendChild(editBtn);
1442
+ toolbarContent.appendChild(addPointBtn);
895
1443
  const editorPanel = document.createElement("div");
896
1444
  editorPanel.style.cssText = `
897
1445
  position: absolute;
898
1446
  top: 10px;
899
1447
  right: 10px;
900
1448
  background: rgba(42, 42, 42, 0.95);
901
- padding: 15px;
1449
+ padding: 0;
902
1450
  border-radius: 5px;
903
1451
  z-index: 1000;
904
1452
  display: none;
@@ -907,49 +1455,108 @@ function initializeMeasurementTools(viewer, model, container) {
907
1455
  font-size: 12px;
908
1456
  min-width: 250px;
909
1457
  `;
1458
+ const editorHeader = document.createElement("div");
1459
+ editorHeader.style.cssText = `
1460
+ padding: 10px 15px;
1461
+ background: rgba(60, 60, 60, 0.95);
1462
+ border-radius: 5px 5px 0 0;
1463
+ font-weight: bold;
1464
+ border-bottom: 1px solid #555;
1465
+ user-select: none;
1466
+ cursor: move;
1467
+ `;
1468
+ editorHeader.textContent = "\u22EE\u22EE Edit Point";
1469
+ editorPanel.appendChild(editorHeader);
1470
+ const editorContent = document.createElement("div");
1471
+ editorContent.style.cssText = `padding: 15px;`;
1472
+ editorPanel.appendChild(editorContent);
910
1473
  container.appendChild(editorPanel);
1474
+ cleanupFunctions.push(makeDraggable(editorPanel, editorHeader));
911
1475
  const measurementsListPanel = document.createElement("div");
912
1476
  measurementsListPanel.style.cssText = `
913
1477
  position: absolute;
914
1478
  bottom: 10px;
915
1479
  right: 10px;
916
1480
  background: rgba(42, 42, 42, 0.95);
917
- padding: 15px;
1481
+ padding: 0;
918
1482
  border-radius: 5px;
919
1483
  z-index: 1000;
920
1484
  color: white;
921
1485
  font-family: sans-serif;
922
1486
  font-size: 12px;
923
- max-width: 350px;
924
- max-height: 400px;
925
- overflow-y: auto;
1487
+ width: 350px;
1488
+ height: 400px;
1489
+ min-width: 250px;
1490
+ min-height: 200px;
1491
+ max-width: 600px;
1492
+ max-height: 800px;
1493
+ display: flex;
1494
+ flex-direction: column;
1495
+ box-sizing: border-box;
1496
+ overflow: hidden;
926
1497
  `;
927
- measurementsListPanel.innerHTML = `
928
- <div style="font-weight: bold; border-bottom: 1px solid #555; padding-bottom: 8px; margin-bottom: 10px;">
929
- Measurements
930
- </div>
931
- <div id="measurements-list-content"></div>
1498
+ const measurementsHeader = document.createElement("div");
1499
+ measurementsHeader.style.cssText = `
1500
+ padding: 10px 15px;
1501
+ background: rgba(60, 60, 60, 0.95);
1502
+ border-radius: 5px 5px 0 0;
1503
+ font-weight: bold;
1504
+ border-bottom: 1px solid #555;
1505
+ user-select: none;
1506
+ cursor: move;
1507
+ flex-shrink: 0;
1508
+ box-sizing: border-box;
932
1509
  `;
1510
+ measurementsHeader.textContent = "\u22EE\u22EE Measurements";
1511
+ measurementsListPanel.appendChild(measurementsHeader);
1512
+ const measurementsContent = document.createElement("div");
1513
+ measurementsContent.className = "measurement-panel-scrollable";
1514
+ measurementsContent.style.cssText = `
1515
+ padding: 10px 15px 25px 15px;
1516
+ overflow-y: scroll;
1517
+ overflow-x: hidden;
1518
+ flex: 1 1 auto;
1519
+ min-height: 0;
1520
+ max-height: 100%;
1521
+ word-wrap: break-word;
1522
+ overflow-wrap: break-word;
1523
+ box-sizing: border-box;
1524
+ position: relative;
1525
+ z-index: 1;
1526
+ `;
1527
+ measurementsContent.innerHTML = `<div id="measurements-list-content" style="width: 100%; box-sizing: border-box;"></div>`;
1528
+ measurementsListPanel.appendChild(measurementsContent);
933
1529
  container.appendChild(measurementsListPanel);
1530
+ cleanupFunctions.push(makeDraggable(measurementsListPanel, measurementsHeader));
1531
+ cleanupFunctions.push(makeResizable(measurementsListPanel));
934
1532
  function getPosition(screenPosition) {
935
- const pickedObject = viewer.scene.pick(screenPosition);
936
- if (viewer.scene.pickPositionSupported && Cesium.defined(pickedObject)) {
937
- const cartesian = viewer.scene.pickPosition(screenPosition);
938
- if (Cesium.defined(cartesian)) {
939
- return cartesian;
1533
+ try {
1534
+ const pickedObject = viewer.scene.pick(screenPosition);
1535
+ if (viewer.scene.pickPositionSupported && Cesium.defined(pickedObject)) {
1536
+ const cartesian = viewer.scene.pickPosition(screenPosition);
1537
+ if (Cesium.defined(cartesian)) {
1538
+ return cartesian;
1539
+ }
940
1540
  }
1541
+ const ray = viewer.camera.getPickRay(screenPosition);
1542
+ if (!ray) {
1543
+ warn(PREFIX3, "Unable to get pick ray from camera");
1544
+ return null;
1545
+ }
1546
+ return viewer.scene.globe.pick(ray, viewer.scene);
1547
+ } catch (err) {
1548
+ error(PREFIX3, "Error getting position:", err);
1549
+ return null;
941
1550
  }
942
- const ray = viewer.camera.getPickRay(screenPosition);
943
- return viewer.scene.globe.pick(ray, viewer.scene);
944
1551
  }
945
1552
  function addMarker(position, color = Cesium.Color.RED) {
946
1553
  const marker = viewer.entities.add({
947
1554
  position,
948
1555
  point: {
949
- pixelSize: 10,
1556
+ pixelSize: CONSTANTS2.MARKER_SIZE,
950
1557
  color,
951
1558
  outlineColor: Cesium.Color.WHITE,
952
- outlineWidth: 2,
1559
+ outlineWidth: CONSTANTS2.MARKER_OUTLINE_WIDTH,
953
1560
  disableDepthTestDistance: Number.POSITIVE_INFINITY
954
1561
  }
955
1562
  });
@@ -966,7 +1573,7 @@ function initializeMeasurementTools(viewer, model, container) {
966
1573
  style: Cesium.LabelStyle.FILL_AND_OUTLINE,
967
1574
  outlineWidth: 2,
968
1575
  outlineColor: Cesium.Color.BLACK,
969
- pixelOffset: new Cesium.Cartesian2(0, -20),
1576
+ pixelOffset: new Cesium.Cartesian2(0, CONSTANTS2.LABEL_PIXEL_OFFSET_Y),
970
1577
  showBackground: true,
971
1578
  backgroundColor: Cesium.Color.fromAlpha(Cesium.Color.BLACK, 0.7),
972
1579
  disableDepthTestDistance: Number.POSITIVE_INFINITY
@@ -989,6 +1596,27 @@ function initializeMeasurementTools(viewer, model, container) {
989
1596
  alt: cartographic.height
990
1597
  };
991
1598
  }
1599
+ function updateOrCreateMeasurement(type, value, points) {
1600
+ const results = model.get("measurement_results") || [];
1601
+ const lastResult = results[results.length - 1];
1602
+ const isActiveType = lastResult && lastResult.type === type && lastResult.isActive;
1603
+ let newResults;
1604
+ if (isActiveType) {
1605
+ newResults = [...results];
1606
+ newResults[newResults.length - 1] = { ...lastResult, value, points };
1607
+ } else {
1608
+ const count = results.filter((r) => r.type === type).length + 1;
1609
+ newResults = [...results, {
1610
+ type,
1611
+ value,
1612
+ points,
1613
+ isActive: type === "multi-distance" || type === "area",
1614
+ name: `${TYPE_LABELS[type]} ${count}`
1615
+ }];
1616
+ }
1617
+ model.set("measurement_results", newResults);
1618
+ model.save_changes();
1619
+ }
992
1620
  function clearAllMeasurements() {
993
1621
  log(PREFIX3, "Clearing all measurements");
994
1622
  measurementState.entities.forEach((e) => viewer.entities.remove(e));
@@ -1025,19 +1653,24 @@ function initializeMeasurementTools(viewer, model, container) {
1025
1653
  model.set("measurement_mode", "");
1026
1654
  model.save_changes();
1027
1655
  }
1656
+ addPointBtn.style.display = "block";
1028
1657
  measurementState.entities.forEach((entity) => {
1029
1658
  if (entity.point) {
1030
- entity.point.pixelSize = 12;
1031
- entity.point.outlineWidth = 3;
1659
+ entity.point.pixelSize = CONSTANTS2.MARKER_SIZE_HOVER;
1660
+ entity.point.outlineWidth = CONSTANTS2.MARKER_OUTLINE_WIDTH_HOVER;
1032
1661
  }
1033
1662
  });
1034
1663
  editHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
1035
1664
  editHandler.setInputAction((click) => {
1036
- const pickedObject = viewer.scene.pick(click.position);
1037
- if (Cesium.defined(pickedObject) && pickedObject.id && pickedObject.id.point) {
1038
- selectPoint(pickedObject.id, click.position);
1665
+ if (editState.addPointMode) {
1666
+ handleAddPointClick(click);
1039
1667
  } else {
1040
- deselectPoint();
1668
+ const pickedObject = viewer.scene.pick(click.position);
1669
+ if (Cesium.defined(pickedObject) && pickedObject.id && pickedObject.id.point) {
1670
+ selectPoint(pickedObject.id, click.position);
1671
+ } else {
1672
+ deselectPoint();
1673
+ }
1041
1674
  }
1042
1675
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
1043
1676
  editHandler.setInputAction((movement) => {
@@ -1049,7 +1682,7 @@ function initializeMeasurementTools(viewer, model, container) {
1049
1682
  }
1050
1683
  }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
1051
1684
  editHandler.setInputAction(() => {
1052
- if (editState.selectedEntity) {
1685
+ if (editState.selectedEntity && !editState.addPointMode) {
1053
1686
  editState.dragging = true;
1054
1687
  viewer.scene.screenSpaceCameraController.enableRotate = false;
1055
1688
  }
@@ -1067,11 +1700,14 @@ function initializeMeasurementTools(viewer, model, container) {
1067
1700
  editHandler.destroy();
1068
1701
  editHandler = null;
1069
1702
  }
1703
+ addPointBtn.style.display = "none";
1704
+ editState.addPointMode = false;
1705
+ addPointBtn.style.background = "#16a085";
1070
1706
  deselectPoint();
1071
1707
  measurementState.entities.forEach((entity) => {
1072
1708
  if (entity.point) {
1073
- entity.point.pixelSize = 10;
1074
- entity.point.outlineWidth = 2;
1709
+ entity.point.pixelSize = CONSTANTS2.MARKER_SIZE;
1710
+ entity.point.outlineWidth = CONSTANTS2.MARKER_OUTLINE_WIDTH;
1075
1711
  }
1076
1712
  });
1077
1713
  viewer.scene.screenSpaceCameraController.enableRotate = true;
@@ -1096,21 +1732,20 @@ function initializeMeasurementTools(viewer, model, container) {
1096
1732
  break;
1097
1733
  }
1098
1734
  }
1099
- if (measurementIndex === -1)
1100
- return;
1735
+ if (measurementIndex === -1) return;
1101
1736
  editState.selectedEntity = entity;
1102
1737
  editState.measurementIndex = measurementIndex;
1103
1738
  editState.pointIndex = pointIndex;
1104
1739
  editState.selectedPoint = entity.position.getValue(Cesium.JulianDate.now());
1105
- entity.point.pixelSize = 15;
1106
- entity.point.outlineWidth = 4;
1740
+ entity.point.pixelSize = CONSTANTS2.MARKER_SIZE_SELECTED;
1741
+ entity.point.outlineWidth = CONSTANTS2.MARKER_OUTLINE_WIDTH_SELECTED;
1107
1742
  entity.point.outlineColor = Cesium.Color.YELLOW;
1108
1743
  showCoordinateEditor(results[measurementIndex], pointIndex);
1109
1744
  }
1110
1745
  function deselectPoint() {
1111
1746
  if (editState.selectedEntity && editState.selectedEntity.point) {
1112
- editState.selectedEntity.point.pixelSize = 12;
1113
- editState.selectedEntity.point.outlineWidth = 3;
1747
+ editState.selectedEntity.point.pixelSize = CONSTANTS2.MARKER_SIZE_HOVER;
1748
+ editState.selectedEntity.point.outlineWidth = CONSTANTS2.MARKER_OUTLINE_WIDTH_HOVER;
1114
1749
  editState.selectedEntity.point.outlineColor = Cesium.Color.WHITE;
1115
1750
  }
1116
1751
  editState.selectedEntity = null;
@@ -1122,10 +1757,8 @@ function initializeMeasurementTools(viewer, model, container) {
1122
1757
  }
1123
1758
  function showCoordinateEditor(measurement, pointIndex) {
1124
1759
  const point = measurement.points[pointIndex];
1125
- editorPanel.innerHTML = `
1126
- <div style="margin-bottom: 10px; font-weight: bold; border-bottom: 1px solid #555; padding-bottom: 5px;">
1127
- Edit Point ${pointIndex + 1} (${measurement.type})
1128
- </div>
1760
+ editorHeader.textContent = `\u22EE\u22EE Edit Point ${pointIndex + 1} (${measurement.type})`;
1761
+ editorContent.innerHTML = `
1129
1762
  <div style="margin-bottom: 8px;">
1130
1763
  <label style="display: block; margin-bottom: 3px;">Longitude (\xB0):</label>
1131
1764
  <input type="number" id="edit-lon" value="${point.lon.toFixed(6)}" step="0.000001"
@@ -1188,16 +1821,14 @@ function initializeMeasurementTools(viewer, model, container) {
1188
1821
  });
1189
1822
  }
1190
1823
  function updatePointPosition(newPosition) {
1191
- if (!editState.selectedEntity)
1192
- return;
1824
+ if (!editState.selectedEntity) return;
1193
1825
  editState.selectedEntity.position = newPosition;
1194
1826
  editState.selectedPoint = newPosition;
1195
1827
  updateMeasurementVisuals();
1196
1828
  }
1197
1829
  function updateMeasurementVisuals() {
1198
1830
  const results = model.get("measurement_results") || [];
1199
- if (editState.measurementIndex === null)
1200
- return;
1831
+ if (editState.measurementIndex === null) return;
1201
1832
  const measurement = results[editState.measurementIndex];
1202
1833
  let entityStartIndex = 0;
1203
1834
  for (let i = 0; i < editState.measurementIndex; i++) {
@@ -1218,10 +1849,10 @@ function initializeMeasurementTools(viewer, model, container) {
1218
1849
  const newPolygon = viewer.entities.add({
1219
1850
  polygon: {
1220
1851
  hierarchy: new Cesium.PolygonHierarchy(positions),
1221
- material: Cesium.Color.ORANGE.withAlpha(0.3),
1852
+ material: Cesium.Color.ORANGE.withAlpha(CONSTANTS2.POLYGON_ALPHA),
1222
1853
  outline: true,
1223
1854
  outlineColor: Cesium.Color.ORANGE,
1224
- outlineWidth: 2
1855
+ outlineWidth: CONSTANTS2.POLYGON_OUTLINE_WIDTH
1225
1856
  }
1226
1857
  });
1227
1858
  measurementState.polylines[polylineStartIndex] = newPolygon;
@@ -1246,10 +1877,9 @@ function initializeMeasurementTools(viewer, model, container) {
1246
1877
  if (type === "distance") {
1247
1878
  const distance = Cesium.Cartesian3.distance(positions[0], positions[1]);
1248
1879
  const midpoint = Cesium.Cartesian3.midpoint(positions[0], positions[1], new Cesium.Cartesian3());
1249
- const distanceText = distance >= 1e3 ? `${(distance / 1e3).toFixed(2)} km` : `${distance.toFixed(2)} m`;
1250
1880
  if (measurementState.labels[labelStartIndex]) {
1251
1881
  measurementState.labels[labelStartIndex].position = midpoint;
1252
- measurementState.labels[labelStartIndex].label.text = distanceText;
1882
+ measurementState.labels[labelStartIndex].label.text = formatValue(distance);
1253
1883
  }
1254
1884
  } else if (type === "height") {
1255
1885
  const carto0 = Cesium.Cartographic.fromCartesian(positions[0]);
@@ -1257,16 +1887,14 @@ function initializeMeasurementTools(viewer, model, container) {
1257
1887
  const verticalDistance = Math.abs(carto1.height - carto0.height);
1258
1888
  const midHeight = (carto0.height + carto1.height) / 2;
1259
1889
  const labelPos = Cesium.Cartesian3.fromRadians(carto1.longitude, carto1.latitude, midHeight);
1260
- const heightText = verticalDistance >= 1e3 ? `${(verticalDistance / 1e3).toFixed(2)} km` : `${verticalDistance.toFixed(2)} m`;
1261
1890
  if (measurementState.labels[labelStartIndex]) {
1262
1891
  measurementState.labels[labelStartIndex].position = labelPos;
1263
- measurementState.labels[labelStartIndex].label.text = heightText;
1892
+ measurementState.labels[labelStartIndex].label.text = formatValue(verticalDistance);
1264
1893
  }
1265
1894
  }
1266
1895
  }
1267
1896
  function finalizeMeasurementUpdate() {
1268
- if (editState.measurementIndex === null || editState.pointIndex === null)
1269
- return;
1897
+ if (editState.measurementIndex === null || editState.pointIndex === null) return;
1270
1898
  const results = model.get("measurement_results") || [];
1271
1899
  const measurement = results[editState.measurementIndex];
1272
1900
  const cartographic = Cesium.Cartographic.fromCartesian(editState.selectedPoint);
@@ -1299,33 +1927,7 @@ function initializeMeasurementTools(viewer, model, container) {
1299
1927
  }
1300
1928
  measurement.value = totalDistance;
1301
1929
  } else if (measurement.type === "area") {
1302
- const polygonHierarchy = new Cesium.PolygonHierarchy(positions);
1303
- const geometry = Cesium.PolygonGeometry.createGeometry(
1304
- new Cesium.PolygonGeometry({
1305
- polygonHierarchy,
1306
- perPositionHeight: false,
1307
- arcType: Cesium.ArcType.GEODESIC
1308
- })
1309
- );
1310
- let area = 0;
1311
- if (geometry) {
1312
- const positionsArray = geometry.attributes.position.values;
1313
- const indices = geometry.indices;
1314
- for (let i = 0; i < indices.length; i += 3) {
1315
- const i0 = indices[i] * 3;
1316
- const i1 = indices[i + 1] * 3;
1317
- const i2 = indices[i + 2] * 3;
1318
- const v0 = new Cesium.Cartesian3(positionsArray[i0], positionsArray[i0 + 1], positionsArray[i0 + 2]);
1319
- const v1 = new Cesium.Cartesian3(positionsArray[i1], positionsArray[i1 + 1], positionsArray[i1 + 2]);
1320
- const v2 = new Cesium.Cartesian3(positionsArray[i2], positionsArray[i2 + 1], positionsArray[i2 + 2]);
1321
- const edge1 = Cesium.Cartesian3.subtract(v1, v0, new Cesium.Cartesian3());
1322
- const edge2 = Cesium.Cartesian3.subtract(v2, v0, new Cesium.Cartesian3());
1323
- const crossProduct = Cesium.Cartesian3.cross(edge1, edge2, new Cesium.Cartesian3());
1324
- const triangleArea = Cesium.Cartesian3.magnitude(crossProduct) / 2;
1325
- area += triangleArea;
1326
- }
1327
- }
1328
- measurement.value = area;
1930
+ measurement.value = calculateGeodesicArea(positions);
1329
1931
  }
1330
1932
  const newResults = [...results];
1331
1933
  model.set("measurement_results", newResults);
@@ -1335,6 +1937,169 @@ function initializeMeasurementTools(viewer, model, container) {
1335
1937
  showCoordinateEditor(measurement, editState.pointIndex);
1336
1938
  }
1337
1939
  }
1940
+ function handleAddPointClick(click) {
1941
+ const position = getPosition(click.position);
1942
+ if (!position) {
1943
+ log(PREFIX3, "No valid position for add point");
1944
+ return;
1945
+ }
1946
+ const results = model.get("measurement_results") || [];
1947
+ let nearestMeasurement = null;
1948
+ let nearestSegment = null;
1949
+ let minDistance = Number.POSITIVE_INFINITY;
1950
+ let insertIndex = -1;
1951
+ for (let measIdx = 0; measIdx < results.length; measIdx++) {
1952
+ const measurement = results[measIdx];
1953
+ if (measurement.type !== "multi-distance" && measurement.type !== "area") {
1954
+ continue;
1955
+ }
1956
+ let entityStartIndex = 0;
1957
+ for (let i = 0; i < measIdx; i++) {
1958
+ entityStartIndex += results[i].points.length;
1959
+ }
1960
+ const positions = [];
1961
+ for (let i = 0; i < measurement.points.length; i++) {
1962
+ const entity = measurementState.entities[entityStartIndex + i];
1963
+ if (entity && entity.position) {
1964
+ positions.push(entity.position.getValue(Cesium.JulianDate.now()));
1965
+ }
1966
+ }
1967
+ for (let i = 0; i < positions.length - 1; i++) {
1968
+ const p1 = positions[i];
1969
+ const p2 = positions[i + 1];
1970
+ const closestPoint = closestPointOnSegment(position, p1, p2);
1971
+ const dist = Cesium.Cartesian3.distance(position, closestPoint);
1972
+ if (dist < minDistance) {
1973
+ minDistance = dist;
1974
+ nearestMeasurement = measIdx;
1975
+ nearestSegment = { start: p1, end: p2 };
1976
+ insertIndex = i + 1;
1977
+ }
1978
+ }
1979
+ if (measurement.type === "area" && positions.length > 2) {
1980
+ const p1 = positions[positions.length - 1];
1981
+ const p2 = positions[0];
1982
+ const closestPoint = closestPointOnSegment(position, p1, p2);
1983
+ const dist = Cesium.Cartesian3.distance(position, closestPoint);
1984
+ if (dist < minDistance) {
1985
+ minDistance = dist;
1986
+ nearestMeasurement = measIdx;
1987
+ nearestSegment = { start: p1, end: p2 };
1988
+ insertIndex = positions.length;
1989
+ }
1990
+ }
1991
+ }
1992
+ if (nearestMeasurement !== null && minDistance < CONSTANTS2.MAX_ADD_POINT_DISTANCE_METERS) {
1993
+ addPointToMeasurement(nearestMeasurement, insertIndex, position);
1994
+ } else {
1995
+ log(PREFIX3, "No nearby line segment found to add point (min distance:", minDistance.toFixed(2), "m)");
1996
+ }
1997
+ }
1998
+ function closestPointOnSegment(point, segStart, segEnd) {
1999
+ const segmentVector = Cesium.Cartesian3.subtract(segEnd, segStart, new Cesium.Cartesian3());
2000
+ const pointVector = Cesium.Cartesian3.subtract(point, segStart, new Cesium.Cartesian3());
2001
+ const segmentLength = Cesium.Cartesian3.magnitude(segmentVector);
2002
+ if (segmentLength === 0) return segStart;
2003
+ const normalizedSegment = Cesium.Cartesian3.normalize(segmentVector, new Cesium.Cartesian3());
2004
+ const projection = Cesium.Cartesian3.dot(pointVector, normalizedSegment);
2005
+ const t = Math.max(0, Math.min(segmentLength, projection));
2006
+ const closestPoint = Cesium.Cartesian3.add(
2007
+ segStart,
2008
+ Cesium.Cartesian3.multiplyByScalar(normalizedSegment, t, new Cesium.Cartesian3()),
2009
+ new Cesium.Cartesian3()
2010
+ );
2011
+ return closestPoint;
2012
+ }
2013
+ function addPointToMeasurement(measurementIndex, insertIndex, position) {
2014
+ const results = model.get("measurement_results") || [];
2015
+ const measurement = results[measurementIndex];
2016
+ const cartographic = Cesium.Cartographic.fromCartesian(position);
2017
+ const newPoint = {
2018
+ lat: Cesium.Math.toDegrees(cartographic.latitude),
2019
+ lon: Cesium.Math.toDegrees(cartographic.longitude),
2020
+ alt: cartographic.height
2021
+ };
2022
+ measurement.points.splice(insertIndex, 0, newPoint);
2023
+ const colors = getColors();
2024
+ const color = colors[measurement.type]?.main || Cesium.Color.WHITE;
2025
+ const marker = addMarker(position, color);
2026
+ let entityInsertIndex = 0;
2027
+ for (let i = 0; i < measurementIndex; i++) {
2028
+ entityInsertIndex += results[i].points.length;
2029
+ }
2030
+ entityInsertIndex += insertIndex;
2031
+ measurementState.entities.splice(entityInsertIndex, 0, marker);
2032
+ const positions = [];
2033
+ let entityStartIndex = 0;
2034
+ for (let i = 0; i < measurementIndex; i++) {
2035
+ entityStartIndex += results[i].points.length;
2036
+ }
2037
+ for (let i = 0; i < measurement.points.length; i++) {
2038
+ const entity = measurementState.entities[entityStartIndex + i];
2039
+ if (entity && entity.position) {
2040
+ positions.push(entity.position.getValue(Cesium.JulianDate.now()));
2041
+ }
2042
+ }
2043
+ if (measurement.type === "multi-distance") {
2044
+ let totalDistance = 0;
2045
+ for (let i = 0; i < positions.length - 1; i++) {
2046
+ totalDistance += Cesium.Cartesian3.distance(positions[i], positions[i + 1]);
2047
+ }
2048
+ measurement.value = totalDistance;
2049
+ } else if (measurement.type === "area") {
2050
+ measurement.value = calculateGeodesicArea(positions);
2051
+ }
2052
+ updateMeasurementVisualsForIndex(measurementIndex);
2053
+ const newResults = [...results];
2054
+ model.set("measurement_results", newResults);
2055
+ model.save_changes();
2056
+ updateMeasurementsList();
2057
+ log(PREFIX3, `Added point to ${measurement.type} measurement at index ${insertIndex}`);
2058
+ }
2059
+ function updateMeasurementVisualsForIndex(measurementIndex) {
2060
+ const results = model.get("measurement_results") || [];
2061
+ const measurement = results[measurementIndex];
2062
+ let entityStartIndex = 0;
2063
+ for (let i = 0; i < measurementIndex; i++) {
2064
+ entityStartIndex += results[i].points.length;
2065
+ }
2066
+ const positions = [];
2067
+ for (let i = 0; i < measurement.points.length; i++) {
2068
+ const entity = measurementState.entities[entityStartIndex + i];
2069
+ if (entity && entity.position) {
2070
+ positions.push(entity.position.getValue(Cesium.JulianDate.now()));
2071
+ }
2072
+ }
2073
+ const colors = getColors();
2074
+ const color = colors[measurement.type]?.main || Cesium.Color.WHITE;
2075
+ if (measurementState.polylines[measurementIndex]) {
2076
+ viewer.entities.remove(measurementState.polylines[measurementIndex]);
2077
+ }
2078
+ if (measurement.type === "multi-distance") {
2079
+ const polyline = viewer.entities.add({
2080
+ polyline: {
2081
+ positions,
2082
+ width: CONSTANTS2.POLYLINE_WIDTH,
2083
+ material: color,
2084
+ clampToGround: false
2085
+ }
2086
+ });
2087
+ measurementState.polylines[measurementIndex] = polyline;
2088
+ const labelStartIndex = measurementIndex * 20;
2089
+ } else if (measurement.type === "area") {
2090
+ const polygon = viewer.entities.add({
2091
+ polygon: {
2092
+ hierarchy: new Cesium.PolygonHierarchy(positions),
2093
+ material: Cesium.Color.fromAlpha(color, CONSTANTS2.POLYGON_ALPHA),
2094
+ outline: true,
2095
+ outlineColor: color,
2096
+ outlineWidth: CONSTANTS2.POLYGON_OUTLINE_WIDTH,
2097
+ perPositionHeight: true
2098
+ }
2099
+ });
2100
+ measurementState.polylines[measurementIndex] = polygon;
2101
+ }
2102
+ }
1338
2103
  function updateMeasurementsList() {
1339
2104
  const results = model.get("measurement_results") || [];
1340
2105
  log(PREFIX3, "Updating measurements list, count:", results.length);
@@ -1358,6 +2123,11 @@ function initializeMeasurementTools(viewer, model, container) {
1358
2123
  cursor: pointer;
1359
2124
  transition: background 0.2s;
1360
2125
  border-left: 3px solid ${getMeasurementColor(measurement.type)};
2126
+ box-sizing: border-box;
2127
+ width: 100%;
2128
+ word-wrap: break-word;
2129
+ overflow-wrap: break-word;
2130
+ overflow: hidden;
1361
2131
  `;
1362
2132
  measurementDiv.onmouseover = () => {
1363
2133
  measurementDiv.style.background = "rgba(255, 255, 255, 0.15)";
@@ -1373,15 +2143,16 @@ function initializeMeasurementTools(viewer, model, container) {
1373
2143
  display: flex;
1374
2144
  justify-content: space-between;
1375
2145
  align-items: center;
2146
+ gap: 8px;
1376
2147
  `;
1377
2148
  nameDiv.innerHTML = `
1378
- <span style="flex: 1;">${name}</span>
1379
- <button id="rename-${index}" style="padding: 2px 6px; background: #3498db; color: white; border: none; border-radius: 2px; cursor: pointer; font-size: 10px;">\u270E</button>
2149
+ <span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;">${name}</span>
2150
+ <button id="rename-${index}" style="padding: 2px 6px; background: #3498db; color: white; border: none; border-radius: 2px; cursor: pointer; font-size: 10px; flex-shrink: 0;">\u270E</button>
1380
2151
  `;
1381
2152
  measurementDiv.appendChild(nameDiv);
1382
2153
  const valueDiv = document.createElement("div");
1383
2154
  valueDiv.style.cssText = "color: #aaa; font-size: 11px; margin-bottom: 3px;";
1384
- valueDiv.textContent = formatMeasurementValue(measurement);
2155
+ valueDiv.textContent = formatValue(measurement.value, measurement.type === "area");
1385
2156
  measurementDiv.appendChild(valueDiv);
1386
2157
  const pointsDiv = document.createElement("div");
1387
2158
  pointsDiv.style.cssText = "color: #888; font-size: 10px;";
@@ -1405,31 +2176,10 @@ function initializeMeasurementTools(viewer, model, container) {
1405
2176
  });
1406
2177
  }
1407
2178
  function getMeasurementColor(type) {
1408
- const colors = {
1409
- "distance": "#e74c3c",
1410
- "multi-distance": "#3498db",
1411
- "height": "#2ecc71",
1412
- "area": "#e67e22"
1413
- };
1414
- return colors[type] || "#95a5a6";
2179
+ return getColors()[type]?.button || "#95a5a6";
1415
2180
  }
1416
2181
  function getMeasurementTypeLabel(type) {
1417
- const labels = {
1418
- "distance": "Distance",
1419
- "multi-distance": "Multi-Distance",
1420
- "height": "Height",
1421
- "area": "Area"
1422
- };
1423
- return labels[type] || type;
1424
- }
1425
- function formatMeasurementValue(measurement) {
1426
- const value = measurement.value;
1427
- const type = measurement.type;
1428
- if (type === "area") {
1429
- return value >= 1e6 ? `${(value / 1e6).toFixed(2)} km\xB2` : `${value.toFixed(2)} m\xB2`;
1430
- } else {
1431
- return value >= 1e3 ? `${(value / 1e3).toFixed(2)} km` : `${value.toFixed(2)} m`;
1432
- }
2182
+ return TYPE_LABELS[type] || type;
1433
2183
  }
1434
2184
  function renameMeasurement(index, currentName) {
1435
2185
  const newName = prompt("Enter new name for measurement:", currentName);
@@ -1444,11 +2194,9 @@ function initializeMeasurementTools(viewer, model, container) {
1444
2194
  }
1445
2195
  function focusOnMeasurement(index) {
1446
2196
  const results = model.get("measurement_results") || [];
1447
- if (index < 0 || index >= results.length)
1448
- return;
2197
+ if (index < 0 || index >= results.length) return;
1449
2198
  const measurement = results[index];
1450
- if (!measurement.points || measurement.points.length === 0)
1451
- return;
2199
+ if (!measurement.points || measurement.points.length === 0) return;
1452
2200
  const positions = measurement.points.map(
1453
2201
  (p) => Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.alt || 0)
1454
2202
  );
@@ -1464,8 +2212,7 @@ function initializeMeasurementTools(viewer, model, container) {
1464
2212
  }
1465
2213
  function handleDistanceClick(click) {
1466
2214
  const position = getPosition(click.position);
1467
- if (!position)
1468
- return;
2215
+ if (!position) return;
1469
2216
  if (measurementState.points.length === 0) {
1470
2217
  measurementState.points.push(position);
1471
2218
  addMarker(position);
@@ -1477,7 +2224,7 @@ function initializeMeasurementTools(viewer, model, container) {
1477
2224
  }
1478
2225
  return measurementState.points;
1479
2226
  }, false),
1480
- width: 3,
2227
+ width: CONSTANTS2.POLYLINE_WIDTH,
1481
2228
  material: Cesium.Color.YELLOW,
1482
2229
  depthFailMaterial: Cesium.Color.YELLOW
1483
2230
  }
@@ -1487,7 +2234,7 @@ function initializeMeasurementTools(viewer, model, container) {
1487
2234
  addMarker(position);
1488
2235
  const distance = calculateDistance(measurementState.points[0], measurementState.points[1]);
1489
2236
  const midpoint = getMidpoint(measurementState.points[0], measurementState.points[1]);
1490
- addLabel(midpoint, `${distance.toFixed(2)} m`);
2237
+ addLabel(midpoint, formatValue(distance));
1491
2238
  if (measurementState.tempPolyline) {
1492
2239
  viewer.entities.remove(measurementState.tempPolyline);
1493
2240
  measurementState.tempPolyline = null;
@@ -1495,7 +2242,7 @@ function initializeMeasurementTools(viewer, model, container) {
1495
2242
  measurementState.polyline = viewer.entities.add({
1496
2243
  polyline: {
1497
2244
  positions: measurementState.points,
1498
- width: 3,
2245
+ width: CONSTANTS2.POLYLINE_WIDTH,
1499
2246
  material: Cesium.Color.RED,
1500
2247
  depthFailMaterial: Cesium.Color.RED
1501
2248
  }
@@ -1515,15 +2262,14 @@ function initializeMeasurementTools(viewer, model, container) {
1515
2262
  }
1516
2263
  function handleMultiDistanceClick(click) {
1517
2264
  const position = getPosition(click.position);
1518
- if (!position)
1519
- return;
2265
+ if (!position) return;
1520
2266
  measurementState.points.push(position);
1521
2267
  addMarker(position, Cesium.Color.BLUE);
1522
2268
  if (measurementState.points.length === 1) {
1523
2269
  measurementState.polyline = viewer.entities.add({
1524
2270
  polyline: {
1525
2271
  positions: new Cesium.CallbackProperty(() => measurementState.points, false),
1526
- width: 3,
2272
+ width: CONSTANTS2.POLYLINE_WIDTH,
1527
2273
  material: Cesium.Color.BLUE,
1528
2274
  depthFailMaterial: Cesium.Color.BLUE
1529
2275
  }
@@ -1533,43 +2279,17 @@ function initializeMeasurementTools(viewer, model, container) {
1533
2279
  const p1 = measurementState.points[measurementState.points.length - 2];
1534
2280
  const p2 = measurementState.points[measurementState.points.length - 1];
1535
2281
  const distance = calculateDistance(p1, p2);
1536
- const midpoint = getMidpoint(p1, p2);
1537
- addLabel(midpoint, `${distance.toFixed(2)} m`);
2282
+ addLabel(getMidpoint(p1, p2), formatValue(distance));
1538
2283
  let totalDistance = 0;
1539
2284
  for (let i = 0; i < measurementState.points.length - 1; i++) {
1540
- totalDistance += calculateDistance(
1541
- measurementState.points[i],
1542
- measurementState.points[i + 1]
1543
- );
1544
- }
1545
- const results = model.get("measurement_results") || [];
1546
- const lastResult = results[results.length - 1];
1547
- let newResults;
1548
- if (lastResult && lastResult.type === "multi-distance" && lastResult.isActive) {
1549
- newResults = [...results];
1550
- newResults[newResults.length - 1] = {
1551
- ...lastResult,
1552
- value: totalDistance,
1553
- points: measurementState.points.map(cartesianToLatLonAlt)
1554
- };
1555
- } else {
1556
- const multiDistanceCount = results.filter((r) => r.type === "multi-distance").length + 1;
1557
- newResults = [...results, {
1558
- type: "multi-distance",
1559
- value: totalDistance,
1560
- points: measurementState.points.map(cartesianToLatLonAlt),
1561
- isActive: true,
1562
- name: `Multi-Distance ${multiDistanceCount}`
1563
- }];
2285
+ totalDistance += calculateDistance(measurementState.points[i], measurementState.points[i + 1]);
1564
2286
  }
1565
- model.set("measurement_results", newResults);
1566
- model.save_changes();
2287
+ updateOrCreateMeasurement("multi-distance", totalDistance, measurementState.points.map(cartesianToLatLonAlt));
1567
2288
  }
1568
2289
  }
1569
2290
  function handleHeightClick(click) {
1570
2291
  const pickedPosition = getPosition(click.position);
1571
- if (!pickedPosition)
1572
- return;
2292
+ if (!pickedPosition) return;
1573
2293
  const cartographic = Cesium.Cartographic.fromCartesian(pickedPosition);
1574
2294
  const terrainHeight = viewer.scene.globe.getHeight(cartographic) || 0;
1575
2295
  const pickedHeight = cartographic.height;
@@ -1584,7 +2304,7 @@ function initializeMeasurementTools(viewer, model, container) {
1584
2304
  const heightLine = viewer.entities.add({
1585
2305
  polyline: {
1586
2306
  positions: [groundPosition, pickedPosition],
1587
- width: 3,
2307
+ width: CONSTANTS2.POLYLINE_WIDTH,
1588
2308
  material: Cesium.Color.GREEN,
1589
2309
  depthFailMaterial: Cesium.Color.GREEN
1590
2310
  }
@@ -1604,8 +2324,7 @@ function initializeMeasurementTools(viewer, model, container) {
1604
2324
  }
1605
2325
  function handleAreaClick(click) {
1606
2326
  const position = getPosition(click.position);
1607
- if (!position)
1608
- return;
2327
+ if (!position) return;
1609
2328
  measurementState.points.push(position);
1610
2329
  addMarker(position, Cesium.Color.ORANGE);
1611
2330
  if (measurementState.points.length === 1) {
@@ -1614,84 +2333,27 @@ function initializeMeasurementTools(viewer, model, container) {
1614
2333
  hierarchy: new Cesium.CallbackProperty(() => {
1615
2334
  return new Cesium.PolygonHierarchy(measurementState.points);
1616
2335
  }, false),
1617
- material: Cesium.Color.ORANGE.withAlpha(0.3),
2336
+ material: Cesium.Color.ORANGE.withAlpha(CONSTANTS2.POLYGON_ALPHA),
1618
2337
  outline: true,
1619
2338
  outlineColor: Cesium.Color.ORANGE,
1620
- outlineWidth: 2
2339
+ outlineWidth: CONSTANTS2.POLYGON_OUTLINE_WIDTH
1621
2340
  }
1622
2341
  });
1623
2342
  measurementState.polylines.push(measurementState.polyline);
1624
2343
  }
1625
2344
  if (measurementState.points.length >= 3) {
1626
2345
  const positions = measurementState.points;
1627
- const polygonHierarchy = new Cesium.PolygonHierarchy(positions);
1628
- const geometry = Cesium.PolygonGeometry.createGeometry(
1629
- new Cesium.PolygonGeometry({
1630
- polygonHierarchy,
1631
- perPositionHeight: false,
1632
- arcType: Cesium.ArcType.GEODESIC
1633
- })
1634
- );
1635
- let area = 0;
1636
- if (geometry) {
1637
- const positionsArray = geometry.attributes.position.values;
1638
- const indices = geometry.indices;
1639
- for (let i = 0; i < indices.length; i += 3) {
1640
- const i0 = indices[i] * 3;
1641
- const i1 = indices[i + 1] * 3;
1642
- const i2 = indices[i + 2] * 3;
1643
- const v0 = new Cesium.Cartesian3(positionsArray[i0], positionsArray[i0 + 1], positionsArray[i0 + 2]);
1644
- const v1 = new Cesium.Cartesian3(positionsArray[i1], positionsArray[i1 + 1], positionsArray[i1 + 2]);
1645
- const v2 = new Cesium.Cartesian3(positionsArray[i2], positionsArray[i2 + 1], positionsArray[i2 + 2]);
1646
- const edge1 = Cesium.Cartesian3.subtract(v1, v0, new Cesium.Cartesian3());
1647
- const edge2 = Cesium.Cartesian3.subtract(v2, v0, new Cesium.Cartesian3());
1648
- const crossProduct = Cesium.Cartesian3.cross(edge1, edge2, new Cesium.Cartesian3());
1649
- const triangleArea = Cesium.Cartesian3.magnitude(crossProduct) / 2;
1650
- area += triangleArea;
1651
- }
1652
- }
1653
- let centroidLon = 0, centroidLat = 0;
1654
- positions.forEach((pos) => {
1655
- const carto = Cesium.Cartographic.fromCartesian(pos);
1656
- centroidLon += carto.longitude;
1657
- centroidLat += carto.latitude;
1658
- });
1659
- centroidLon /= positions.length;
1660
- centroidLat /= positions.length;
1661
- const areaText = area >= 1e6 ? `${(area / 1e6).toFixed(2)} km\xB2` : `${area.toFixed(2)} m\xB2`;
2346
+ const area = calculateGeodesicArea(positions);
1662
2347
  const oldLabel = measurementState.labels.find((l) => l.label && l.label.text._value.includes("m\xB2") || l.label.text._value.includes("km\xB2"));
1663
2348
  if (oldLabel) {
1664
2349
  viewer.entities.remove(oldLabel);
1665
2350
  measurementState.labels = measurementState.labels.filter((l) => l !== oldLabel);
1666
2351
  }
1667
- const centroidCarto = new Cesium.Cartographic(centroidLon, centroidLat);
1668
- const promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [centroidCarto]);
1669
- promise.then(() => {
1670
- const centroid = Cesium.Cartographic.toCartesian(centroidCarto);
1671
- addLabel(centroid, areaText);
2352
+ const centroidCarto = calculateCentroid(positions);
2353
+ Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [centroidCarto]).then(() => {
2354
+ addLabel(Cesium.Cartographic.toCartesian(centroidCarto), formatValue(area, true));
1672
2355
  });
1673
- const results = model.get("measurement_results") || [];
1674
- const lastResult = results[results.length - 1];
1675
- let newResults;
1676
- if (lastResult && lastResult.type === "area" && lastResult.isActive) {
1677
- newResults = [...results];
1678
- newResults[newResults.length - 1] = {
1679
- ...lastResult,
1680
- value: area,
1681
- points: measurementState.points.map(cartesianToLatLonAlt)
1682
- };
1683
- } else {
1684
- const areaCount = results.filter((r) => r.type === "area").length + 1;
1685
- newResults = [...results, {
1686
- type: "area",
1687
- value: area,
1688
- points: measurementState.points.map(cartesianToLatLonAlt),
1689
- isActive: true,
1690
- name: `Area ${areaCount}`
1691
- }];
1692
- }
1693
- model.set("measurement_results", newResults);
1694
- model.save_changes();
2356
+ updateOrCreateMeasurement("area", area, measurementState.points.map(cartesianToLatLonAlt));
1695
2357
  }
1696
2358
  }
1697
2359
  function handleMouseMove(movement) {
@@ -1710,12 +2372,10 @@ function initializeMeasurementTools(viewer, model, container) {
1710
2372
  }
1711
2373
  clearInProgressMeasurement();
1712
2374
  measurementState.mode = mode;
1713
- distanceBtn.style.background = mode === "distance" ? "#e74c3c" : "#3498db";
1714
- multiDistanceBtn.style.background = mode === "multi-distance" ? "#e74c3c" : "#3498db";
1715
- heightBtn.style.background = mode === "height" ? "#e74c3c" : "#3498db";
1716
- areaBtn.style.background = mode === "area" ? "#e74c3c" : "#3498db";
1717
- if (!mode)
1718
- return;
2375
+ Object.entries(modeButtons).forEach(([btnMode, btn]) => {
2376
+ btn.style.background = mode === btnMode ? "#e74c3c" : "#3498db";
2377
+ });
2378
+ if (!mode) return;
1719
2379
  measurementHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
1720
2380
  if (mode === "distance") {
1721
2381
  measurementHandler.setInputAction(handleDistanceClick, Cesium.ScreenSpaceEventType.LEFT_CLICK);
@@ -1757,12 +2417,10 @@ function initializeMeasurementTools(viewer, model, container) {
1757
2417
  }
1758
2418
  }
1759
2419
  function loadAndDisplayMeasurements(measurements) {
1760
- if (!Array.isArray(measurements))
1761
- return;
2420
+ if (!Array.isArray(measurements)) return;
1762
2421
  measurements.forEach((measurement) => {
1763
2422
  const { type, points } = measurement;
1764
- if (!type || !Array.isArray(points) || points.length < 2)
1765
- return;
2423
+ if (!type || !Array.isArray(points) || points.length < 2) return;
1766
2424
  const positions = points.map((point) => {
1767
2425
  const [lon, lat, alt] = point;
1768
2426
  return Cesium.Cartesian3.fromDegrees(lon, lat, alt || 0);
@@ -1783,22 +2441,21 @@ function initializeMeasurementTools(viewer, model, container) {
1783
2441
  const line = viewer.entities.add({
1784
2442
  polyline: {
1785
2443
  positions,
1786
- width: 3,
2444
+ width: CONSTANTS2.POLYLINE_WIDTH,
1787
2445
  material: Cesium.Color.RED
1788
2446
  }
1789
2447
  });
1790
2448
  measurementState.polylines.push(line);
1791
2449
  const distance = Cesium.Cartesian3.distance(positions[0], positions[1]);
1792
2450
  const midpoint = Cesium.Cartesian3.midpoint(positions[0], positions[1], new Cesium.Cartesian3());
1793
- const distanceText = distance >= 1e3 ? `${(distance / 1e3).toFixed(2)} km` : `${distance.toFixed(2)} m`;
1794
- addLabel(midpoint, distanceText);
2451
+ addLabel(midpoint, formatValue(distance));
1795
2452
  }
1796
2453
  function displayMultiDistance(positions) {
1797
2454
  positions.forEach((pos) => addMarker(pos, Cesium.Color.BLUE));
1798
2455
  const line = viewer.entities.add({
1799
2456
  polyline: {
1800
2457
  positions,
1801
- width: 3,
2458
+ width: CONSTANTS2.POLYLINE_WIDTH,
1802
2459
  material: Cesium.Color.BLUE
1803
2460
  }
1804
2461
  });
@@ -1808,12 +2465,9 @@ function initializeMeasurementTools(viewer, model, container) {
1808
2465
  const segmentDistance = Cesium.Cartesian3.distance(positions[i], positions[i + 1]);
1809
2466
  totalDistance += segmentDistance;
1810
2467
  const midpoint = Cesium.Cartesian3.midpoint(positions[i], positions[i + 1], new Cesium.Cartesian3());
1811
- const segmentText = segmentDistance >= 1e3 ? `${(segmentDistance / 1e3).toFixed(2)} km` : `${segmentDistance.toFixed(2)} m`;
1812
- addLabel(midpoint, segmentText);
2468
+ addLabel(midpoint, formatValue(segmentDistance));
1813
2469
  }
1814
- const lastPos = positions[positions.length - 1];
1815
- const totalText = totalDistance >= 1e3 ? `Total: ${(totalDistance / 1e3).toFixed(2)} km` : `Total: ${totalDistance.toFixed(2)} m`;
1816
- addLabel(lastPos, totalText);
2470
+ addLabel(positions[positions.length - 1], `Total: ${formatValue(totalDistance)}`);
1817
2471
  }
1818
2472
  function displayHeight(positions) {
1819
2473
  positions.forEach((pos) => addMarker(pos, Cesium.Color.GREEN));
@@ -1827,68 +2481,31 @@ function initializeMeasurementTools(viewer, model, container) {
1827
2481
  Cesium.Cartesian3.fromRadians(carto1.longitude, carto1.latitude, carto0.height),
1828
2482
  positions[1]
1829
2483
  ],
1830
- width: 3,
2484
+ width: CONSTANTS2.POLYLINE_WIDTH,
1831
2485
  material: Cesium.Color.GREEN
1832
2486
  }
1833
2487
  });
1834
2488
  measurementState.polylines.push(line);
1835
2489
  const midHeight = (carto0.height + carto1.height) / 2;
1836
2490
  const labelPos = Cesium.Cartesian3.fromRadians(carto1.longitude, carto1.latitude, midHeight);
1837
- const heightText = verticalDistance >= 1e3 ? `${(verticalDistance / 1e3).toFixed(2)} km` : `${verticalDistance.toFixed(2)} m`;
1838
- addLabel(labelPos, heightText);
2491
+ addLabel(labelPos, formatValue(verticalDistance));
1839
2492
  }
1840
2493
  function displayArea(positions) {
1841
2494
  positions.forEach((pos) => addMarker(pos, Cesium.Color.ORANGE));
1842
2495
  const polygon = viewer.entities.add({
1843
2496
  polygon: {
1844
2497
  hierarchy: new Cesium.PolygonHierarchy(positions),
1845
- material: Cesium.Color.ORANGE.withAlpha(0.3),
2498
+ material: Cesium.Color.ORANGE.withAlpha(CONSTANTS2.POLYGON_ALPHA),
1846
2499
  outline: true,
1847
2500
  outlineColor: Cesium.Color.ORANGE,
1848
- outlineWidth: 2
2501
+ outlineWidth: CONSTANTS2.POLYGON_OUTLINE_WIDTH
1849
2502
  }
1850
2503
  });
1851
2504
  measurementState.polylines.push(polygon);
1852
- const polygonHierarchy = new Cesium.PolygonHierarchy(positions);
1853
- const geometry = Cesium.PolygonGeometry.createGeometry(
1854
- new Cesium.PolygonGeometry({
1855
- polygonHierarchy,
1856
- perPositionHeight: false,
1857
- arcType: Cesium.ArcType.GEODESIC
1858
- })
1859
- );
1860
- let area = 0;
1861
- if (geometry) {
1862
- const positionsArray = geometry.attributes.position.values;
1863
- const indices = geometry.indices;
1864
- for (let i = 0; i < indices.length; i += 3) {
1865
- const i0 = indices[i] * 3;
1866
- const i1 = indices[i + 1] * 3;
1867
- const i2 = indices[i + 2] * 3;
1868
- const v0 = new Cesium.Cartesian3(positionsArray[i0], positionsArray[i0 + 1], positionsArray[i0 + 2]);
1869
- const v1 = new Cesium.Cartesian3(positionsArray[i1], positionsArray[i1 + 1], positionsArray[i1 + 2]);
1870
- const v2 = new Cesium.Cartesian3(positionsArray[i2], positionsArray[i2 + 1], positionsArray[i2 + 2]);
1871
- const edge1 = Cesium.Cartesian3.subtract(v1, v0, new Cesium.Cartesian3());
1872
- const edge2 = Cesium.Cartesian3.subtract(v2, v0, new Cesium.Cartesian3());
1873
- const crossProduct = Cesium.Cartesian3.cross(edge1, edge2, new Cesium.Cartesian3());
1874
- const triangleArea = Cesium.Cartesian3.magnitude(crossProduct) / 2;
1875
- area += triangleArea;
1876
- }
1877
- }
1878
- let centroidLon = 0, centroidLat = 0;
1879
- positions.forEach((pos) => {
1880
- const carto = Cesium.Cartographic.fromCartesian(pos);
1881
- centroidLon += carto.longitude;
1882
- centroidLat += carto.latitude;
1883
- });
1884
- centroidLon /= positions.length;
1885
- centroidLat /= positions.length;
1886
- const areaText = area >= 1e6 ? `${(area / 1e6).toFixed(2)} km\xB2` : `${area.toFixed(2)} m\xB2`;
1887
- const centroidCarto = new Cesium.Cartographic(centroidLon, centroidLat);
1888
- const promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [centroidCarto]);
1889
- promise.then(() => {
1890
- const centroid = Cesium.Cartographic.toCartesian(centroidCarto);
1891
- addLabel(centroid, areaText);
2505
+ const area = calculateGeodesicArea(positions);
2506
+ const centroidCarto = calculateCentroid(positions);
2507
+ Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [centroidCarto]).then(() => {
2508
+ addLabel(Cesium.Cartographic.toCartesian(centroidCarto), formatValue(area, true));
1892
2509
  });
1893
2510
  }
1894
2511
  model.on("change:measurement_mode", () => {
@@ -1913,8 +2530,7 @@ function initializeMeasurementTools(viewer, model, container) {
1913
2530
  updateMeasurementsList();
1914
2531
  });
1915
2532
  model.on("change:load_measurements_trigger", () => {
1916
- if (isDestroyed)
1917
- return;
2533
+ if (isDestroyed) return;
1918
2534
  const triggerData = model.get("load_measurements_trigger");
1919
2535
  log(PREFIX3, "Load measurements trigger:", triggerData);
1920
2536
  if (triggerData && triggerData.measurements) {
@@ -1923,8 +2539,7 @@ function initializeMeasurementTools(viewer, model, container) {
1923
2539
  }
1924
2540
  });
1925
2541
  model.on("change:focus_measurement_trigger", () => {
1926
- if (isDestroyed)
1927
- return;
2542
+ if (isDestroyed) return;
1928
2543
  const triggerData = model.get("focus_measurement_trigger");
1929
2544
  log(PREFIX3, "Focus measurement trigger:", triggerData);
1930
2545
  if (triggerData && typeof triggerData.index === "number") {
@@ -1932,8 +2547,7 @@ function initializeMeasurementTools(viewer, model, container) {
1932
2547
  }
1933
2548
  });
1934
2549
  model.on("change:show_measurement_tools", () => {
1935
- if (isDestroyed)
1936
- return;
2550
+ if (isDestroyed) return;
1937
2551
  const show = model.get("show_measurement_tools");
1938
2552
  log(PREFIX3, "Show measurement tools:", show);
1939
2553
  toolbarDiv.style.display = show ? "flex" : "none";
@@ -1944,8 +2558,7 @@ function initializeMeasurementTools(viewer, model, container) {
1944
2558
  }
1945
2559
  });
1946
2560
  model.on("change:show_measurements_list", () => {
1947
- if (isDestroyed)
1948
- return;
2561
+ if (isDestroyed) return;
1949
2562
  const show = model.get("show_measurements_list");
1950
2563
  log(PREFIX3, "Show measurements list:", show);
1951
2564
  measurementsListPanel.style.display = show ? "block" : "none";
@@ -1961,18 +2574,302 @@ function initializeMeasurementTools(viewer, model, container) {
1961
2574
  isDestroyed = true;
1962
2575
  if (measurementHandler) {
1963
2576
  measurementHandler.destroy();
2577
+ measurementHandler = null;
2578
+ }
2579
+ if (editHandler) {
2580
+ editHandler.destroy();
2581
+ editHandler = null;
1964
2582
  }
2583
+ cleanupFunctions.forEach((cleanup) => {
2584
+ try {
2585
+ cleanup();
2586
+ } catch (err) {
2587
+ warn(PREFIX3, "Error during cleanup:", err);
2588
+ }
2589
+ });
2590
+ cleanupFunctions.length = 0;
1965
2591
  clearAllMeasurements();
1966
2592
  if (toolbarDiv.parentNode) {
1967
2593
  toolbarDiv.remove();
1968
2594
  }
2595
+ if (editorPanel.parentNode) {
2596
+ editorPanel.remove();
2597
+ }
2598
+ if (measurementsListPanel.parentNode) {
2599
+ measurementsListPanel.remove();
2600
+ }
1969
2601
  log(PREFIX3, "Measurement tools destroyed");
1970
2602
  }
1971
2603
  };
1972
2604
  }
1973
2605
 
2606
+ // src/cesiumjs_anywidget/js/point-picking.js
2607
+ var PREFIX4 = "PointPicking";
2608
+ function initializePointPicking(viewer, model, container) {
2609
+ const Cesium = window.Cesium;
2610
+ let handler = null;
2611
+ let pickedPointEntities = [];
2612
+ let isPickingMode = false;
2613
+ let pickingPanel = null;
2614
+ let statusText = null;
2615
+ function createPickingPanel() {
2616
+ pickingPanel = document.createElement("div");
2617
+ pickingPanel.id = "point-picking-panel";
2618
+ pickingPanel.style.cssText = `
2619
+ position: absolute;
2620
+ top: 10px;
2621
+ left: 50%;
2622
+ transform: translateX(-50%);
2623
+ background: rgba(0, 0, 0, 0.8);
2624
+ color: white;
2625
+ padding: 10px 20px;
2626
+ border-radius: 5px;
2627
+ font-family: sans-serif;
2628
+ font-size: 14px;
2629
+ z-index: 1000;
2630
+ display: none;
2631
+ pointer-events: none;
2632
+ text-align: center;
2633
+ `;
2634
+ statusText = document.createElement("span");
2635
+ statusText.textContent = "\u{1F3AF} Point Picking Mode - Click on the scene to pick a point";
2636
+ pickingPanel.appendChild(statusText);
2637
+ container.appendChild(pickingPanel);
2638
+ }
2639
+ function setPickingPanelVisible(visible) {
2640
+ if (pickingPanel) {
2641
+ pickingPanel.style.display = visible ? "block" : "none";
2642
+ }
2643
+ }
2644
+ function updateStatus(text) {
2645
+ if (statusText) {
2646
+ statusText.textContent = text;
2647
+ }
2648
+ }
2649
+ function getPositionFromScreen(screenPosition) {
2650
+ const pickedObject = viewer.scene.pick(screenPosition);
2651
+ if (viewer.scene.pickPositionSupported && Cesium.defined(pickedObject)) {
2652
+ const cartesian = viewer.scene.pickPosition(screenPosition);
2653
+ if (Cesium.defined(cartesian)) {
2654
+ return cartesian;
2655
+ }
2656
+ }
2657
+ const ray = viewer.camera.getPickRay(screenPosition);
2658
+ if (ray) {
2659
+ return viewer.scene.globe.pick(ray, viewer.scene);
2660
+ }
2661
+ return null;
2662
+ }
2663
+ function cartesianToLatLonAlt(cartesian) {
2664
+ const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
2665
+ return {
2666
+ latitude: Cesium.Math.toDegrees(cartographic.latitude),
2667
+ longitude: Cesium.Math.toDegrees(cartographic.longitude),
2668
+ altitude_wgs84: cartographic.height
2669
+ };
2670
+ }
2671
+ async function getAltitudeMSL(latitude, longitude, altitude_wgs84) {
2672
+ try {
2673
+ const positions = [Cesium.Cartographic.fromDegrees(longitude, latitude)];
2674
+ const terrainProvider = viewer.terrainProvider;
2675
+ if (terrainProvider && terrainProvider.availability) {
2676
+ const sampledPositions = await Cesium.sampleTerrainMostDetailed(terrainProvider, positions);
2677
+ const terrainHeight = sampledPositions[0].height || 0;
2678
+ return altitude_wgs84;
2679
+ }
2680
+ } catch (e) {
2681
+ warn(PREFIX4, "Could not sample terrain for MSL conversion:", e);
2682
+ }
2683
+ return altitude_wgs84;
2684
+ }
2685
+ function addPointMarker(position, config) {
2686
+ const color = config.color || [255, 0, 0, 255];
2687
+ const label = config.label || config.point_id || "";
2688
+ const pointId = config.point_id || `point_${Date.now()}`;
2689
+ const cesiumColor = new Cesium.Color(
2690
+ color[0] / 255,
2691
+ color[1] / 255,
2692
+ color[2] / 255,
2693
+ color[3] / 255
2694
+ );
2695
+ const entity = viewer.entities.add({
2696
+ id: `picked_point_${pointId}`,
2697
+ position,
2698
+ point: {
2699
+ pixelSize: 12,
2700
+ color: cesiumColor,
2701
+ outlineColor: Cesium.Color.WHITE,
2702
+ outlineWidth: 2,
2703
+ disableDepthTestDistance: Number.POSITIVE_INFINITY
2704
+ },
2705
+ label: {
2706
+ text: label,
2707
+ font: "12pt sans-serif",
2708
+ fillColor: cesiumColor,
2709
+ outlineColor: Cesium.Color.BLACK,
2710
+ outlineWidth: 2,
2711
+ style: Cesium.LabelStyle.FILL_AND_OUTLINE,
2712
+ pixelOffset: new Cesium.Cartesian2(15, 0),
2713
+ horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
2714
+ disableDepthTestDistance: Number.POSITIVE_INFINITY
2715
+ }
2716
+ });
2717
+ pickedPointEntities.push(entity);
2718
+ return entity;
2719
+ }
2720
+ function clearPointMarkers() {
2721
+ for (const entity of pickedPointEntities) {
2722
+ viewer.entities.remove(entity);
2723
+ }
2724
+ pickedPointEntities = [];
2725
+ log(PREFIX4, "Cleared all point markers");
2726
+ }
2727
+ function removePointMarker(pointId) {
2728
+ const entityId = `picked_point_${pointId}`;
2729
+ const entity = viewer.entities.getById(entityId);
2730
+ if (entity) {
2731
+ viewer.entities.remove(entity);
2732
+ pickedPointEntities = pickedPointEntities.filter((e) => e.id !== entityId);
2733
+ log(PREFIX4, `Removed point marker: ${pointId}`);
2734
+ }
2735
+ }
2736
+ async function handleClick(click) {
2737
+ if (!isPickingMode) return;
2738
+ const position = getPositionFromScreen(click.position);
2739
+ if (!position) {
2740
+ warn(PREFIX4, "Could not determine click position");
2741
+ updateStatus("\u26A0\uFE0F Could not pick point at this location. Try clicking on visible terrain or 3D tiles.");
2742
+ return;
2743
+ }
2744
+ const coords = cartesianToLatLonAlt(position);
2745
+ const altitude_msl = await getAltitudeMSL(coords.latitude, coords.longitude, coords.altitude_wgs84);
2746
+ const config = model.get("point_picking_config") || {};
2747
+ const currentPoints = model.get("picked_points") || [];
2748
+ const pointNumber = currentPoints.length + 1;
2749
+ const labelPrefix = config.label_prefix || "GCP";
2750
+ const pointIdPrefix = config.point_id_prefix || labelPrefix;
2751
+ const pointId = `${pointIdPrefix}_${pointNumber}`;
2752
+ const label = `${labelPrefix}_${pointNumber}`;
2753
+ addPointMarker(position, {
2754
+ ...config,
2755
+ point_id: pointId,
2756
+ label
2757
+ });
2758
+ const pointData = {
2759
+ id: pointId,
2760
+ latitude: coords.latitude,
2761
+ longitude: coords.longitude,
2762
+ altitude_wgs84: coords.altitude_wgs84,
2763
+ altitude_msl,
2764
+ color: config.color || [255, 0, 0, 255],
2765
+ label,
2766
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2767
+ };
2768
+ log(PREFIX4, "Picked point:", pointData);
2769
+ model.set("picked_points", [...currentPoints, pointData]);
2770
+ model.save_changes();
2771
+ model.set("point_picking_event", pointData);
2772
+ model.save_changes();
2773
+ updateStatus(`\u2705 Picked ${label} at (${coords.latitude.toFixed(6)}\xB0, ${coords.longitude.toFixed(6)}\xB0, ${altitude_msl.toFixed(1)}m) - Click for next point`);
2774
+ const continuous = config.continuous !== false;
2775
+ if (!continuous) {
2776
+ isPickingMode = false;
2777
+ model.set("point_picking_mode", false);
2778
+ model.save_changes();
2779
+ setPickingPanelVisible(false);
2780
+ log(PREFIX4, "Single-point mode: picking stopped after one point");
2781
+ }
2782
+ }
2783
+ function startPickingMode() {
2784
+ if (handler) {
2785
+ handler.destroy();
2786
+ }
2787
+ handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
2788
+ handler.setInputAction(handleClick, Cesium.ScreenSpaceEventType.LEFT_CLICK);
2789
+ isPickingMode = true;
2790
+ setPickingPanelVisible(true);
2791
+ const config = model.get("point_picking_config") || {};
2792
+ const currentPoints = model.get("picked_points") || [];
2793
+ const nextNumber = currentPoints.length + 1;
2794
+ const labelPrefix = config.label_prefix || "GCP";
2795
+ const continuous = config.continuous !== false;
2796
+ if (continuous) {
2797
+ updateStatus(`\u{1F3AF} Point Picking Mode (continuous) - Click to pick ${labelPrefix}_${nextNumber}`);
2798
+ } else {
2799
+ updateStatus(`\u{1F3AF} Point Picking Mode (single) - Click to pick ${labelPrefix}_${nextNumber}`);
2800
+ }
2801
+ log(PREFIX4, "Started point picking mode", { continuous, labelPrefix });
2802
+ }
2803
+ function stopPickingMode() {
2804
+ if (handler) {
2805
+ handler.destroy();
2806
+ handler = null;
2807
+ }
2808
+ isPickingMode = false;
2809
+ setPickingPanelVisible(false);
2810
+ log(PREFIX4, "Stopped point picking mode");
2811
+ }
2812
+ createPickingPanel();
2813
+ model.on("change:point_picking_mode", () => {
2814
+ const enabled = model.get("point_picking_mode");
2815
+ if (enabled) {
2816
+ startPickingMode();
2817
+ } else {
2818
+ stopPickingMode();
2819
+ }
2820
+ });
2821
+ model.on("change:point_picking_config", () => {
2822
+ if (isPickingMode) {
2823
+ const config = model.get("point_picking_config") || {};
2824
+ const currentPoints = model.get("picked_points") || [];
2825
+ const nextNumber = currentPoints.length + 1;
2826
+ const labelPrefix = config.label_prefix || "GCP";
2827
+ updateStatus(`\u{1F3AF} Ready to pick ${labelPrefix}_${nextNumber} - Click on scene`);
2828
+ }
2829
+ });
2830
+ model.on("change:picked_points", () => {
2831
+ const points = model.get("picked_points") || [];
2832
+ if (points.length === 0 && pickedPointEntities.length > 0) {
2833
+ clearPointMarkers();
2834
+ }
2835
+ const currentIds = new Set(pickedPointEntities.map((e) => e.id.replace("picked_point_", "")));
2836
+ const newIds = new Set(points.map((p) => p.id));
2837
+ for (const id of currentIds) {
2838
+ if (!newIds.has(id)) {
2839
+ removePointMarker(id);
2840
+ }
2841
+ }
2842
+ });
2843
+ if (model.get("point_picking_mode")) {
2844
+ startPickingMode();
2845
+ }
2846
+ const existingPoints = model.get("picked_points") || [];
2847
+ for (const point of existingPoints) {
2848
+ const cartesian = Cesium.Cartesian3.fromDegrees(
2849
+ point.longitude,
2850
+ point.latitude,
2851
+ point.altitude_wgs84
2852
+ );
2853
+ addPointMarker(cartesian, {
2854
+ point_id: point.id,
2855
+ color: point.color,
2856
+ label: point.label
2857
+ });
2858
+ }
2859
+ return {
2860
+ destroy: () => {
2861
+ stopPickingMode();
2862
+ clearPointMarkers();
2863
+ if (pickingPanel && pickingPanel.parentNode) {
2864
+ pickingPanel.remove();
2865
+ }
2866
+ log(PREFIX4, "Point picking module destroyed");
2867
+ }
2868
+ };
2869
+ }
2870
+
1974
2871
  // src/cesiumjs_anywidget/js/index.js
1975
- window.CESIUM_BASE_URL = "https://cesium.com/downloads/cesiumjs/releases/1.135/Build/Cesium/";
2872
+ window.CESIUM_BASE_URL = "https://cesium.com/downloads/cesiumjs/releases/1.137/Build/Cesium/";
1976
2873
  async function render({ model, el }) {
1977
2874
  setDebugMode(model.get("debug_mode") || false);
1978
2875
  model.on("change:debug_mode", () => {
@@ -1999,8 +2896,11 @@ async function render({ model, el }) {
1999
2896
  let viewer = null;
2000
2897
  let cameraSync = null;
2001
2898
  let measurementTools = null;
2899
+ let pointPicking = null;
2900
+ let photoProjection = null;
2002
2901
  let geoJsonLoader = null;
2003
2902
  let czmlLoader = null;
2903
+ let photorealisticTiles = null;
2004
2904
  (async () => {
2005
2905
  try {
2006
2906
  log("Main", "Creating Cesium Viewer...");
@@ -2015,6 +2915,9 @@ async function render({ model, el }) {
2015
2915
  log("Main", "Initializing measurement tools...");
2016
2916
  measurementTools = initializeMeasurementTools(viewer, model, container);
2017
2917
  log("Main", "Measurement tools initialized");
2918
+ log("Main", "Initializing point picking...");
2919
+ pointPicking = initializePointPicking(viewer, model, container);
2920
+ log("Main", "Point picking initialized");
2018
2921
  log("Main", "Setting up viewer listeners...");
2019
2922
  setupViewerListeners(viewer, model, container, Cesium);
2020
2923
  log("Main", "Viewer listeners set up");
@@ -2024,6 +2927,16 @@ async function render({ model, el }) {
2024
2927
  log("Main", "Setting up CZML loader...");
2025
2928
  czmlLoader = setupCZMLLoader(viewer, model, Cesium);
2026
2929
  log("Main", "CZML loader set up");
2930
+ log("Main", "Setting up Photorealistic 3D Tiles...");
2931
+ photorealisticTiles = setupPhotorealisticTiles(viewer, model, Cesium);
2932
+ log("Main", "Photorealistic 3D Tiles set up");
2933
+ model.on("change:camera_command", () => {
2934
+ const command = model.get("camera_command");
2935
+ if (command && command.command === "projectPhoto" && photoProjection) {
2936
+ log("Main", "Handling projectPhoto command");
2937
+ photoProjection.projectPhoto(command);
2938
+ }
2939
+ });
2027
2940
  log("Main", "Initialization complete");
2028
2941
  } catch (err) {
2029
2942
  error("Main", "Error initializing CesiumJS viewer:", err);
@@ -2041,6 +2954,14 @@ async function render({ model, el }) {
2041
2954
  log("Main", "Destroying measurement tools...");
2042
2955
  measurementTools.destroy();
2043
2956
  }
2957
+ if (photoProjection) {
2958
+ log("Main", "Destroying photo projection...");
2959
+ photoProjection.destroy();
2960
+ }
2961
+ if (pointPicking) {
2962
+ log("Main", "Destroying point picking...");
2963
+ pointPicking.destroy();
2964
+ }
2044
2965
  if (geoJsonLoader) {
2045
2966
  log("Main", "Destroying GeoJSON loader...");
2046
2967
  geoJsonLoader.destroy();
@@ -2049,6 +2970,10 @@ async function render({ model, el }) {
2049
2970
  log("Main", "Destroying CZML loader...");
2050
2971
  czmlLoader.destroy();
2051
2972
  }
2973
+ if (photorealisticTiles) {
2974
+ log("Main", "Destroying photorealistic tiles...");
2975
+ photorealisticTiles.destroy();
2976
+ }
2052
2977
  if (viewer) {
2053
2978
  log("Main", "Destroying viewer...");
2054
2979
  viewer.destroy();
@@ -2056,7 +2981,7 @@ async function render({ model, el }) {
2056
2981
  log("Main", "Cleanup complete");
2057
2982
  };
2058
2983
  }
2059
- var js_default = { render };
2984
+ var index_default = { render };
2060
2985
  export {
2061
- js_default as default
2986
+ index_default as default
2062
2987
  };