/**
* 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 };