Skip to main content

oxigdal_wasm/
lib.rs

1//! # OxiGDAL WASM - WebAssembly Bindings for Browser-based Geospatial Processing
2//!
3//! This crate provides comprehensive WebAssembly bindings for OxiGDAL, enabling
4//! high-performance browser-based geospatial data processing with a focus on
5//! Cloud Optimized GeoTIFF (COG) visualization and manipulation.
6//!
7//! ## Features
8//!
9//! ### Core Capabilities
10//! - **COG Viewing**: Efficient viewing of Cloud Optimized GeoTIFFs
11//! - **Tile Management**: Advanced tile caching and pyramid management
12//! - **Progressive Rendering**: Smooth progressive loading with adaptive quality
13//! - **Image Processing**: Color manipulation, contrast enhancement, filters
14//! - **Performance Profiling**: Built-in profiling and bottleneck detection
15//! - **Worker Pool**: Parallel tile loading using Web Workers
16//! - **Streaming**: Adaptive tile streaming with bandwidth estimation
17//!
18//! ### Advanced Features
19//! - **Compression**: Multiple compression algorithms for bandwidth reduction
20//! - **Color Operations**: Extensive color space conversions and palettes
21//! - **TypeScript Bindings**: Auto-generated TypeScript definitions
22//! - **Error Handling**: Comprehensive error types and recovery
23//! - **Viewport Management**: Advanced viewport transformations and history
24//!
25//! ## Architecture
26//!
27//! The crate is organized into several modules:
28//!
29//! - `bindings`: TypeScript type definitions and documentation generation
30//! - `canvas`: Image processing, resampling, and canvas rendering utilities
31//! - `color`: Advanced color manipulation, palettes, and color correction
32//! - `compression`: Tile compression algorithms (RLE, Delta, Huffman, LZ77)
33//! - `error`: Comprehensive error types for all operations
34//! - `fetch`: HTTP fetching with retry logic and parallel requests
35//! - `profiler`: Performance profiling and bottleneck detection
36//! - `rendering`: Canvas rendering, double buffering, and progressive rendering
37//! - `streaming`: Adaptive tile streaming with bandwidth management
38//! - `tile`: Tile coordinate systems, caching, and pyramid management
39//! - `worker`: Web Worker pool for parallel processing
40//!
41//! ## Basic Usage Example (JavaScript)
42//!
43//! ```javascript
44//! import init, { WasmCogViewer } from '@cooljapan/oxigdal';
45//!
46//! async function viewCog(url) {
47//!     // Initialize the WASM module
48//!     await init();
49//!
50//!     // Create a viewer instance
51//!     const viewer = new WasmCogViewer();
52//!
53//!     // Open a COG file
54//!     await viewer.open(url);
55//!
56//!     // Get image metadata
57//!     console.log(`Image size: ${viewer.width()}x${viewer.height()}`);
58//!     console.log(`Tile size: ${viewer.tile_width()}x${viewer.tile_height()}`);
59//!     console.log(`Bands: ${viewer.band_count()}`);
60//!     console.log(`Overviews: ${viewer.overview_count()}`);
61//!
62//!     // Read a tile as ImageData for canvas rendering
63//!     const imageData = await viewer.read_tile_as_image_data(0, 0, 0);
64//!
65//!     // Render to canvas
66//!     const canvas = document.getElementById('map-canvas');
67//!     const ctx = canvas.getContext('2d');
68//!     ctx.putImageData(imageData, 0, 0);
69//! }
70//! ```
71//!
72//! ## Advanced Usage Example (JavaScript)
73//!
74//! ```javascript
75//! import init, {
76//!     AdvancedCogViewer,
77//!     WasmImageProcessor,
78//!     WasmColorPalette,
79//!     WasmProfiler,
80//!     WasmTileCache
81//! } from '@cooljapan/oxigdal';
82//!
83//! async function advancedProcessing() {
84//!     await init();
85//!
86//!     // Create an advanced viewer with caching
87//!     const viewer = new AdvancedCogViewer();
88//!     await viewer.open('https://example.com/image.tif', 100); // 100MB cache
89//!
90//!     // Setup profiling
91//!     const profiler = new WasmProfiler();
92//!     profiler.startTimer('tile_load');
93//!
94//!     // Load and process a tile
95//!     const imageData = await viewer.readTileAsImageData(0, 0, 0);
96//!     profiler.stopTimer('tile_load');
97//!
98//!     // Apply color palette
99//!     const palette = WasmColorPalette.createViridis();
100//!     const imageBytes = new Uint8Array(imageData.data.buffer);
101//!     palette.applyToGrayscale(imageBytes);
102//!
103//!     // Apply image processing
104//!     WasmImageProcessor.linearStretch(imageBytes, imageData.width, imageData.height);
105//!
106//!     // Get cache statistics
107//!     const cacheStats = viewer.getCacheStats();
108//!     console.log('Cache hit rate:', JSON.parse(cacheStats).hit_count);
109//!
110//!     // Get profiling statistics
111//!     const profStats = profiler.getAllStats();
112//!     console.log('Performance:', profStats);
113//! }
114//! ```
115//!
116//! ## Progressive Loading Example (JavaScript)
117//!
118//! ```javascript
119//! async function progressiveLoad(url, canvas) {
120//!     const viewer = new AdvancedCogViewer();
121//!     await viewer.open(url, 100);
122//!
123//!     // Start with low quality for quick feedback
124//!     viewer.setViewportSize(canvas.width, canvas.height);
125//!     viewer.fitToImage();
126//!
127//!     const ctx = canvas.getContext('2d');
128//!
129//!     // Load visible tiles progressively
130//!     const viewport = JSON.parse(viewer.getViewport());
131//!     for (let level = viewer.overview_count(); level >= 0; level--) {
132//!         // Load tiles at this level
133//!         const imageData = await viewer.readTileAsImageData(level, 0, 0);
134//!         ctx.putImageData(imageData, 0, 0);
135//!
136//!         // Allow UI updates
137//!         await new Promise(resolve => setTimeout(resolve, 0));
138//!     }
139//! }
140//! ```
141//!
142//! ## Performance Considerations
143//!
144//! ### Memory Management
145//! - The tile cache automatically evicts old tiles using LRU strategy
146//! - Configure cache size based on available memory
147//! - Use compression to reduce memory footprint
148//!
149//! ### Network Optimization
150//! - HTTP range requests are used for partial file reads
151//! - Retry logic handles network failures gracefully
152//! - Parallel requests improve throughput
153//! - Adaptive streaming adjusts quality based on bandwidth
154//!
155//! ### Rendering Performance
156//! - Double buffering prevents flickering
157//! - Progressive rendering provides quick feedback
158//! - Web Workers enable parallel tile processing
159//! - Canvas operations are optimized for WASM
160//!
161//! ## Error Handling
162//!
163//! All operations return `Result` types that can be converted to JavaScript
164//! exceptions. Errors are categorized by type:
165//!
166//! - `FetchError`: Network and HTTP errors
167//! - `CanvasError`: Canvas and rendering errors
168//! - `WorkerError`: Web Worker errors
169//! - `TileCacheError`: Cache management errors
170//! - `JsInteropError`: JavaScript interop errors
171//!
172//! ```javascript
173//! try {
174//!     await viewer.open(url);
175//! } catch (error) {
176//!     if (error.message.includes('HTTP 404')) {
177//!         console.error('File not found');
178//!     } else if (error.message.includes('CORS')) {
179//!         console.error('Cross-origin request blocked');
180//!     } else {
181//!         console.error('Unknown error:', error);
182//!     }
183//! }
184//! ```
185//!
186//! ## Browser Compatibility
187//!
188//! This crate requires:
189//! - WebAssembly support
190//! - Fetch API with range request support
191//! - Canvas API
192//! - Web Workers (optional, for parallel processing)
193//! - Performance API (optional, for profiling)
194//!
195//! Supported browsers:
196//! - Chrome 57+
197//! - Firefox 52+
198//! - Safari 11+
199//! - Edge 16+
200//!
201//! ## Building for Production
202//!
203//! ```bash
204//! # Optimize for size
205//! wasm-pack build --target web --release -- --features optimize-size
206//!
207//! # Optimize for speed
208//! wasm-pack build --target web --release -- --features optimize-speed
209//!
210//! # Generate TypeScript definitions
211//! wasm-pack build --target bundler --release
212//! ```
213//!
214//! ## License
215//!
216//! This crate is part of the OxiGDAL project and follows the same licensing terms.
217
218#![warn(missing_docs)]
219#![warn(clippy::all)]
220#![deny(clippy::unwrap_used)]
221// WASM crate allows - for internal implementation patterns
222#![allow(clippy::needless_range_loop)]
223#![allow(clippy::expect_used)]
224#![allow(clippy::should_implement_trait)]
225#![allow(clippy::new_without_default)]
226#![allow(clippy::ptr_arg)]
227#![allow(clippy::type_complexity)]
228
229use serde::{Deserialize, Serialize};
230use wasm_bindgen::prelude::*;
231use web_sys::{ImageData, console};
232
233use oxigdal_core::error::OxiGdalError;
234use oxigdal_core::io::ByteRange;
235
236mod animation;
237mod bindings;
238mod canvas;
239mod cog_reader;
240mod color;
241mod compression;
242mod error;
243mod fetch;
244mod profiler;
245mod rendering;
246mod streaming;
247#[cfg(test)]
248mod tests;
249mod tile;
250mod worker;
251
252// WASM Component Model (wasm32-wasip2) support
253pub mod component;
254pub mod wasm_memory;
255
256pub use animation::{
257    Animation, Easing, EasingFunction, PanAnimation, SpringAnimation, ZoomAnimation,
258};
259pub use bindings::{
260    DocGenerator, TsClass, TsFunction, TsInterface, TsModule, TsParameter, TsType, TsTypeAlias,
261    create_oxigdal_wasm_docs,
262};
263pub use canvas::{
264    ChannelHistogramJson, ContrastMethod, CustomBinHistogramJson, Histogram, HistogramJson, Hsv,
265    ImageProcessor, ImageStats, ResampleMethod, Resampler, Rgb, WasmImageProcessor, YCbCr,
266};
267pub use color::{
268    ChannelOps, ColorCorrectionMatrix, ColorPalette, ColorQuantizer, ColorTemperature,
269    GradientGenerator, PaletteEntry, WasmColorPalette, WhiteBalance,
270};
271pub use compression::{
272    CompressionAlgorithm, CompressionBenchmark, CompressionSelector, CompressionStats,
273    DeltaCompressor, HuffmanCompressor, Lz77Compressor, RleCompressor, TileCompressor,
274};
275pub use error::{
276    CanvasError, FetchError, JsInteropError, TileCacheError, WasmError, WasmResult, WorkerError,
277};
278pub use fetch::{
279    EnhancedFetchBackend, FetchBackend, FetchStats, PrioritizedRequest, RequestPriority,
280    RequestQueue, RetryConfig,
281};
282pub use profiler::{
283    Bottleneck, BottleneckDetector, CounterStats, FrameRateStats, FrameRateTracker, MemoryMonitor,
284    MemorySnapshot, MemoryStats, PerformanceCounter, Profiler, ProfilerSummary, WasmProfiler,
285};
286pub use rendering::{
287    AnimationManager, AnimationStats, CanvasBuffer, CanvasRenderer, ProgressiveRenderStats,
288    ProgressiveRenderer, RenderQuality, ViewportHistory, ViewportState, ViewportTransform,
289};
290pub use streaming::{
291    BandwidthEstimator, ImportanceCalculator, LoadStrategy, MultiResolutionStreamer,
292    PrefetchScheduler, ProgressiveLoader, QualityAdapter, StreamBuffer, StreamBufferStats,
293    StreamingQuality, StreamingStats, TileStreamer,
294};
295pub use tile::{
296    CacheStats, CachedTile, PrefetchStrategy, TileBounds, TileCache, TileCoord, TilePrefetcher,
297    TilePyramid, WasmTileCache,
298};
299pub use worker::{
300    JobId, JobStatus, PendingJob, PoolStats, WasmWorkerPool, WorkerInfo, WorkerJobRequest,
301    WorkerJobResponse, WorkerPool, WorkerRequestType, WorkerResponseType,
302};
303
304/// Initialize the WASM module with better error handling
305#[wasm_bindgen(start)]
306pub fn init() {
307    #[cfg(feature = "console_error_panic_hook")]
308    console_error_panic_hook::set_once();
309}
310
311/// WASM-compatible COG (Cloud Optimized GeoTIFF) viewer
312///
313/// This is the basic COG viewer for browser-based geospatial data visualization.
314/// It provides simple access to COG metadata and tile reading functionality.
315///
316/// # Features
317///
318/// - Efficient tile-based access to large GeoTIFF files
319/// - Support for multi-band imagery
320/// - Overview/pyramid level access for different zoom levels
321/// - CORS-compatible HTTP range request support
322/// - Automatic TIFF header parsing
323/// - GeoTIFF metadata extraction (CRS, geotransform, etc.)
324///
325/// # Performance
326///
327/// The viewer uses HTTP range requests to fetch only the required portions
328/// of the file, making it efficient for large files. However, for production
329/// use cases with caching and advanced features, consider using
330/// `AdvancedCogViewer` instead.
331///
332/// # Example
333///
334/// ```javascript
335/// const viewer = new WasmCogViewer();
336/// await viewer.open('<https://example.com/image.tif>');
337/// console.log(`Size: ${viewer.width()}x${viewer.height()}`);
338/// const tile = await viewer.read_tile_as_image_data(0, 0, 0);
339/// ```
340#[wasm_bindgen]
341pub struct WasmCogViewer {
342    /// URL of the opened COG file
343    url: Option<String>,
344    /// Image width in pixels
345    width: u64,
346    /// Image height in pixels
347    height: u64,
348    /// Tile width in pixels (typically 256 or 512)
349    tile_width: u32,
350    /// Tile height in pixels (typically 256 or 512)
351    tile_height: u32,
352    /// Number of bands/channels in the image
353    band_count: u32,
354    /// Number of overview/pyramid levels available
355    overview_count: usize,
356    /// EPSG code for the coordinate reference system (if available)
357    epsg_code: Option<u32>,
358    /// GeoTIFF geotransform data (for calculating geographic bounds)
359    pixel_scale_x: Option<f64>,
360    pixel_scale_y: Option<f64>,
361    tiepoint_pixel_x: Option<f64>,
362    tiepoint_pixel_y: Option<f64>,
363    tiepoint_geo_x: Option<f64>,
364    tiepoint_geo_y: Option<f64>,
365}
366
367#[wasm_bindgen]
368impl WasmCogViewer {
369    /// Creates a new COG viewer
370    #[wasm_bindgen(constructor)]
371    pub fn new() -> Self {
372        Self {
373            url: None,
374            width: 0,
375            height: 0,
376            tile_width: 256,
377            tile_height: 256,
378            band_count: 0,
379            overview_count: 0,
380            epsg_code: None,
381            pixel_scale_x: None,
382            pixel_scale_y: None,
383            tiepoint_pixel_x: None,
384            tiepoint_pixel_y: None,
385            tiepoint_geo_x: None,
386            tiepoint_geo_y: None,
387        }
388    }
389
390    /// Opens a COG file from a URL
391    ///
392    /// This method performs the following operations:
393    /// 1. Sends a HEAD request to determine file size and range support
394    /// 2. Fetches the TIFF header to validate format
395    /// 3. Parses IFD (Image File Directory) to extract metadata
396    /// 4. Extracts GeoTIFF tags for coordinate system information
397    /// 5. Counts overview levels for multi-resolution support
398    ///
399    /// # Arguments
400    ///
401    /// * `url` - The URL of the COG file to open. Must support HTTP range requests
402    ///           for optimal performance. CORS must be properly configured.
403    ///
404    /// # Returns
405    ///
406    /// Returns `Ok(())` on success, or a JavaScript error on failure.
407    ///
408    /// # Errors
409    ///
410    /// This method can fail for several reasons:
411    /// - Network errors (no connection, timeout, etc.)
412    /// - HTTP errors (404, 403, 500, etc.)
413    /// - CORS errors (missing headers)
414    /// - Invalid TIFF format
415    /// - Unsupported TIFF variant
416    ///
417    /// # Example
418    ///
419    /// ```javascript
420    /// const viewer = new WasmCogViewer();
421    /// try {
422    ///     await viewer.open('<https://example.com/landsat.tif>');
423    ///     console.log('Successfully opened COG');
424    /// } catch (error) {
425    ///     console.error('Failed to open:', error);
426    /// }
427    /// ```
428    #[wasm_bindgen]
429    pub async fn open(&mut self, url: &str) -> std::result::Result<(), JsValue> {
430        // Log the operation for debugging
431        console::log_1(&format!("Opening COG: {}", url).into());
432
433        // Use WASM-specific async COG reader
434        let reader = cog_reader::WasmCogReader::open(url.to_string())
435            .await
436            .map_err(|e| to_js_error(&e))?;
437
438        let metadata = reader.metadata();
439
440        self.url = Some(url.to_string());
441        self.width = metadata.width;
442        self.height = metadata.height;
443        self.tile_width = metadata.tile_width;
444        self.tile_height = metadata.tile_height;
445        self.band_count = u32::from(metadata.samples_per_pixel);
446        self.overview_count = metadata.overview_count;
447        self.epsg_code = metadata.epsg_code;
448
449        // Extract geotransform for bounds calculation
450        self.pixel_scale_x = metadata.pixel_scale_x;
451        self.pixel_scale_y = metadata.pixel_scale_y;
452        self.tiepoint_pixel_x = metadata.tiepoint_pixel_x;
453        self.tiepoint_pixel_y = metadata.tiepoint_pixel_y;
454        self.tiepoint_geo_x = metadata.tiepoint_geo_x;
455        self.tiepoint_geo_y = metadata.tiepoint_geo_y;
456
457        console::log_1(
458            &format!(
459                "Opened COG: {}x{}, {} bands, {} overviews",
460                self.width, self.height, self.band_count, self.overview_count
461            )
462            .into(),
463        );
464
465        Ok(())
466    }
467
468    /// Returns the image width
469    #[wasm_bindgen]
470    pub fn width(&self) -> u64 {
471        self.width
472    }
473
474    /// Returns the image height
475    #[wasm_bindgen]
476    pub fn height(&self) -> u64 {
477        self.height
478    }
479
480    /// Returns the tile width
481    #[wasm_bindgen]
482    pub fn tile_width(&self) -> u32 {
483        self.tile_width
484    }
485
486    /// Returns the tile height
487    #[wasm_bindgen]
488    pub fn tile_height(&self) -> u32 {
489        self.tile_height
490    }
491
492    /// Returns the number of bands
493    #[wasm_bindgen]
494    pub fn band_count(&self) -> u32 {
495        self.band_count
496    }
497
498    /// Returns the number of overview levels
499    #[wasm_bindgen]
500    pub fn overview_count(&self) -> usize {
501        self.overview_count
502    }
503
504    /// Returns the EPSG code if available
505    #[wasm_bindgen]
506    pub fn epsg_code(&self) -> Option<u32> {
507        self.epsg_code
508    }
509
510    /// Returns the URL
511    #[wasm_bindgen]
512    pub fn url(&self) -> Option<String> {
513        self.url.clone()
514    }
515
516    /// Returns metadata as JSON
517    #[wasm_bindgen]
518    pub fn metadata_json(&self) -> String {
519        serde_json::json!({
520            "url": self.url,
521            "width": self.width,
522            "height": self.height,
523            "tileWidth": self.tile_width,
524            "tileHeight": self.tile_height,
525            "bandCount": self.band_count,
526            "overviewCount": self.overview_count,
527            "epsgCode": self.epsg_code,
528            "geotransform": {
529                "pixelScaleX": self.pixel_scale_x,
530                "pixelScaleY": self.pixel_scale_y,
531                "tiepointPixelX": self.tiepoint_pixel_x,
532                "tiepointPixelY": self.tiepoint_pixel_y,
533                "tiepointGeoX": self.tiepoint_geo_x,
534                "tiepointGeoY": self.tiepoint_geo_y,
535            },
536        })
537        .to_string()
538    }
539
540    /// Returns pixel scale X (degrees/pixel in lon direction)
541    #[wasm_bindgen]
542    pub fn pixel_scale_x(&self) -> Option<f64> {
543        self.pixel_scale_x
544    }
545
546    /// Returns pixel scale Y (degrees/pixel in lat direction, negative)
547    #[wasm_bindgen]
548    pub fn pixel_scale_y(&self) -> Option<f64> {
549        self.pixel_scale_y
550    }
551
552    /// Returns tiepoint geo X (top-left longitude)
553    #[wasm_bindgen]
554    pub fn tiepoint_geo_x(&self) -> Option<f64> {
555        self.tiepoint_geo_x
556    }
557
558    /// Returns tiepoint geo Y (top-left latitude)
559    #[wasm_bindgen]
560    pub fn tiepoint_geo_y(&self) -> Option<f64> {
561        self.tiepoint_geo_y
562    }
563
564    /// Reads a tile and returns raw bytes
565    #[wasm_bindgen]
566    pub async fn read_tile(
567        &self,
568        _level: usize,
569        tile_x: u32,
570        tile_y: u32,
571    ) -> std::result::Result<Vec<u8>, JsValue> {
572        let url = self
573            .url
574            .as_ref()
575            .ok_or_else(|| JsValue::from_str("No file opened"))?;
576
577        // Create WASM-specific async COG reader
578        let reader = cog_reader::WasmCogReader::open(url.clone())
579            .await
580            .map_err(|e| to_js_error(&e))?;
581
582        // Read tile with async I/O
583        reader
584            .read_tile(tile_x, tile_y)
585            .await
586            .map_err(|e| to_js_error(&e))
587    }
588
589    /// Reads a tile and converts to RGBA ImageData for canvas rendering
590    #[wasm_bindgen]
591    pub async fn read_tile_as_image_data(
592        &self,
593        level: usize,
594        tile_x: u32,
595        tile_y: u32,
596    ) -> std::result::Result<ImageData, JsValue> {
597        let tile_data = self.read_tile(level, tile_x, tile_y).await?;
598
599        let pixel_count = (self.tile_width * self.tile_height) as usize;
600        let mut rgba = vec![0u8; pixel_count * 4];
601
602        // Convert to RGBA based on band count
603        match self.band_count {
604            1 => {
605                // Grayscale
606                for (i, &v) in tile_data.iter().take(pixel_count).enumerate() {
607                    rgba[i * 4] = v;
608                    rgba[i * 4 + 1] = v;
609                    rgba[i * 4 + 2] = v;
610                    rgba[i * 4 + 3] = 255;
611                }
612            }
613            3 => {
614                // RGB
615                for i in 0..pixel_count.min(tile_data.len() / 3) {
616                    rgba[i * 4] = tile_data[i * 3];
617                    rgba[i * 4 + 1] = tile_data[i * 3 + 1];
618                    rgba[i * 4 + 2] = tile_data[i * 3 + 2];
619                    rgba[i * 4 + 3] = 255;
620                }
621            }
622            4 => {
623                // RGBA
624                for i in 0..pixel_count.min(tile_data.len() / 4) {
625                    rgba[i * 4] = tile_data[i * 4];
626                    rgba[i * 4 + 1] = tile_data[i * 4 + 1];
627                    rgba[i * 4 + 2] = tile_data[i * 4 + 2];
628                    rgba[i * 4 + 3] = tile_data[i * 4 + 3];
629                }
630            }
631            _ => {
632                // Use first band as grayscale
633                for (i, &v) in tile_data.iter().take(pixel_count).enumerate() {
634                    rgba[i * 4] = v;
635                    rgba[i * 4 + 1] = v;
636                    rgba[i * 4 + 2] = v;
637                    rgba[i * 4 + 3] = 255;
638                }
639            }
640        }
641
642        let clamped = wasm_bindgen::Clamped(rgba.as_slice());
643        ImageData::new_with_u8_clamped_array_and_sh(clamped, self.tile_width, self.tile_height)
644    }
645}
646
647impl Default for WasmCogViewer {
648    fn default() -> Self {
649        Self::new()
650    }
651}
652
653/// Converts an `OxiGdalError` to a `JsValue`
654fn to_js_error(err: &OxiGdalError) -> JsValue {
655    JsValue::from_str(&err.to_string())
656}
657
658/// Version information
659#[wasm_bindgen]
660#[must_use]
661pub fn version() -> String {
662    env!("CARGO_PKG_VERSION").to_string()
663}
664
665/// Checks if the given URL points to a TIFF file by reading the header
666///
667/// # Errors
668/// Returns an error if the URL cannot be fetched or the header cannot be read
669#[wasm_bindgen]
670pub async fn is_tiff_url(url: &str) -> std::result::Result<bool, JsValue> {
671    let backend = FetchBackend::new(url.to_string())
672        .await
673        .map_err(|e| to_js_error(&e))?;
674    let header = backend
675        .read_range_async(ByteRange::from_offset_length(0, 8))
676        .await
677        .map_err(|e| to_js_error(&e))?;
678    Ok(oxigdal_geotiff::is_tiff(&header))
679}
680
681/// Viewport for managing the visible area of the image
682#[derive(Debug, Clone, Serialize, Deserialize)]
683pub struct Viewport {
684    /// Center X coordinate in image space
685    pub center_x: f64,
686    /// Center Y coordinate in image space
687    pub center_y: f64,
688    /// Zoom level (0 = most zoomed out)
689    pub zoom: u32,
690    /// Viewport width in pixels
691    pub width: u32,
692    /// Viewport height in pixels
693    pub height: u32,
694}
695
696impl Viewport {
697    /// Creates a new viewport
698    pub const fn new(center_x: f64, center_y: f64, zoom: u32, width: u32, height: u32) -> Self {
699        Self {
700            center_x,
701            center_y,
702            zoom,
703            width,
704            height,
705        }
706    }
707
708    /// Returns the visible bounds in image coordinates
709    pub const fn bounds(&self) -> (f64, f64, f64, f64) {
710        let half_width = (self.width as f64) / 2.0;
711        let half_height = (self.height as f64) / 2.0;
712
713        let min_x = self.center_x - half_width;
714        let min_y = self.center_y - half_height;
715        let max_x = self.center_x + half_width;
716        let max_y = self.center_y + half_height;
717
718        (min_x, min_y, max_x, max_y)
719    }
720
721    /// Pans the viewport by the given delta
722    pub fn pan(&mut self, dx: f64, dy: f64) {
723        self.center_x += dx;
724        self.center_y += dy;
725    }
726
727    /// Zooms in (increases zoom level)
728    pub fn zoom_in(&mut self) {
729        self.zoom = self.zoom.saturating_add(1);
730    }
731
732    /// Zooms out (decreases zoom level)
733    pub fn zoom_out(&mut self) {
734        self.zoom = self.zoom.saturating_sub(1);
735    }
736
737    /// Sets the zoom level
738    pub fn set_zoom(&mut self, zoom: u32) {
739        self.zoom = zoom;
740    }
741
742    /// Centers the viewport on a point
743    pub fn center_on(&mut self, x: f64, y: f64) {
744        self.center_x = x;
745        self.center_y = y;
746    }
747
748    /// Fits the viewport to the given image size
749    pub fn fit_to_image(&mut self, image_width: u64, image_height: u64) {
750        self.center_x = (image_width as f64) / 2.0;
751        self.center_y = (image_height as f64) / 2.0;
752
753        // Calculate zoom level to fit image
754        let x_scale = (image_width as f64) / (self.width as f64);
755        let y_scale = (image_height as f64) / (self.height as f64);
756        let scale = x_scale.max(y_scale);
757
758        self.zoom = scale.log2().ceil() as u32;
759    }
760}
761
762/// Advanced COG viewer with comprehensive tile management and caching
763///
764/// This is the recommended viewer for production applications. It provides
765/// advanced features including:
766///
767/// - **LRU Tile Caching**: Automatic memory management with configurable size
768/// - **Viewport Management**: Pan, zoom, and viewport history (undo/redo)
769/// - **Prefetching**: Intelligent prefetching of nearby tiles
770/// - **Multi-resolution**: Automatic selection of appropriate overview level
771/// - **Image Processing**: Built-in contrast enhancement and statistics
772/// - **Performance Tracking**: Cache hit rates and loading metrics
773///
774/// # Memory Management
775///
776/// The viewer uses an LRU (Least Recently Used) cache to manage memory
777/// efficiently. When the cache is full, the least recently accessed tiles
778/// are evicted. Configure the cache size based on your application's memory
779/// constraints and typical usage patterns.
780///
781/// Recommended cache sizes:
782/// - Mobile devices: 50-100 MB
783/// - Desktop browsers: 100-500 MB
784/// - High-end workstations: 500-1000 MB
785///
786/// # Prefetching Strategies
787///
788/// The viewer supports multiple prefetching strategies:
789///
790/// - **None**: No prefetching (lowest memory, highest latency)
791/// - **Neighbors**: Prefetch immediately adjacent tiles
792/// - **Pyramid**: Prefetch parent and child tiles (smooth zooming)
793///
794/// # Performance Optimization
795///
796/// For best performance:
797/// 1. Use an appropriate cache size (100-200 MB recommended)
798/// 2. Enable prefetching for smoother user experience
799/// 3. Use viewport management to minimize unnecessary tile loads
800/// 4. Monitor cache statistics to tune parameters
801///
802/// # Example
803///
804/// ```javascript
805/// const viewer = new AdvancedCogViewer();
806/// await viewer.open('<https://example.com/image.tif>', 100); // 100MB cache
807///
808/// // Setup viewport
809/// viewer.setViewportSize(800, 600);
810/// viewer.fitToImage();
811///
812/// // Enable prefetching
813/// viewer.setPrefetchStrategy('neighbors');
814///
815/// // Load and display tiles
816/// const imageData = await viewer.readTileAsImageData(0, 0, 0);
817/// ctx.putImageData(imageData, 0, 0);
818///
819/// // Check performance
820/// const stats = JSON.parse(viewer.getCacheStats());
821/// console.log(`Hit rate: ${stats.hit_count / (stats.hit_count + stats.miss_count)}`);
822/// ```
823#[wasm_bindgen]
824pub struct AdvancedCogViewer {
825    /// URL of the opened COG file
826    url: Option<String>,
827
828    /// Image metadata - width in pixels
829    width: u64,
830    /// Image metadata - height in pixels
831    height: u64,
832    /// Tile dimensions - width in pixels
833    tile_width: u32,
834    /// Tile dimensions - height in pixels
835    tile_height: u32,
836    /// Number of spectral bands in the image
837    band_count: u32,
838    /// Number of overview/pyramid levels
839    overview_count: usize,
840    /// EPSG code for coordinate reference system
841    epsg_code: Option<u32>,
842
843    /// Tile pyramid structure for multi-resolution access
844    pyramid: Option<TilePyramid>,
845    /// LRU tile cache for efficient memory management
846    cache: Option<TileCache>,
847    /// Current viewport state (pan, zoom, bounds)
848    viewport: Option<Viewport>,
849    /// Strategy for prefetching nearby tiles
850    prefetch_strategy: PrefetchStrategy,
851}
852
853#[wasm_bindgen]
854impl AdvancedCogViewer {
855    /// Creates a new advanced COG viewer
856    #[wasm_bindgen(constructor)]
857    pub fn new() -> Self {
858        Self {
859            url: None,
860            width: 0,
861            height: 0,
862            tile_width: 256,
863            tile_height: 256,
864            band_count: 0,
865            overview_count: 0,
866            epsg_code: None,
867            pyramid: None,
868            cache: None,
869            viewport: None,
870            prefetch_strategy: PrefetchStrategy::Neighbors,
871        }
872    }
873
874    /// Opens a COG file from a URL with advanced caching enabled
875    ///
876    /// This method initializes the viewer with full caching and viewport management.
877    /// It performs the following operations:
878    ///
879    /// 1. **Initial Connection**: Sends HEAD request to validate URL and check range support
880    /// 2. **Header Parsing**: Fetches and parses TIFF header (8-16 bytes)
881    /// 3. **Metadata Extraction**: Parses IFD to extract image dimensions, tile size, bands
882    /// 4. **GeoTIFF Tags**: Extracts coordinate system information (EPSG, geotransform)
883    /// 5. **Pyramid Creation**: Builds tile pyramid structure for all overview levels
884    /// 6. **Cache Initialization**: Creates LRU cache with specified size
885    /// 7. **Viewport Setup**: Initializes viewport with default settings
886    ///
887    /// # Arguments
888    ///
889    /// * `url` - The URL of the COG file. Must support HTTP range requests (Accept-Ranges: bytes)
890    ///           and have proper CORS headers configured.
891    /// * `cache_size_mb` - Size of the tile cache in megabytes. Recommended values:
892    ///   - Mobile: 50-100 MB
893    ///   - Desktop: 100-500 MB
894    ///   - High-end: 500-1000 MB
895    ///
896    /// # Returns
897    ///
898    /// Returns `Ok(())` on successful initialization, or a JavaScript error on failure.
899    ///
900    /// # Errors
901    ///
902    /// This method can fail for several reasons:
903    ///
904    /// ## Network Errors
905    /// - Connection timeout
906    /// - DNS resolution failure
907    /// - SSL/TLS errors
908    ///
909    /// ## HTTP Errors
910    /// - 404 Not Found: File doesn't exist at the URL
911    /// - 403 Forbidden: Access denied
912    /// - 500 Server Error: Server-side issues
913    ///
914    /// ## CORS Errors
915    /// - Missing Access-Control-Allow-Origin header
916    /// - Missing Access-Control-Allow-Headers for range requests
917    ///
918    /// ## Format Errors
919    /// - Invalid TIFF magic bytes
920    /// - Corrupted IFD structure
921    /// - Unsupported TIFF variant
922    /// - Missing required tags
923    ///
924    /// # Performance Considerations
925    ///
926    /// Opening a COG typically requires 2-4 HTTP requests:
927    /// 1. HEAD request (~10ms)
928    /// 2. Header fetch (~20ms for 16 bytes)
929    /// 3. IFD fetch (~50ms for typical IFD)
930    /// 4. GeoTIFF tags fetch (~30ms if separate)
931    ///
932    /// Total typical open time: 100-200ms on good connections.
933    ///
934    /// # Example
935    ///
936    /// ```javascript
937    /// const viewer = new AdvancedCogViewer();
938    ///
939    /// try {
940    ///     // Open with 100MB cache
941    ///     await viewer.open('<https://example.com/landsat8.tif>', 100);
942    ///
943    ///     console.log(`Opened: ${viewer.width()}x${viewer.height()}`);
944    ///     console.log(`Tiles: ${viewer.tile_width()}x${viewer.tile_height()}`);
945    ///     console.log(`Cache size: 100 MB`);
946    /// } catch (error) {
947    ///     if (error.message.includes('404')) {
948    ///         console.error('File not found');
949    ///     } else if (error.message.includes('CORS')) {
950    ///         console.error('CORS not configured. Add these headers:');
951    ///         console.error('  Access-Control-Allow-Origin: *');
952    ///         console.error('  Access-Control-Allow-Headers: Range');
953    ///     } else {
954    ///         console.error('Failed to open:', error.message);
955    ///     }
956    /// }
957    /// ```
958    ///
959    /// # See Also
960    ///
961    /// - `WasmCogViewer::open()` - Simple version without caching
962    /// - `set_prefetch_strategy()` - Configure prefetching after opening
963    /// - `get_cache_stats()` - Monitor cache performance
964    #[wasm_bindgen]
965    pub async fn open(&mut self, url: &str, cache_size_mb: usize) -> Result<(), JsValue> {
966        // Log operation for debugging and performance tracking
967        console::log_1(&format!("Opening COG with caching: {}", url).into());
968
969        let backend = FetchBackend::new(url.to_string())
970            .await
971            .map_err(|e| to_js_error(&e))?;
972
973        // Read header
974        let header_bytes = backend
975            .read_range_async(ByteRange::from_offset_length(0, 16))
976            .await
977            .map_err(|e| to_js_error(&e))?;
978
979        let header =
980            oxigdal_geotiff::TiffHeader::parse(&header_bytes).map_err(|e| to_js_error(&e))?;
981
982        // Parse the full file
983        let tiff = oxigdal_geotiff::TiffFile::parse(&backend).map_err(|e| to_js_error(&e))?;
984
985        // Get image info
986        let info = oxigdal_geotiff::ImageInfo::from_ifd(
987            tiff.primary_ifd(),
988            &backend,
989            header.byte_order,
990            header.variant,
991        )
992        .map_err(|e| to_js_error(&e))?;
993
994        self.url = Some(url.to_string());
995        self.width = info.width;
996        self.height = info.height;
997        self.tile_width = info.tile_width.unwrap_or(256);
998        self.tile_height = info.tile_height.unwrap_or(256);
999        self.band_count = u32::from(info.samples_per_pixel);
1000        self.overview_count = tiff.image_count().saturating_sub(1);
1001
1002        // Get EPSG code
1003        if let Ok(Some(geo_keys)) = oxigdal_geotiff::geokeys::GeoKeyDirectory::from_ifd(
1004            tiff.primary_ifd(),
1005            &backend,
1006            header.byte_order,
1007            header.variant,
1008        ) {
1009            self.epsg_code = geo_keys.epsg_code();
1010        }
1011
1012        // Create tile pyramid
1013        self.pyramid = Some(TilePyramid::new(
1014            self.width,
1015            self.height,
1016            self.tile_width,
1017            self.tile_height,
1018        ));
1019
1020        // Create tile cache
1021        let cache_size = cache_size_mb * 1024 * 1024;
1022        self.cache = Some(TileCache::new(cache_size));
1023
1024        // Create default viewport
1025        let mut viewport = Viewport::new(
1026            (self.width as f64) / 2.0,
1027            (self.height as f64) / 2.0,
1028            0,
1029            800,
1030            600,
1031        );
1032        viewport.fit_to_image(self.width, self.height);
1033        self.viewport = Some(viewport);
1034
1035        console::log_1(
1036            &format!(
1037                "Opened COG: {}x{}, {} bands, {} overviews, cache: {}MB",
1038                self.width, self.height, self.band_count, self.overview_count, cache_size_mb
1039            )
1040            .into(),
1041        );
1042
1043        Ok(())
1044    }
1045
1046    /// Returns the image width
1047    #[wasm_bindgen]
1048    pub fn width(&self) -> u64 {
1049        self.width
1050    }
1051
1052    /// Returns the image height
1053    #[wasm_bindgen]
1054    pub fn height(&self) -> u64 {
1055        self.height
1056    }
1057
1058    /// Returns the tile width
1059    #[wasm_bindgen]
1060    pub fn tile_width(&self) -> u32 {
1061        self.tile_width
1062    }
1063
1064    /// Returns the tile height
1065    #[wasm_bindgen]
1066    pub fn tile_height(&self) -> u32 {
1067        self.tile_height
1068    }
1069
1070    /// Returns the number of bands
1071    #[wasm_bindgen]
1072    pub fn band_count(&self) -> u32 {
1073        self.band_count
1074    }
1075
1076    /// Returns the number of overview levels
1077    #[wasm_bindgen]
1078    pub fn overview_count(&self) -> usize {
1079        self.overview_count
1080    }
1081
1082    /// Returns the EPSG code if available
1083    #[wasm_bindgen]
1084    pub fn epsg_code(&self) -> Option<u32> {
1085        self.epsg_code
1086    }
1087
1088    /// Returns the URL
1089    #[wasm_bindgen]
1090    pub fn url(&self) -> Option<String> {
1091        self.url.clone()
1092    }
1093
1094    /// Sets the viewport size
1095    #[wasm_bindgen(js_name = setViewportSize)]
1096    pub fn set_viewport_size(&mut self, width: u32, height: u32) {
1097        if let Some(ref mut viewport) = self.viewport {
1098            viewport.width = width;
1099            viewport.height = height;
1100        }
1101    }
1102
1103    /// Pans the viewport
1104    #[wasm_bindgen]
1105    pub fn pan(&mut self, dx: f64, dy: f64) {
1106        if let Some(ref mut viewport) = self.viewport {
1107            viewport.pan(dx, dy);
1108        }
1109    }
1110
1111    /// Zooms in
1112    #[wasm_bindgen(js_name = zoomIn)]
1113    pub fn zoom_in(&mut self) {
1114        if let Some(ref mut viewport) = self.viewport {
1115            viewport.zoom_in();
1116        }
1117    }
1118
1119    /// Zooms out
1120    #[wasm_bindgen(js_name = zoomOut)]
1121    pub fn zoom_out(&mut self) {
1122        if let Some(ref mut viewport) = self.viewport {
1123            viewport.zoom_out();
1124        }
1125    }
1126
1127    /// Sets the zoom level
1128    #[wasm_bindgen(js_name = setZoom)]
1129    pub fn set_zoom(&mut self, zoom: u32) {
1130        if let Some(ref mut viewport) = self.viewport {
1131            viewport.set_zoom(zoom);
1132        }
1133    }
1134
1135    /// Centers the viewport on a point
1136    #[wasm_bindgen(js_name = centerOn)]
1137    pub fn center_on(&mut self, x: f64, y: f64) {
1138        if let Some(ref mut viewport) = self.viewport {
1139            viewport.center_on(x, y);
1140        }
1141    }
1142
1143    /// Fits the viewport to the image
1144    #[wasm_bindgen(js_name = fitToImage)]
1145    pub fn fit_to_image(&mut self) {
1146        if let Some(ref mut viewport) = self.viewport {
1147            viewport.fit_to_image(self.width, self.height);
1148        }
1149    }
1150
1151    /// Returns the current viewport as JSON
1152    #[wasm_bindgen(js_name = getViewport)]
1153    pub fn get_viewport(&self) -> Option<String> {
1154        self.viewport
1155            .as_ref()
1156            .and_then(|v| serde_json::to_string(v).ok())
1157    }
1158
1159    /// Returns cache statistics as JSON
1160    #[wasm_bindgen(js_name = getCacheStats)]
1161    pub fn get_cache_stats(&self) -> Option<String> {
1162        self.cache
1163            .as_ref()
1164            .and_then(|c| serde_json::to_string(&c.stats()).ok())
1165    }
1166
1167    /// Clears the tile cache
1168    #[wasm_bindgen(js_name = clearCache)]
1169    pub fn clear_cache(&mut self) {
1170        if let Some(ref mut cache) = self.cache {
1171            cache.clear();
1172        }
1173    }
1174
1175    /// Sets the prefetch strategy
1176    #[wasm_bindgen(js_name = setPrefetchStrategy)]
1177    pub fn set_prefetch_strategy(&mut self, strategy: &str) {
1178        self.prefetch_strategy = match strategy {
1179            "none" => PrefetchStrategy::None,
1180            "neighbors" => PrefetchStrategy::Neighbors,
1181            "pyramid" => PrefetchStrategy::Pyramid,
1182            _ => PrefetchStrategy::Neighbors,
1183        };
1184    }
1185
1186    /// Returns comprehensive metadata as JSON
1187    #[wasm_bindgen(js_name = getMetadata)]
1188    pub fn get_metadata(&self) -> String {
1189        let pyramid_info = self.pyramid.as_ref().map(|p| {
1190            serde_json::json!({
1191                "numLevels": p.num_levels,
1192                "totalTiles": p.total_tiles(),
1193                "tilesPerLevel": p.tiles_per_level,
1194            })
1195        });
1196
1197        serde_json::json!({
1198            "url": self.url,
1199            "width": self.width,
1200            "height": self.height,
1201            "tileWidth": self.tile_width,
1202            "tileHeight": self.tile_height,
1203            "bandCount": self.band_count,
1204            "overviewCount": self.overview_count,
1205            "epsgCode": self.epsg_code,
1206            "pyramid": pyramid_info,
1207        })
1208        .to_string()
1209    }
1210
1211    /// Computes image statistics for a region
1212    #[wasm_bindgen(js_name = computeStats)]
1213    pub async fn compute_stats(
1214        &self,
1215        level: usize,
1216        tile_x: u32,
1217        tile_y: u32,
1218    ) -> Result<String, JsValue> {
1219        let tile_data = self.read_tile_internal(level, tile_x, tile_y).await?;
1220
1221        let pixel_count = (self.tile_width * self.tile_height) as usize;
1222        let mut rgba = vec![0u8; pixel_count * 4];
1223
1224        // Convert to RGBA
1225        self.convert_to_rgba(&tile_data, &mut rgba)?;
1226
1227        let stats = ImageStats::from_rgba(&rgba, self.tile_width, self.tile_height)
1228            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1229
1230        serde_json::to_string(&stats).map_err(|e| JsValue::from_str(&e.to_string()))
1231    }
1232
1233    /// Computes histogram for a region (tile)
1234    ///
1235    /// Returns a comprehensive JSON object containing:
1236    /// - Image dimensions (width, height, total_pixels)
1237    /// - Per-channel histograms (red, green, blue, luminance)
1238    /// - Statistics for each channel (min, max, mean, median, std_dev, count)
1239    /// - Histogram bins (256 bins for 8-bit values)
1240    ///
1241    /// # Arguments
1242    ///
1243    /// * `level` - Overview/pyramid level (0 = full resolution)
1244    /// * `tile_x` - Tile X coordinate
1245    /// * `tile_y` - Tile Y coordinate
1246    ///
1247    /// # Example
1248    ///
1249    /// ```javascript
1250    /// const viewer = new AdvancedCogViewer();
1251    /// await viewer.open('<https://example.com/image.tif>', 100);
1252    ///
1253    /// // Get histogram for tile at (0, 0) at full resolution
1254    /// const histogramJson = await viewer.computeHistogram(0, 0, 0);
1255    /// const histogram = JSON.parse(histogramJson);
1256    ///
1257    /// console.log(`Luminance mean: ${histogram.luminance.mean}`);
1258    /// console.log(`Luminance std_dev: ${histogram.luminance.std_dev}`);
1259    /// console.log(`Red min/max: ${histogram.red.min} - ${histogram.red.max}`);
1260    /// ```
1261    #[wasm_bindgen(js_name = computeHistogram)]
1262    pub async fn compute_histogram(
1263        &self,
1264        level: usize,
1265        tile_x: u32,
1266        tile_y: u32,
1267    ) -> Result<String, JsValue> {
1268        let tile_data = self.read_tile_internal(level, tile_x, tile_y).await?;
1269
1270        let pixel_count = (self.tile_width * self.tile_height) as usize;
1271        let mut rgba = vec![0u8; pixel_count * 4];
1272
1273        self.convert_to_rgba(&tile_data, &mut rgba)?;
1274
1275        let hist = Histogram::from_rgba(&rgba, self.tile_width, self.tile_height)
1276            .map_err(|e| JsValue::from_str(&e.to_string()))?;
1277
1278        hist.to_json_string(self.tile_width, self.tile_height)
1279            .map_err(|e| JsValue::from_str(&e.to_string()))
1280    }
1281
1282    /// Reads a tile with caching
1283    #[wasm_bindgen(js_name = readTileCached)]
1284    pub async fn read_tile_cached(
1285        &mut self,
1286        level: usize,
1287        tile_x: u32,
1288        tile_y: u32,
1289    ) -> Result<Vec<u8>, JsValue> {
1290        let coord = TileCoord::new(level as u32, tile_x, tile_y);
1291        let timestamp = js_sys::Date::now() / 1000.0;
1292
1293        // Check cache
1294        if let Some(ref mut cache) = self.cache {
1295            if let Some(data) = cache.get(&coord, timestamp) {
1296                return Ok(data);
1297            }
1298        }
1299
1300        // Cache miss - fetch tile
1301        let data = self.read_tile_internal(level, tile_x, tile_y).await?;
1302
1303        // Store in cache
1304        if let Some(ref mut cache) = self.cache {
1305            let _ = cache.put(coord, data.clone(), timestamp);
1306        }
1307
1308        Ok(data)
1309    }
1310
1311    /// Internal tile reading
1312    async fn read_tile_internal(
1313        &self,
1314        level: usize,
1315        tile_x: u32,
1316        tile_y: u32,
1317    ) -> Result<Vec<u8>, JsValue> {
1318        let url = self
1319            .url
1320            .as_ref()
1321            .ok_or_else(|| JsValue::from_str("No file opened"))?;
1322
1323        let backend = FetchBackend::new(url.clone())
1324            .await
1325            .map_err(|e| to_js_error(&e))?;
1326
1327        let reader = oxigdal_geotiff::CogReader::open(backend).map_err(|e| to_js_error(&e))?;
1328        reader
1329            .read_tile(level, tile_x, tile_y)
1330            .map_err(|e| to_js_error(&e))
1331    }
1332
1333    /// Converts tile data to RGBA
1334    fn convert_to_rgba(&self, tile_data: &[u8], rgba: &mut [u8]) -> Result<(), JsValue> {
1335        let pixel_count = (self.tile_width * self.tile_height) as usize;
1336
1337        match self.band_count {
1338            1 => {
1339                // Grayscale
1340                for (i, &v) in tile_data.iter().take(pixel_count).enumerate() {
1341                    rgba[i * 4] = v;
1342                    rgba[i * 4 + 1] = v;
1343                    rgba[i * 4 + 2] = v;
1344                    rgba[i * 4 + 3] = 255;
1345                }
1346            }
1347            3 => {
1348                // RGB
1349                for i in 0..pixel_count.min(tile_data.len() / 3) {
1350                    rgba[i * 4] = tile_data[i * 3];
1351                    rgba[i * 4 + 1] = tile_data[i * 3 + 1];
1352                    rgba[i * 4 + 2] = tile_data[i * 3 + 2];
1353                    rgba[i * 4 + 3] = 255;
1354                }
1355            }
1356            4 => {
1357                // RGBA
1358                for i in 0..pixel_count.min(tile_data.len() / 4) {
1359                    rgba[i * 4] = tile_data[i * 4];
1360                    rgba[i * 4 + 1] = tile_data[i * 4 + 1];
1361                    rgba[i * 4 + 2] = tile_data[i * 4 + 2];
1362                    rgba[i * 4 + 3] = tile_data[i * 4 + 3];
1363                }
1364            }
1365            _ => {
1366                // Use first band as grayscale
1367                for (i, &v) in tile_data.iter().take(pixel_count).enumerate() {
1368                    rgba[i * 4] = v;
1369                    rgba[i * 4 + 1] = v;
1370                    rgba[i * 4 + 2] = v;
1371                    rgba[i * 4 + 3] = 255;
1372                }
1373            }
1374        }
1375
1376        Ok(())
1377    }
1378
1379    /// Reads a tile as ImageData with caching
1380    #[wasm_bindgen(js_name = readTileAsImageData)]
1381    pub async fn read_tile_as_image_data(
1382        &mut self,
1383        level: usize,
1384        tile_x: u32,
1385        tile_y: u32,
1386    ) -> Result<ImageData, JsValue> {
1387        let tile_data = self.read_tile_cached(level, tile_x, tile_y).await?;
1388
1389        let pixel_count = (self.tile_width * self.tile_height) as usize;
1390        let mut rgba = vec![0u8; pixel_count * 4];
1391
1392        self.convert_to_rgba(&tile_data, &mut rgba)?;
1393
1394        let clamped = wasm_bindgen::Clamped(rgba.as_slice());
1395        ImageData::new_with_u8_clamped_array_and_sh(clamped, self.tile_width, self.tile_height)
1396    }
1397
1398    /// Applies contrast enhancement to a tile
1399    #[wasm_bindgen(js_name = readTileWithContrast)]
1400    pub async fn read_tile_with_contrast(
1401        &mut self,
1402        level: usize,
1403        tile_x: u32,
1404        tile_y: u32,
1405        method: &str,
1406    ) -> Result<ImageData, JsValue> {
1407        let tile_data = self.read_tile_cached(level, tile_x, tile_y).await?;
1408
1409        let pixel_count = (self.tile_width * self.tile_height) as usize;
1410        let mut rgba = vec![0u8; pixel_count * 4];
1411
1412        self.convert_to_rgba(&tile_data, &mut rgba)?;
1413
1414        // Apply contrast enhancement
1415        use crate::canvas::ContrastMethod;
1416        let contrast_method = match method {
1417            "linear" => ContrastMethod::LinearStretch,
1418            "histogram" => ContrastMethod::HistogramEqualization,
1419            "adaptive" => ContrastMethod::AdaptiveHistogramEqualization,
1420            _ => ContrastMethod::LinearStretch,
1421        };
1422
1423        ImageProcessor::enhance_contrast(
1424            &mut rgba,
1425            self.tile_width,
1426            self.tile_height,
1427            contrast_method,
1428        )
1429        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1430
1431        let clamped = wasm_bindgen::Clamped(rgba.as_slice());
1432        ImageData::new_with_u8_clamped_array_and_sh(clamped, self.tile_width, self.tile_height)
1433    }
1434}
1435
1436impl Default for AdvancedCogViewer {
1437    fn default() -> Self {
1438        Self::new()
1439    }
1440}
1441
1442/// Batch tile loader for efficient multi-tile loading
1443#[wasm_bindgen]
1444pub struct BatchTileLoader {
1445    viewer: AdvancedCogViewer,
1446    max_parallel: usize,
1447}
1448
1449#[wasm_bindgen]
1450impl BatchTileLoader {
1451    /// Creates a new batch tile loader
1452    #[wasm_bindgen(constructor)]
1453    pub fn new(max_parallel: usize) -> Self {
1454        Self {
1455            viewer: AdvancedCogViewer::new(),
1456            max_parallel,
1457        }
1458    }
1459
1460    /// Opens a COG
1461    #[wasm_bindgen]
1462    pub async fn open(&mut self, url: &str, cache_size_mb: usize) -> Result<(), JsValue> {
1463        self.viewer.open(url, cache_size_mb).await
1464    }
1465
1466    /// Loads multiple tiles in parallel
1467    #[wasm_bindgen(js_name = loadTilesBatch)]
1468    pub async fn load_tiles_batch(
1469        &mut self,
1470        level: usize,
1471        tile_coords: Vec<u32>, // Flattened [x1, y1, x2, y2, ...]
1472    ) -> Result<Vec<JsValue>, JsValue> {
1473        let mut results = Vec::new();
1474
1475        for chunk in tile_coords.chunks_exact(2).take(self.max_parallel) {
1476            let tile_x = chunk[0];
1477            let tile_y = chunk[1];
1478
1479            match self
1480                .viewer
1481                .read_tile_as_image_data(level, tile_x, tile_y)
1482                .await
1483            {
1484                Ok(image_data) => results.push(image_data.into()),
1485                Err(e) => results.push(e),
1486            }
1487        }
1488
1489        Ok(results)
1490    }
1491}
1492
1493/// GeoJSON export utilities
1494#[wasm_bindgen]
1495pub struct GeoJsonExporter;
1496
1497#[wasm_bindgen]
1498impl GeoJsonExporter {
1499    /// Exports image bounds as GeoJSON
1500    #[wasm_bindgen(js_name = exportBounds)]
1501    pub fn export_bounds(
1502        west: f64,
1503        south: f64,
1504        east: f64,
1505        north: f64,
1506        epsg: Option<u32>,
1507    ) -> String {
1508        serde_json::json!({
1509            "type": "Feature",
1510            "geometry": {
1511                "type": "Polygon",
1512                "coordinates": [[
1513                    [west, south],
1514                    [east, south],
1515                    [east, north],
1516                    [west, north],
1517                    [west, south]
1518                ]]
1519            },
1520            "properties": {
1521                "epsg": epsg
1522            }
1523        })
1524        .to_string()
1525    }
1526
1527    /// Exports a point as GeoJSON
1528    #[wasm_bindgen(js_name = exportPoint)]
1529    pub fn export_point(x: f64, y: f64, properties: &str) -> String {
1530        let props: serde_json::Value =
1531            serde_json::from_str(properties).unwrap_or(serde_json::json!({}));
1532
1533        serde_json::json!({
1534            "type": "Feature",
1535            "geometry": {
1536                "type": "Point",
1537                "coordinates": [x, y]
1538            },
1539            "properties": props
1540        })
1541        .to_string()
1542    }
1543}