/** * MultispectralUI - User interface components for multispectral visualization * * Provides interactive controls for manipulating visualization parameters in * the LayerMultispectral class. Features include preset selection, single band * visualization controls, and adaptive UI positioning. * * The UI can be configured as a floating panel or embedded within an existing * container element, adapting automatically to the available space. */ class MultispectralUI { /** * Creates a new MultispectralUI instance * * @param {LayerMultispectral} layer - Multispectral layer to control * @param {Object} [options] - UI configuration options * @param {string} [options.containerId] - ID of container element for UI (optional) * @param {boolean} [options.showPresets=true] - Whether to show preset selection controls * @param {boolean} [options.showSingleBand=true] - Whether to show single band control panel * @param {boolean} [options.floatingPanel=true] - Whether to create a floating panel UI */ constructor(layer, options = {}) { this.layer = layer; // Default options this.options = { containerId: null, showPresets: true, showSingleBand: true, floatingPanel: true, ...options }; // UI state this.uiElements = {}; // Initialize when layer is ready if (layer.status === 'ready') { this.initialize(); } else { layer.addEvent('ready', () => this.initialize()); } } /** * Initializes the UI components * * Sets up the container element, creates UI controls, and configures * event handling based on the provided options. * * @private */ initialize() { // Get container element let container; let targetContainer; if (this.options.containerId) { // Use existing container if ID provided container = document.getElementById(this.options.containerId); targetContainer = container; if (!container) { console.error(`Container element with ID '${this.options.containerId}' not found`); return; } } else if (this.options.floatingPanel) { // Create floating panel if no container specified // Find OpenLIME container to use as a relative parent let openlimeContainer; // Try to get viewer's container from the layer if (this.layer.viewer && this.layer.viewer.containerElement) { openlimeContainer = this.layer.viewer.containerElement; } else { // Fallback to looking for .openlime class openlimeContainer = document.querySelector('.openlime'); if (!openlimeContainer) { console.warn('OpenLIME container not found, using body as fallback'); openlimeContainer = document.body; } } // Make the parent container positioned if it's not already if (getComputedStyle(openlimeContainer).position === 'static') { openlimeContainer.style.position = 'relative'; } // Create floating container container = document.createElement('div'); container.className = 'ms-controls'; container.style.position = 'absolute'; container.style.top = '10px'; container.style.right = '10px'; container.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; container.style.padding = '10px'; container.style.borderRadius = '5px'; container.style.color = 'white'; container.style.zIndex = '1000'; container.style.width = '250px'; container.style.maxHeight = '80vh'; container.style.overflowY = 'auto'; container.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.3)'; container.style.fontFamily = 'Arial, sans-serif'; // Append to OpenLIME container instead of document.body openlimeContainer.appendChild(container); targetContainer = openlimeContainer; } else { console.error('No container specified and floating panel disabled'); return; } this.container = container; this.targetContainer = targetContainer; // Create UI components this.createHeader(); if (this.options.showPresets) { this.createPresetSelector(); } if (this.options.showSingleBand) { this.createSingleBandControls(); } // Update positioning on window resize this.setupResizeHandler(); } /** * Sets up window resize event handler to update panel positioning * * Ensures the UI panel remains properly positioned and sized when * the window or container is resized. * * @private */ setupResizeHandler() { // Only needed for floating panel if (!this.options.floatingPanel || !this.container) return; // Store initial container dimensions this.initialContainerRect = this.targetContainer.getBoundingClientRect(); // Define resize handler this.resizeHandler = () => { // Handle potential edge case where container/targetContainer is removed from DOM if (!document.contains(this.container) || !document.contains(this.targetContainer)) { window.removeEventListener('resize', this.resizeHandler); return; } // Ensure the panel stays visible on resize const containerRect = this.targetContainer.getBoundingClientRect(); const panelRect = this.container.getBoundingClientRect(); // If the container width gets too small, adjust the panel width if (containerRect.width < 300) { this.container.style.width = Math.max(containerRect.width * 0.8, 150) + 'px'; } else { // Reset to default width this.container.style.width = '250px'; } // Ensure the panel is fully visible const rightEdgeOffset = panelRect.right - containerRect.right; if (rightEdgeOffset > 0) { // Panel extends beyond right edge, adjust position const currentRight = parseInt(this.container.style.right) || 10; this.container.style.right = (currentRight + rightEdgeOffset + 10) + 'px'; } }; // Add resize listener window.addEventListener('resize', this.resizeHandler); // Initial call to handle any existing size issues this.resizeHandler(); } /** * Creates header UI element with title and band information * * Displays the title and key information about the multispectral * dataset including band count and wavelength range. * * @private */ createHeader() { const headerDiv = document.createElement('div'); headerDiv.style.marginBottom = '10px'; const title = document.createElement('h3'); title.textContent = 'Multispectral Controls'; title.style.margin = '0 0 5px 0'; title.style.fontSize = '16px'; title.style.fontWeight = 'bold'; headerDiv.appendChild(title); const bandInfo = document.createElement('div'); bandInfo.textContent = `${this.layer.getBandCount()} bands: ${Math.min(...this.layer.getWavelengths())}nm - ${Math.max(...this.layer.getWavelengths())}nm`; bandInfo.style.fontSize = '12px'; bandInfo.style.color = '#ccc'; headerDiv.appendChild(bandInfo); this.container.appendChild(headerDiv); } /** * Creates preset selector UI element * * Provides a dropdown menu for selecting predefined Color Twist Weight * configurations from the available presets. * * @private */ createPresetSelector() { const presetDiv = document.createElement('div'); presetDiv.style.marginBottom = '15px'; const label = document.createElement('label'); label.textContent = 'Analysis Presets'; label.style.display = 'block'; label.style.marginBottom = '5px'; label.style.fontSize = '14px'; label.style.fontWeight = 'bold'; presetDiv.appendChild(label); const presetSelector = document.createElement('select'); presetSelector.style.width = '100%'; presetSelector.style.padding = '5px'; presetSelector.style.backgroundColor = '#333'; presetSelector.style.color = 'white'; presetSelector.style.border = '1px solid #555'; presetSelector.style.borderRadius = '3px'; // Add default option const defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.textContent = '-- Select Preset --'; presetSelector.appendChild(defaultOption); // Add presets from layer const presets = this.layer.getAvailablePresets(); presets.forEach(preset => { const option = document.createElement('option'); option.value = preset; // Format preset name for display (camelCase to Title Case) const formattedName = preset .replace(/([A-Z])/g, ' $1') .replace(/^./, str => str.toUpperCase()); option.textContent = formattedName; presetSelector.appendChild(option); }); // Add apply button for preset selection const applyButton = document.createElement('button'); applyButton.textContent = 'Apply'; applyButton.style.width = '100%'; applyButton.style.marginTop = '5px'; applyButton.style.padding = '5px'; applyButton.style.backgroundColor = '#555'; applyButton.style.color = 'white'; applyButton.style.border = 'none'; applyButton.style.borderRadius = '3px'; applyButton.style.cursor = 'pointer'; // Function to apply the selected preset const applySelectedPreset = () => { const selectedPreset = presetSelector.value; if (selectedPreset) { this.layer.applyPreset(selectedPreset); // Set the layer mode to 'rgb' for preset visualization if (this.layer.getMode() !== 'rgb') { this.layer.setMode('rgb'); } } }; // // Still keep the change event for convenience // presetSelector.addEventListener('change', () => { // applySelectedPreset(); // }); // Add click handler for the apply button applyButton.addEventListener('click', () => { applySelectedPreset(); }); presetDiv.appendChild(presetSelector); presetDiv.appendChild(applyButton); this.container.appendChild(presetDiv); this.uiElements.presetSelector = presetSelector; } /** * Creates single band visualization controls * * Provides controls for selecting a specific spectral band and * output channel for single-band visualization. * * @private */ createSingleBandControls() { const singleBandDiv = document.createElement('div'); singleBandDiv.style.marginBottom = '15px'; const label = document.createElement('label'); label.textContent = 'Single Band View'; label.style.display = 'block'; label.style.marginBottom = '5px'; label.style.fontSize = '14px'; label.style.fontWeight = 'bold'; singleBandDiv.appendChild(label); // Create wavelength selector const wavelengthDiv = document.createElement('div'); wavelengthDiv.style.display = 'flex'; wavelengthDiv.style.alignItems = 'center'; wavelengthDiv.style.marginBottom = '5px'; const wavelengthLabel = document.createElement('span'); wavelengthLabel.textContent = 'Wavelength:'; wavelengthLabel.style.width = '80px'; wavelengthLabel.style.fontSize = '12px'; const wavelengthSelector = document.createElement('select'); wavelengthSelector.style.flex = '1'; wavelengthSelector.style.padding = '3px'; wavelengthSelector.style.backgroundColor = '#333'; wavelengthSelector.style.color = 'white'; wavelengthSelector.style.border = '1px solid #555'; wavelengthSelector.style.borderRadius = '3px'; // Populate wavelength options const wavelengths = this.layer.getWavelengths(); wavelengths.forEach((wavelength, index) => { const option = document.createElement('option'); option.value = index; option.textContent = `${wavelength}nm`; wavelengthSelector.appendChild(option); }); wavelengthDiv.appendChild(wavelengthLabel); wavelengthDiv.appendChild(wavelengthSelector); singleBandDiv.appendChild(wavelengthDiv); // Create output channel selector const channelDiv = document.createElement('div'); channelDiv.style.display = 'flex'; channelDiv.style.alignItems = 'center'; const channelLabel = document.createElement('span'); channelLabel.textContent = 'Output Channel:'; channelLabel.style.width = '80px'; channelLabel.style.fontSize = '12px'; const channelSelector = document.createElement('select'); channelSelector.style.flex = '1'; channelSelector.style.padding = '3px'; channelSelector.style.backgroundColor = '#333'; channelSelector.style.color = 'white'; channelSelector.style.border = '1px solid #555'; channelSelector.style.borderRadius = '3px'; // Add channel options const channels = [ { value: 0, label: 'Gray' }, { value: 1, label: 'Red' }, { value: 2, label: 'Green' }, { value: 3, label: 'Blue' } ]; channels.forEach(channel => { const option = document.createElement('option'); option.value = channel.value; option.textContent = channel.label; channelSelector.appendChild(option); }); channelDiv.appendChild(channelLabel); channelDiv.appendChild(channelSelector); singleBandDiv.appendChild(channelDiv); // Apply button const applyButton = document.createElement('button'); applyButton.textContent = 'Apply'; applyButton.style.width = '100%'; applyButton.style.marginTop = '5px'; applyButton.style.padding = '5px'; applyButton.style.backgroundColor = '#555'; applyButton.style.color = 'white'; applyButton.style.border = 'none'; applyButton.style.borderRadius = '3px'; applyButton.style.cursor = 'pointer'; applyButton.addEventListener('click', () => { const bandIndex = parseInt(wavelengthSelector.value); const channelIndex = parseInt(channelSelector.value); this.layer.setSingleBand(bandIndex, channelIndex); // Update mode selector if it exists if (this.uiElements.modeSelector) { this.uiElements.modeSelector.value = 'single_band'; } }); singleBandDiv.appendChild(applyButton); this.container.appendChild(singleBandDiv); this.uiElements.wavelengthSelector = wavelengthSelector; this.uiElements.channelSelector = channelSelector; } /** * Destroys UI and removes elements from DOM * * Cleans up all created UI elements and event listeners. * Call this method before removing the layer to prevent memory leaks. */ destroy() { // Remove resize event listener if (this.resizeHandler) { window.removeEventListener('resize', this.resizeHandler); this.resizeHandler = null; } // Remove container from DOM if it's a floating panel if (this.container && this.options.floatingPanel && this.targetContainer) { this.targetContainer.removeChild(this.container); } this.uiElements = {}; this.container = null; this.targetContainer = null; } } export { MultispectralUI };