From e02cc8b5ae3fb859bf48eea9b137d1138e2ec14a Mon Sep 17 00:00:00 2001 From: robyngit Date: Tue, 31 Mar 2026 11:19:15 -0400 Subject: [PATCH 1/3] Add a map-level debug flag for Cesium maps When enabled, the Cesium widget now shows tile/grid debug imagery, an FPS counter, a camera position overlay, and layer debug logging. This also adds a shared fixed-number formatter in Utilities and coverage for the new config and widget behavior. Issue #2807 --- src/css/map-view.css | 18 +++ src/js/common/Utilities.js | 12 ++ src/js/models/maps/Map.js | 9 ++ src/js/views/maps/CesiumWidgetView.js | 124 +++++++++++++++++- test/js/specs/unit/common/Utilities.spec.js | 16 +++ test/js/specs/unit/models/maps/Map.spec.js | 10 ++ .../unit/views/maps/CesiumWidgetView.spec.js | 29 ++++ 7 files changed, 217 insertions(+), 1 deletion(-) diff --git a/src/css/map-view.css b/src/css/map-view.css index a18691df0d..81c893f8e9 100644 --- a/src/css/map-view.css +++ b/src/css/map-view.css @@ -373,6 +373,24 @@ height: 100%; } +.cesium-debug-overlay { + position: absolute; + bottom: 1rem; + right: 1rem; + z-index: var(--map-z-index-base); + min-width: 11rem; + padding: 0.5rem 0.75rem; + border-radius: var(--map-border-radius-small); + background: rgba(19, 28, 43, 0.82); + box-shadow: var(--map-shadow-md); + color: #fff; + font-family: monospace; + font-size: 0.75rem; + line-height: 1.45; + white-space: pre-line; + pointer-events: none; +} + /***************************************************************************************** * * Scale Bar diff --git a/src/js/common/Utilities.js b/src/js/common/Utilities.js index a1fd6d796c..607b84a309 100644 --- a/src/js/common/Utilities.js +++ b/src/js/common/Utilities.js @@ -134,6 +134,18 @@ define([], () => { return value.toExponential(2).toString(); }, + /** + * Format a finite number using a fixed number of decimal places. + * @param {number} value The number value to be formatted. + * @param {number} [digits=2] The number of decimal places to display. + * @param {string} [fallback=""] The value to return when `value` is not finite. + * @returns {string} A fixed-decimal number string or the fallback value. + * @since 0.0.0 + */ + formatFixedNumber(value, digits = 2, fallback = "") { + return Number.isFinite(value) ? value.toFixed(digits) : fallback; + }, + /** * Calculate the number of decimal places we should use based on the range of the data. * @param {number} range The range of data values. diff --git a/src/js/models/maps/Map.js b/src/js/models/maps/Map.js index 4f3a204496..51c4804536 100644 --- a/src/js/models/maps/Map.js +++ b/src/js/models/maps/Map.js @@ -106,6 +106,12 @@ define([ * feedback section. showFeedback must be true for this to be shown. * @property {string} [globeBaseColor=null] - The base color of the globe * when no layer is shown. + * @property {boolean} [debug=false] - Enables Cesium's built-in map + * debugging aids, such as tile coordinate overlays, terrain wireframes, + * and a camera position overlay. This does not automatically enable + * layer-specific debug flags like 3D Tiles `debugShowGeometricError`; + * those can still be passed directly through a layer's + * `cesiumOptions`. * @property {MapConfig#ZoomPresets} [zoomPresets=null] - A predefined * list of locations with an enabled list of layer IDs to be shown the * zoom presets UI, or an object with a URL to fetch the presets from. @@ -238,6 +244,8 @@ define([ * feedback section. * @property {string} [globeBaseColor=null] - The base color of the globe * when no layer is shown. + * @property {boolean} [debug=false] - Enables Cesium's built-in map + * debugging aids and overlays for development. * @property {ZoomPresets} [zoomPresets=null] - A Backbone.Collection of a * predefined list of locations with an enabled list of layer IDs to be * shown the zoom presets UI. Requires `showViewfinder` to be true as this @@ -275,6 +283,7 @@ define([ showFeedback: false, feedbackText: null, globeBaseColor: null, + debug: false, zoomPresets: null, }; }, diff --git a/src/js/views/maps/CesiumWidgetView.js b/src/js/views/maps/CesiumWidgetView.js index 4da250f9a6..e58b085136 100644 --- a/src/js/views/maps/CesiumWidgetView.js +++ b/src/js/views/maps/CesiumWidgetView.js @@ -9,6 +9,7 @@ define([ "models/maps/assets/MapAsset", "models/maps/assets/Cesium3DTileset", "models/maps/Feature", + "common/Utilities", "text!templates/maps/cesium-widget-view.html", "common/SearchParams", ], ( @@ -20,6 +21,7 @@ define([ MapAsset, Cesium3DTileset, Feature, + Utilities, Template, SearchParams, ) => { @@ -204,6 +206,10 @@ define([ // Render the layers view.addLayers(); + if (view.isDebugEnabled()) { + view.enableDebugMode(); + } + const destination = SearchParams.getDestination(); if (this.model.get("showShareUrl") && destination) { // Go to position specified in query params. @@ -223,6 +229,15 @@ define([ } }, + /** + * Returns true when the map config enables Cesium debug mode. + * @returns {boolean} + * @since 0.0.0 + */ + isDebugEnabled() { + return Boolean(this.model?.get("debug")); + }, + /** * Create the Cesium Widget and save a reference to it to the view * @since 2.27.0 @@ -265,6 +280,102 @@ define([ return view.widget; }, + /** + * Enable Cesium's built-in map debugging aids for development. + * @since 0.0.0 + */ + enableDebugMode() { + this.scene.debugShowFramesPerSecond = true; + this.scene.globe.showSkirts = false; + this.showImageryGrid(); + this.renderDebugCameraOverlay(); + this.updateDebugCameraOverlay(); + this.logDebugLayerSummary(); + this.requestRender(); + }, + + /** + * Create the camera debug overlay if it doesn't exist yet. + * @returns {HTMLElement|null} The overlay element. + * @since 0.0.0 + */ + renderDebugCameraOverlay() { + if (!this.isDebugEnabled() || !this.el) { + return null; + } + + if (!this.debugCameraOverlay) { + this.debugCameraOverlay = this.el.ownerDocument.createElement("div"); + this.debugCameraOverlay.className = "cesium-debug-overlay"; + this.el.appendChild(this.debugCameraOverlay); + } + + return this.debugCameraOverlay; + }, + + /** + * Update the text shown in the camera debug overlay. + * @since 0.0.0 + */ + updateDebugCameraOverlay() { + const overlay = this.renderDebugCameraOverlay(); + if (!overlay || !this.camera) { + return; + } + + const cameraPosition = this.getCameraPosition(); + const formatNum = (num, digits = 2) => + Utilities.formatFixedNumber(num, digits, "n/a"); + const { longitude, latitude, height, heading, pitch, roll } = + cameraPosition; + overlay.textContent = [ + "Camera", + `lon: ${formatNum(longitude)}`, + `lat: ${formatNum(latitude)}`, + `height: ${formatNum(height)}`, + `heading: ${formatNum(heading)}`, + `pitch: ${formatNum(pitch)}`, + `roll: ${formatNum(roll)}`, + ].join("\n"); + }, + + /** + * Log a summary of the currently configured layers when debug mode is on. + * @since 0.0.0 + */ + logDebugLayerSummary() { + const allLayers = this.model.get("allLayers"); + const layers = allLayers + ? allLayers.map((layer) => ({ + label: layer.get("label"), + type: layer.get("type"), + status: layer.get("status"), + visible: layer.get("visible"), + })) + : []; + + console.info("[Cesium debug] Loaded layers", layers); + }, + + /** + * Log a single layer event when debug mode is on. + * @param {string} action The event that occurred. + * @param {MapAsset} mapAsset The layer involved. + * @since 0.0.0 + */ + logDebugLayerEvent(action, mapAsset) { + if (!this.isDebugEnabled() || !mapAsset) { + return; + } + + console.info(`[Cesium debug] ${action}`, { + label: mapAsset.get("label"), + type: mapAsset.get("type"), + status: mapAsset.get("status"), + visible: mapAsset.get("visible"), + }); + }, + /** * Create a DataSourceDisplay and DataSourceCollection for the Cesium * widget. This is required to display vector data (e.g. GeoJSON) on the @@ -488,6 +599,10 @@ define([ "debouncedUpdateSearchParams", ], }; + if (view.isDebugEnabled()) { + cameraEvents.moveEnd.push("updateDebugCameraOverlay"); + cameraEvents.changed.push("updateDebugCameraOverlay"); + } // add a listener that triggers the same event on the interactions // model, and runs any functions configured above. Object.entries(cameraEvents).forEach(([label, functions]) => { @@ -987,7 +1102,12 @@ define([ * {@link Map#defaults}) */ getCameraPosition() { - return this.getDegreesFromCartesian(this.camera.position); + return { + ...this.getDegreesFromCartesian(this.camera.position), + heading: Cesium.Math.toDegrees(this.camera.heading), + pitch: Cesium.Math.toDegrees(this.camera.pitch), + roll: Cesium.Math.toDegrees(this.camera.roll), + }; }, /** @@ -1442,6 +1562,7 @@ define([ if (!shouldRender) return false; renderFunction.call(view, cesiumModel); + view.logDebugLayerEvent("Rendered layer", mapAsset); if (listenModel) { listenModel.stopListening(mapAsset); listenModel.destroy(); @@ -1494,6 +1615,7 @@ define([ // If there is a function for this type of asset, call it if (removeFunction && typeof removeFunction === "function") { removeFunction.call(this, cesiumModel); + this.logDebugLayerEvent("Removed layer", mapAsset); } else { console.log( "No remove function found for this type of asset", diff --git a/test/js/specs/unit/common/Utilities.spec.js b/test/js/specs/unit/common/Utilities.spec.js index b3d5aff8f7..db04095fdf 100644 --- a/test/js/specs/unit/common/Utilities.spec.js +++ b/test/js/specs/unit/common/Utilities.spec.js @@ -51,6 +51,22 @@ define(["../../../../../../src/js/common/Utilities"], function (EntityUtils) { }); }); + describe("formatFixedNumber", () => { + it("formats finite numbers using fixed decimal places", () => { + expect(EntityUtils.formatFixedNumber(1.2345, 2)).to.equal("1.23"); + expect(EntityUtils.formatFixedNumber(1.2345, 0)).to.equal("1"); + }); + + it("returns the fallback for non-finite values", () => { + expect(EntityUtils.formatFixedNumber(Number.NaN, 2, "n/a")).to.equal( + "n/a", + ); + expect(EntityUtils.formatFixedNumber(Infinity, 2, "n/a")).to.equal( + "n/a", + ); + }); + }); + describe("deepEqual", () => { it("should return true if two objects are deeply equal", () => { const a = { a: 1, b: { c: 2 } }; diff --git a/test/js/specs/unit/models/maps/Map.spec.js b/test/js/specs/unit/models/maps/Map.spec.js index 6317d8a477..bcaa695b31 100644 --- a/test/js/specs/unit/models/maps/Map.spec.js +++ b/test/js/specs/unit/models/maps/Map.spec.js @@ -33,6 +33,10 @@ define([ expect(state.model).to.be.instanceof(Map); }); + it("defaults debug to false", () => { + expect(state.model.get("debug")).to.equal(false); + }); + it("ignores layers if layerCategories exist", () => { const map = new Map({ layerCategories: [{ layers: [{}] }], @@ -118,6 +122,12 @@ define([ .get("enabledLayerIds"), ).to.eql(["layer1"]); }); + + it("accepts debug from config", () => { + const map = new Map({ debug: true }); + + expect(map.get("debug")).to.equal(true); + }); }); describe("getLayerGroups", () => { diff --git a/test/js/specs/unit/views/maps/CesiumWidgetView.spec.js b/test/js/specs/unit/views/maps/CesiumWidgetView.spec.js index fa25f2cf09..bcf9177fd0 100644 --- a/test/js/specs/unit/views/maps/CesiumWidgetView.spec.js +++ b/test/js/specs/unit/views/maps/CesiumWidgetView.spec.js @@ -219,6 +219,35 @@ define([ roll: 340.4941155938977, }); }); + + it("enables Cesium debug helpers when configured", () => { + state.view.model.set("debug", true); + + state.view.render(); + + expect(state.view.scene.debugShowFramesPerSecond).to.equal(true); + expect(state.view.scene.globe.showSkirts).to.equal(false); + expect(state.view.scene.imageryLayers.length).to.equal(3); + const imageryProviders = Array.from( + { length: state.view.scene.imageryLayers.length }, + (_, index) => + state.view.scene.imageryLayers.get(index).imageryProvider, + ); + expect( + imageryProviders.some( + (provider) => provider instanceof Cesium.GridImageryProvider, + ), + ).to.equal(true); + expect( + imageryProviders.some( + (provider) => + provider instanceof Cesium.TileCoordinatesImageryProvider, + ), + ).to.equal(true); + expect( + state.view.el.querySelector(".cesium-debug-overlay")?.textContent, + ).to.contain("Camera"); + }); }); }); }); From 6b8a8161a6ce3782e4552fca6c548156e15238b4 Mon Sep 17 00:00:00 2001 From: robyngit Date: Tue, 31 Mar 2026 11:35:33 -0400 Subject: [PATCH 2/3] Ensure the FPS debug overlay is visible on Map Issue #2807 --- src/css/map-view.css | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/css/map-view.css b/src/css/map-view.css index 81c893f8e9..4d74aeb162 100644 --- a/src/css/map-view.css +++ b/src/css/map-view.css @@ -373,12 +373,9 @@ height: 100%; } -.cesium-debug-overlay { - position: absolute; - bottom: 1rem; - right: 1rem; - z-index: var(--map-z-index-base); - min-width: 11rem; +.cesium-debug-overlay, +.cesium-performanceDisplay { + min-width: 7rem; padding: 0.5rem 0.75rem; border-radius: var(--map-border-radius-small); background: rgba(19, 28, 43, 0.82); @@ -389,6 +386,28 @@ line-height: 1.45; white-space: pre-line; pointer-events: none; + text-align: left; + border: none; + color: #fff; +} + +.cesium-debug-overlay, +.cesium-performanceDisplay-defaultContainer { + z-index: var(--map-z-index-base); + position: absolute; + bottom: 1rem; +} + +.cesium-debug-overlay { + right: 1rem; +} + +.cesium-performanceDisplay-defaultContainer { + right: 12rem; +} + +.cesium-performanceDisplay { + min-width: 7rem; } /***************************************************************************************** From 9559eb58a0532cc74f818e02248c6104b475f295 Mon Sep 17 00:00:00 2001 From: robyngit Date: Tue, 31 Mar 2026 11:59:46 -0400 Subject: [PATCH 3/3] Add a show3DTilesInspector flag for Cesium maps When enabled, the Cesium widget now renders Cesium's built-in 3D Tiles inspector and lazy-loads the inspector stylesheet through MetacatUI's CSS loader. Issue #2807 --- src/css/Cesium3DTilesInspector.css | 261 ++++++++++++++++++ src/css/map-view.css | 14 + src/js/models/maps/Map.js | 6 + src/js/views/maps/CesiumWidgetView.js | 91 ++++++ test/js/specs/unit/models/maps/Map.spec.js | 6 + .../unit/views/maps/CesiumWidgetView.spec.js | 46 +++ 6 files changed, 424 insertions(+) create mode 100644 src/css/Cesium3DTilesInspector.css diff --git a/src/css/Cesium3DTilesInspector.css b/src/css/Cesium3DTilesInspector.css new file mode 100644 index 0000000000..3814c52537 --- /dev/null +++ b/src/css/Cesium3DTilesInspector.css @@ -0,0 +1,261 @@ +/* CesiumJS v 1.139.1: CesiumInspector/CesiumInspector.css */ + +.cesium-cesiumInspector { + border-radius: 5px; + transition: width ease-in-out 0.25s; + background: rgba(48, 51, 54, 0.8); + border: 1px solid #444; + color: #edffff; + display: inline-block; + position: relative; + padding: 4px 12px; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + overflow: hidden; +} + +.cesium-cesiumInspector-button { + text-align: center; + font-size: 11pt; +} + +.cesium-cesiumInspector-visible .cesium-cesiumInspector-button { + border-bottom: 1px solid #aaa; + padding-bottom: 3px; +} + +.cesium-cesiumInspector input:enabled, +.cesium-cesiumInspector-button { + cursor: pointer; +} + +.cesium-cesiumInspector-visible { + width: 185px; + height: auto; +} + +.cesium-cesiumInspector-hidden { + width: 122px; + height: 17px; +} + +.cesium-cesiumInspector-sectionContent { + max-height: 600px; +} + +.cesium-cesiumInspector-section-collapsed + .cesium-cesiumInspector-sectionContent { + max-height: 0; + padding: 0 !important; + overflow: hidden; +} + +.cesium-cesiumInspector-dropDown { + margin: 5px 0; + font-family: sans-serif; + font-size: 10pt; + width: 185px; +} + +.cesium-cesiumInspector-frustumStatistics { + padding-left: 10px; + padding: 5px; + background-color: rgba(80, 80, 80, 0.75); +} + +.cesium-cesiumInspector-pickButton { + background-color: rgba(0, 0, 0, 0.3); + border: 1px solid #444; + color: #edffff; + border-radius: 5px; + padding: 3px 7px; + cursor: pointer; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + margin: 0 auto; +} + +.cesium-cesiumInspector-pickButton:focus { + outline: none; +} + +.cesium-cesiumInspector-pickButton:active, +.cesium-cesiumInspector-pickButtonHighlight { + color: #000; /* For text buttons */ + background: #adf; + border-color: #fff; + box-shadow: 0 0 8px #fff; +} + +.cesium-cesiumInspector-center { + text-align: center; +} + +.cesium-cesiumInspector-sectionHeader { + font-weight: bold; + font-size: 10pt; + margin: 0; + cursor: pointer; +} + +.cesium-cesiumInspector-pickSection { + border: 1px solid #aaa; + border-radius: 5px; + padding: 3px; + margin-bottom: 5px; +} + +.cesium-cesiumInspector-sectionContent { + margin-bottom: 10px; + transition: max-height 0.25s; +} + +.cesium-cesiumInspector-tileText { + padding-bottom: 10px; + border-bottom: 1px solid #aaa; +} + +.cesium-cesiumInspector-relativeText { + padding-top: 10px; +} + +.cesium-cesiumInspector-sectionHeader::before { + margin-right: 5px; + content: "-"; + width: 1ch; + display: inline-block; +} + +.cesium-cesiumInspector-section-collapsed + .cesium-cesiumInspector-sectionHeader::before { + content: "+"; +} + +/* CesiumJS v 1.139.1: Cesium3DTilesInspector/Cesium3DTilesInspector.css */ + +ul.cesium-cesiumInspector-statistics { + margin: 0; + padding-top: 3px; + padding-bottom: 3px; +} + +ul.cesium-cesiumInspector-statistics + ul.cesium-cesiumInspector-statistics { + border-top: 1px solid #aaa; +} + +.cesium-cesiumInspector-slider { + margin-top: 5px; +} + +.cesium-cesiumInspector-slider input[type="number"] { + text-align: left; + background-color: #222; + outline: none; + border: 1px solid #444; + color: #edffff; + width: 100px; + border-radius: 3px; + padding: 1px; + margin-left: 10px; + cursor: auto; +} + +.cesium-cesiumInspector-slider input[type="number"]::-webkit-outer-spin-button, +.cesium-cesiumInspector-slider input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.cesium-cesiumInspector-slider input[type="range"] { + margin-left: 5px; + vertical-align: middle; +} + +.cesium-cesiumInspector-hide .cesium-cesiumInspector-styleEditor { + display: none; +} + +.cesium-cesiumInspector-styleEditor { + padding: 10px; + border-radius: 5px; + background: rgba(48, 51, 54, 0.8); + border: 1px solid #444; +} + +.cesium-cesiumInspector-styleEditor textarea { + width: 100%; + height: 300px; + background: transparent; + color: #edffff; + border: none; + padding: 0; + white-space: pre; + overflow-wrap: normal; + overflow-x: auto; +} + +.cesium-3DTilesInspector { + width: 300px; + pointer-events: all; +} + +.cesium-3DTilesInspector-statistics { + font-size: 11px; +} + +.cesium-3DTilesInspector-disabledElementsInfo { + margin: 5px 0 0 0; + padding: 0 0 0 20px; + color: #eed202; +} + +.cesium-3DTilesInspector div, +.cesium-3DTilesInspector input[type="range"] { + width: 100%; + box-sizing: border-box; +} + +.cesium-cesiumInspector-error { + color: #ff9e9e; + overflow: auto; +} + +.cesium-3DTilesInspector .cesium-cesiumInspector-section { + margin-top: 3px; +} + +.cesium-3DTilesInspector + .cesium-cesiumInspector-sectionHeader + + .cesium-cesiumInspector-show { + border-top: 1px solid white; +} + +input.cesium-cesiumInspector-url { + overflow: hidden; + white-space: nowrap; + overflow-x: scroll; + background-color: transparent; + color: white; + outline: none; + border: none; + height: 1em; + width: 100%; +} + +.cesium-cesiumInspector .field-group { + display: table; +} + +.cesium-cesiumInspector .field-group > label { + display: table-cell; + font-weight: bold; +} + +.cesium-cesiumInspector .field-group > .field { + display: table-cell; + width: 100%; +} diff --git a/src/css/map-view.css b/src/css/map-view.css index 4d74aeb162..704ebaaaed 100644 --- a/src/css/map-view.css +++ b/src/css/map-view.css @@ -410,6 +410,20 @@ min-width: 7rem; } +.cesium-3d-tiles-inspector-container { + position: absolute; + top: 4rem; + right: 1rem; + z-index: var(--map-z-index-base); + max-height: calc(100% - 5rem); + overflow-y: auto; + overflow-x: hidden; +} + +.cesium-3d-tiles-inspector-container h3 { + line-height: unset; +} + /***************************************************************************************** * * Scale Bar diff --git a/src/js/models/maps/Map.js b/src/js/models/maps/Map.js index 51c4804536..802b06c25c 100644 --- a/src/js/models/maps/Map.js +++ b/src/js/models/maps/Map.js @@ -112,6 +112,9 @@ define([ * layer-specific debug flags like 3D Tiles `debugShowGeometricError`; * those can still be passed directly through a layer's * `cesiumOptions`. + * @property {boolean} [show3DTilesInspector=false] - Whether or not to + * show Cesium's built-in 3D Tiles inspector widget for tileset + * debugging. * @property {MapConfig#ZoomPresets} [zoomPresets=null] - A predefined * list of locations with an enabled list of layer IDs to be shown the * zoom presets UI, or an object with a URL to fetch the presets from. @@ -246,6 +249,8 @@ define([ * when no layer is shown. * @property {boolean} [debug=false] - Enables Cesium's built-in map * debugging aids and overlays for development. + * @property {boolean} [show3DTilesInspector=false] - Whether or not to + * show Cesium's built-in 3D Tiles inspector widget. * @property {ZoomPresets} [zoomPresets=null] - A Backbone.Collection of a * predefined list of locations with an enabled list of layer IDs to be * shown the zoom presets UI. Requires `showViewfinder` to be true as this @@ -284,6 +289,7 @@ define([ feedbackText: null, globeBaseColor: null, debug: false, + show3DTilesInspector: false, zoomPresets: null, }; }, diff --git a/src/js/views/maps/CesiumWidgetView.js b/src/js/views/maps/CesiumWidgetView.js index e58b085136..7586b75645 100644 --- a/src/js/views/maps/CesiumWidgetView.js +++ b/src/js/views/maps/CesiumWidgetView.js @@ -209,6 +209,9 @@ define([ if (view.isDebugEnabled()) { view.enableDebugMode(); } + if (view.is3DTilesInspectorEnabled()) { + view.render3DTilesInspector(); + } const destination = SearchParams.getDestination(); if (this.model.get("showShareUrl") && destination) { @@ -238,6 +241,16 @@ define([ return Boolean(this.model?.get("debug")); }, + /** + * Returns true when the map config enables the Cesium 3D Tiles + * inspector. + * @returns {boolean} + * @since 0.0.0 + */ + is3DTilesInspectorEnabled() { + return Boolean(this.model?.get("show3DTilesInspector")); + }, + /** * Create the Cesium Widget and save a reference to it to the view * @since 2.27.0 @@ -294,6 +307,83 @@ define([ this.requestRender(); }, + /** + * Add the Cesium 3D Tiles inspector stylesheet to the app once, when + * the inspector is actually needed. + * @since 0.0.0 + */ + load3DTilesInspectorCSS() { + const cssID = "cesium3DTilesInspector"; + + if (MetacatUI.loadedCSS?.includes(cssID)) { + return; + } + + require([`text!${MetacatUI.root}/css/Cesium3DTilesInspector.css`], ( + inspectorCSS, + ) => { + MetacatUI.appModel.addCSS(inspectorCSS, cssID); + }); + }, + + /** + * Create and render Cesium's built-in 3D Tiles inspector widget. + * @returns {Cesium.Cesium3DTilesInspector|null} The inspector widget. + * @since 0.0.0 + */ + render3DTilesInspector() { + if ( + !this.is3DTilesInspectorEnabled() || + !this.el || + !this.scene || + typeof Cesium.Cesium3DTilesInspector !== "function" + ) { + return null; + } + + this.load3DTilesInspectorCSS(); + + if (!this.tilesInspectorContainer) { + this.tilesInspectorContainer = + this.el.ownerDocument.createElement("div"); + this.tilesInspectorContainer.className = + "cesium-3d-tiles-inspector-container"; + this.el.appendChild(this.tilesInspectorContainer); + } + + if (!this.tilesInspector) { + this.tilesInspector = new Cesium.Cesium3DTilesInspector( + this.tilesInspectorContainer, + this.scene, + ); + } + + return this.tilesInspector; + }, + + /** + * Destroy the 3D Tiles inspector widget and remove its container. + */ + destroy3DTilesInspector() { + if (this.tilesInspector) { + const isDestroyed = + typeof this.tilesInspector.isDestroyed === "function" && + this.tilesInspector.isDestroyed(); + if ( + !isDestroyed && + typeof this.tilesInspector.destroy === "function" + ) { + this.tilesInspector.destroy(); + } + this.tilesInspector = null; + } + + if (this.tilesInspectorContainer) { + this.tilesInspectorContainer.remove(); + this.tilesInspectorContainer = null; + } + }, + /** * Create the camera debug overlay if it doesn't exist yet. * @returns {HTMLElement|null} The overlay element. @@ -1799,6 +1889,7 @@ define([ /** Remove nav and mouse listeners when the view is closed */ onClose() { + this.destroy3DTilesInspector(); this.removeMouseListeners(); this.removeNavigationListeners(); }, diff --git a/test/js/specs/unit/models/maps/Map.spec.js b/test/js/specs/unit/models/maps/Map.spec.js index bcaa695b31..aca7230caa 100644 --- a/test/js/specs/unit/models/maps/Map.spec.js +++ b/test/js/specs/unit/models/maps/Map.spec.js @@ -128,6 +128,12 @@ define([ expect(map.get("debug")).to.equal(true); }); + + it("accepts show3DTilesInspector from config", () => { + const map = new Map({ show3DTilesInspector: true }); + + expect(map.get("show3DTilesInspector")).to.equal(true); + }); }); describe("getLayerGroups", () => { diff --git a/test/js/specs/unit/views/maps/CesiumWidgetView.spec.js b/test/js/specs/unit/views/maps/CesiumWidgetView.spec.js index bcf9177fd0..37600a87fa 100644 --- a/test/js/specs/unit/views/maps/CesiumWidgetView.spec.js +++ b/test/js/specs/unit/views/maps/CesiumWidgetView.spec.js @@ -17,6 +17,8 @@ define([ const spy = sinon.spy(); describe("CesiumWidgetView Test Suite", () => { + let tilesInspectorStub; + const state = cleanState(() => { SearchParams.clearSavedView(); @@ -28,6 +30,8 @@ define([ afterEach(() => { SearchParams.clearSavedView(); spy.resetHistory(); + tilesInspectorStub?.restore(); + tilesInspectorStub = null; }); describe("Initialization", () => { @@ -248,6 +252,48 @@ define([ state.view.el.querySelector(".cesium-debug-overlay")?.textContent, ).to.contain("Camera"); }); + + it("renders the 3D Tiles inspector when configured", () => { + tilesInspectorStub = sinon + .stub(Cesium, "Cesium3DTilesInspector") + .callsFake(function (container, scene) { + this.container = container; + this.scene = scene; + this.destroy = sinon.spy(); + this.isDestroyed = () => false; + }); + state.view.load3DTilesInspectorCSS = sinon.spy(); + state.view.model.set("show3DTilesInspector", true); + + state.view.render(); + + expect(state.view.load3DTilesInspectorCSS.calledOnce).to.equal(true); + expect(tilesInspectorStub.callCount).to.equal(1); + expect(tilesInspectorStub.args[0][1]).to.equal(state.view.scene); + expect( + state.view.el.querySelector(".cesium-3d-tiles-inspector-container"), + ).to.equal(state.view.tilesInspectorContainer); + }); + }); + + describe("cleanup", () => { + it("destroys the 3D Tiles inspector on close", () => { + const destroy = sinon.spy(); + const container = document.createElement("div"); + state.view.el.appendChild(container); + state.view.tilesInspector = { + destroy, + isDestroyed: () => false, + }; + state.view.tilesInspectorContainer = container; + + state.view.onClose(); + + expect(destroy.calledOnce).to.equal(true); + expect(state.view.tilesInspector).to.equal(null); + expect(state.view.tilesInspectorContainer).to.equal(null); + expect(state.view.el.contains(container)).to.equal(false); + }); }); }); });