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}