#![warn(missing_docs)]
#![warn(clippy::all)]
#![deny(clippy::unwrap_used)]
#![allow(clippy::needless_range_loop)]
#![allow(clippy::expect_used)]
#![allow(clippy::should_implement_trait)]
#![allow(clippy::new_without_default)]
#![allow(clippy::ptr_arg)]
#![allow(clippy::type_complexity)]
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use web_sys::{ImageData, console};
use oxigdal_core::error::OxiGdalError;
use oxigdal_core::io::ByteRange;
mod animation;
mod bindings;
mod canvas;
mod cog_reader;
mod color;
mod compression;
mod error;
mod fetch;
mod profiler;
mod rendering;
mod streaming;
#[cfg(test)]
mod tests;
mod tile;
mod worker;
pub mod component;
pub mod wasm_memory;
pub use animation::{
Animation, Easing, EasingFunction, PanAnimation, SpringAnimation, ZoomAnimation,
};
pub use bindings::{
DocGenerator, TsClass, TsFunction, TsInterface, TsModule, TsParameter, TsType, TsTypeAlias,
create_oxigdal_wasm_docs,
};
pub use canvas::{
ChannelHistogramJson, ContrastMethod, CustomBinHistogramJson, Histogram, HistogramJson, Hsv,
ImageProcessor, ImageStats, ResampleMethod, Resampler, Rgb, WasmImageProcessor, YCbCr,
};
pub use color::{
ChannelOps, ColorCorrectionMatrix, ColorPalette, ColorQuantizer, ColorTemperature,
GradientGenerator, PaletteEntry, WasmColorPalette, WhiteBalance,
};
pub use compression::{
CompressionAlgorithm, CompressionBenchmark, CompressionSelector, CompressionStats,
DeltaCompressor, HuffmanCompressor, Lz77Compressor, RleCompressor, TileCompressor,
};
pub use error::{
CanvasError, FetchError, JsInteropError, TileCacheError, WasmError, WasmResult, WorkerError,
};
pub use fetch::{
EnhancedFetchBackend, FetchBackend, FetchStats, PrioritizedRequest, RequestPriority,
RequestQueue, RetryConfig,
};
pub use profiler::{
Bottleneck, BottleneckDetector, CounterStats, FrameRateStats, FrameRateTracker, MemoryMonitor,
MemorySnapshot, MemoryStats, PerformanceCounter, Profiler, ProfilerSummary, WasmProfiler,
};
pub use rendering::{
AnimationManager, AnimationStats, CanvasBuffer, CanvasRenderer, ProgressiveRenderStats,
ProgressiveRenderer, RenderQuality, ViewportHistory, ViewportState, ViewportTransform,
};
pub use streaming::{
BandwidthEstimator, ImportanceCalculator, LoadStrategy, MultiResolutionStreamer,
PrefetchScheduler, ProgressiveLoader, QualityAdapter, StreamBuffer, StreamBufferStats,
StreamingQuality, StreamingStats, TileStreamer,
};
pub use tile::{
CacheStats, CachedTile, PrefetchStrategy, TileBounds, TileCache, TileCoord, TilePrefetcher,
TilePyramid, WasmTileCache,
};
pub use worker::{
JobId, JobStatus, PendingJob, PoolStats, WasmWorkerPool, WorkerInfo, WorkerJobRequest,
WorkerJobResponse, WorkerPool, WorkerRequestType, WorkerResponseType,
};
#[wasm_bindgen(start)]
pub fn init() {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}
#[wasm_bindgen]
pub struct WasmCogViewer {
url: Option<String>,
width: u64,
height: u64,
tile_width: u32,
tile_height: u32,
band_count: u32,
overview_count: usize,
epsg_code: Option<u32>,
pixel_scale_x: Option<f64>,
pixel_scale_y: Option<f64>,
tiepoint_pixel_x: Option<f64>,
tiepoint_pixel_y: Option<f64>,
tiepoint_geo_x: Option<f64>,
tiepoint_geo_y: Option<f64>,
}
#[wasm_bindgen]
impl WasmCogViewer {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
url: None,
width: 0,
height: 0,
tile_width: 256,
tile_height: 256,
band_count: 0,
overview_count: 0,
epsg_code: None,
pixel_scale_x: None,
pixel_scale_y: None,
tiepoint_pixel_x: None,
tiepoint_pixel_y: None,
tiepoint_geo_x: None,
tiepoint_geo_y: None,
}
}
#[wasm_bindgen]
pub async fn open(&mut self, url: &str) -> std::result::Result<(), JsValue> {
console::log_1(&format!("Opening COG: {}", url).into());
let reader = cog_reader::WasmCogReader::open(url.to_string())
.await
.map_err(|e| to_js_error(&e))?;
let metadata = reader.metadata();
self.url = Some(url.to_string());
self.width = metadata.width;
self.height = metadata.height;
self.tile_width = metadata.tile_width;
self.tile_height = metadata.tile_height;
self.band_count = u32::from(metadata.samples_per_pixel);
self.overview_count = metadata.overview_count;
self.epsg_code = metadata.epsg_code;
self.pixel_scale_x = metadata.pixel_scale_x;
self.pixel_scale_y = metadata.pixel_scale_y;
self.tiepoint_pixel_x = metadata.tiepoint_pixel_x;
self.tiepoint_pixel_y = metadata.tiepoint_pixel_y;
self.tiepoint_geo_x = metadata.tiepoint_geo_x;
self.tiepoint_geo_y = metadata.tiepoint_geo_y;
console::log_1(
&format!(
"Opened COG: {}x{}, {} bands, {} overviews",
self.width, self.height, self.band_count, self.overview_count
)
.into(),
);
Ok(())
}
#[wasm_bindgen]
pub fn width(&self) -> u64 {
self.width
}
#[wasm_bindgen]
pub fn height(&self) -> u64 {
self.height
}
#[wasm_bindgen]
pub fn tile_width(&self) -> u32 {
self.tile_width
}
#[wasm_bindgen]
pub fn tile_height(&self) -> u32 {
self.tile_height
}
#[wasm_bindgen]
pub fn band_count(&self) -> u32 {
self.band_count
}
#[wasm_bindgen]
pub fn overview_count(&self) -> usize {
self.overview_count
}
#[wasm_bindgen]
pub fn epsg_code(&self) -> Option<u32> {
self.epsg_code
}
#[wasm_bindgen]
pub fn url(&self) -> Option<String> {
self.url.clone()
}
#[wasm_bindgen]
pub fn metadata_json(&self) -> String {
serde_json::json!({
"url": self.url,
"width": self.width,
"height": self.height,
"tileWidth": self.tile_width,
"tileHeight": self.tile_height,
"bandCount": self.band_count,
"overviewCount": self.overview_count,
"epsgCode": self.epsg_code,
"geotransform": {
"pixelScaleX": self.pixel_scale_x,
"pixelScaleY": self.pixel_scale_y,
"tiepointPixelX": self.tiepoint_pixel_x,
"tiepointPixelY": self.tiepoint_pixel_y,
"tiepointGeoX": self.tiepoint_geo_x,
"tiepointGeoY": self.tiepoint_geo_y,
},
})
.to_string()
}
#[wasm_bindgen]
pub fn pixel_scale_x(&self) -> Option<f64> {
self.pixel_scale_x
}
#[wasm_bindgen]
pub fn pixel_scale_y(&self) -> Option<f64> {
self.pixel_scale_y
}
#[wasm_bindgen]
pub fn tiepoint_geo_x(&self) -> Option<f64> {
self.tiepoint_geo_x
}
#[wasm_bindgen]
pub fn tiepoint_geo_y(&self) -> Option<f64> {
self.tiepoint_geo_y
}
#[wasm_bindgen]
pub async fn read_tile(
&self,
_level: usize,
tile_x: u32,
tile_y: u32,
) -> std::result::Result<Vec<u8>, JsValue> {
let url = self
.url
.as_ref()
.ok_or_else(|| JsValue::from_str("No file opened"))?;
let reader = cog_reader::WasmCogReader::open(url.clone())
.await
.map_err(|e| to_js_error(&e))?;
reader
.read_tile(tile_x, tile_y)
.await
.map_err(|e| to_js_error(&e))
}
#[wasm_bindgen]
pub async fn read_tile_as_image_data(
&self,
level: usize,
tile_x: u32,
tile_y: u32,
) -> std::result::Result<ImageData, JsValue> {
let tile_data = self.read_tile(level, tile_x, tile_y).await?;
let pixel_count = (self.tile_width * self.tile_height) as usize;
let mut rgba = vec![0u8; pixel_count * 4];
match self.band_count {
1 => {
for (i, &v) in tile_data.iter().take(pixel_count).enumerate() {
rgba[i * 4] = v;
rgba[i * 4 + 1] = v;
rgba[i * 4 + 2] = v;
rgba[i * 4 + 3] = 255;
}
}
3 => {
for i in 0..pixel_count.min(tile_data.len() / 3) {
rgba[i * 4] = tile_data[i * 3];
rgba[i * 4 + 1] = tile_data[i * 3 + 1];
rgba[i * 4 + 2] = tile_data[i * 3 + 2];
rgba[i * 4 + 3] = 255;
}
}
4 => {
for i in 0..pixel_count.min(tile_data.len() / 4) {
rgba[i * 4] = tile_data[i * 4];
rgba[i * 4 + 1] = tile_data[i * 4 + 1];
rgba[i * 4 + 2] = tile_data[i * 4 + 2];
rgba[i * 4 + 3] = tile_data[i * 4 + 3];
}
}
_ => {
for (i, &v) in tile_data.iter().take(pixel_count).enumerate() {
rgba[i * 4] = v;
rgba[i * 4 + 1] = v;
rgba[i * 4 + 2] = v;
rgba[i * 4 + 3] = 255;
}
}
}
let clamped = wasm_bindgen::Clamped(rgba.as_slice());
ImageData::new_with_u8_clamped_array_and_sh(clamped, self.tile_width, self.tile_height)
}
}
impl Default for WasmCogViewer {
fn default() -> Self {
Self::new()
}
}
fn to_js_error(err: &OxiGdalError) -> JsValue {
JsValue::from_str(&err.to_string())
}
#[wasm_bindgen]
#[must_use]
pub fn version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[wasm_bindgen]
pub async fn is_tiff_url(url: &str) -> std::result::Result<bool, JsValue> {
let backend = FetchBackend::new(url.to_string())
.await
.map_err(|e| to_js_error(&e))?;
let header = backend
.read_range_async(ByteRange::from_offset_length(0, 8))
.await
.map_err(|e| to_js_error(&e))?;
Ok(oxigdal_geotiff::is_tiff(&header))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Viewport {
pub center_x: f64,
pub center_y: f64,
pub zoom: u32,
pub width: u32,
pub height: u32,
}
impl Viewport {
pub const fn new(center_x: f64, center_y: f64, zoom: u32, width: u32, height: u32) -> Self {
Self {
center_x,
center_y,
zoom,
width,
height,
}
}
pub const fn bounds(&self) -> (f64, f64, f64, f64) {
let half_width = (self.width as f64) / 2.0;
let half_height = (self.height as f64) / 2.0;
let min_x = self.center_x - half_width;
let min_y = self.center_y - half_height;
let max_x = self.center_x + half_width;
let max_y = self.center_y + half_height;
(min_x, min_y, max_x, max_y)
}
pub fn pan(&mut self, dx: f64, dy: f64) {
self.center_x += dx;
self.center_y += dy;
}
pub fn zoom_in(&mut self) {
self.zoom = self.zoom.saturating_add(1);
}
pub fn zoom_out(&mut self) {
self.zoom = self.zoom.saturating_sub(1);
}
pub fn set_zoom(&mut self, zoom: u32) {
self.zoom = zoom;
}
pub fn center_on(&mut self, x: f64, y: f64) {
self.center_x = x;
self.center_y = y;
}
pub fn fit_to_image(&mut self, image_width: u64, image_height: u64) {
self.center_x = (image_width as f64) / 2.0;
self.center_y = (image_height as f64) / 2.0;
let x_scale = (image_width as f64) / (self.width as f64);
let y_scale = (image_height as f64) / (self.height as f64);
let scale = x_scale.max(y_scale);
self.zoom = scale.log2().ceil() as u32;
}
}
#[wasm_bindgen]
pub struct AdvancedCogViewer {
url: Option<String>,
width: u64,
height: u64,
tile_width: u32,
tile_height: u32,
band_count: u32,
overview_count: usize,
epsg_code: Option<u32>,
pyramid: Option<TilePyramid>,
cache: Option<TileCache>,
viewport: Option<Viewport>,
prefetch_strategy: PrefetchStrategy,
}
#[wasm_bindgen]
impl AdvancedCogViewer {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
url: None,
width: 0,
height: 0,
tile_width: 256,
tile_height: 256,
band_count: 0,
overview_count: 0,
epsg_code: None,
pyramid: None,
cache: None,
viewport: None,
prefetch_strategy: PrefetchStrategy::Neighbors,
}
}
#[wasm_bindgen]
pub async fn open(&mut self, url: &str, cache_size_mb: usize) -> Result<(), JsValue> {
console::log_1(&format!("Opening COG with caching: {}", url).into());
let backend = FetchBackend::new(url.to_string())
.await
.map_err(|e| to_js_error(&e))?;
let header_bytes = backend
.read_range_async(ByteRange::from_offset_length(0, 16))
.await
.map_err(|e| to_js_error(&e))?;
let header =
oxigdal_geotiff::TiffHeader::parse(&header_bytes).map_err(|e| to_js_error(&e))?;
let tiff = oxigdal_geotiff::TiffFile::parse(&backend).map_err(|e| to_js_error(&e))?;
let info = oxigdal_geotiff::ImageInfo::from_ifd(
tiff.primary_ifd(),
&backend,
header.byte_order,
header.variant,
)
.map_err(|e| to_js_error(&e))?;
self.url = Some(url.to_string());
self.width = info.width;
self.height = info.height;
self.tile_width = info.tile_width.unwrap_or(256);
self.tile_height = info.tile_height.unwrap_or(256);
self.band_count = u32::from(info.samples_per_pixel);
self.overview_count = tiff.image_count().saturating_sub(1);
if let Ok(Some(geo_keys)) = oxigdal_geotiff::geokeys::GeoKeyDirectory::from_ifd(
tiff.primary_ifd(),
&backend,
header.byte_order,
header.variant,
) {
self.epsg_code = geo_keys.epsg_code();
}
self.pyramid = Some(TilePyramid::new(
self.width,
self.height,
self.tile_width,
self.tile_height,
));
let cache_size = cache_size_mb * 1024 * 1024;
self.cache = Some(TileCache::new(cache_size));
let mut viewport = Viewport::new(
(self.width as f64) / 2.0,
(self.height as f64) / 2.0,
0,
800,
600,
);
viewport.fit_to_image(self.width, self.height);
self.viewport = Some(viewport);
console::log_1(
&format!(
"Opened COG: {}x{}, {} bands, {} overviews, cache: {}MB",
self.width, self.height, self.band_count, self.overview_count, cache_size_mb
)
.into(),
);
Ok(())
}
#[wasm_bindgen]
pub fn width(&self) -> u64 {
self.width
}
#[wasm_bindgen]
pub fn height(&self) -> u64 {
self.height
}
#[wasm_bindgen]
pub fn tile_width(&self) -> u32 {
self.tile_width
}
#[wasm_bindgen]
pub fn tile_height(&self) -> u32 {
self.tile_height
}
#[wasm_bindgen]
pub fn band_count(&self) -> u32 {
self.band_count
}
#[wasm_bindgen]
pub fn overview_count(&self) -> usize {
self.overview_count
}
#[wasm_bindgen]
pub fn epsg_code(&self) -> Option<u32> {
self.epsg_code
}
#[wasm_bindgen]
pub fn url(&self) -> Option<String> {
self.url.clone()
}
#[wasm_bindgen(js_name = setViewportSize)]
pub fn set_viewport_size(&mut self, width: u32, height: u32) {
if let Some(ref mut viewport) = self.viewport {
viewport.width = width;
viewport.height = height;
}
}
#[wasm_bindgen]
pub fn pan(&mut self, dx: f64, dy: f64) {
if let Some(ref mut viewport) = self.viewport {
viewport.pan(dx, dy);
}
}
#[wasm_bindgen(js_name = zoomIn)]
pub fn zoom_in(&mut self) {
if let Some(ref mut viewport) = self.viewport {
viewport.zoom_in();
}
}
#[wasm_bindgen(js_name = zoomOut)]
pub fn zoom_out(&mut self) {
if let Some(ref mut viewport) = self.viewport {
viewport.zoom_out();
}
}
#[wasm_bindgen(js_name = setZoom)]
pub fn set_zoom(&mut self, zoom: u32) {
if let Some(ref mut viewport) = self.viewport {
viewport.set_zoom(zoom);
}
}
#[wasm_bindgen(js_name = centerOn)]
pub fn center_on(&mut self, x: f64, y: f64) {
if let Some(ref mut viewport) = self.viewport {
viewport.center_on(x, y);
}
}
#[wasm_bindgen(js_name = fitToImage)]
pub fn fit_to_image(&mut self) {
if let Some(ref mut viewport) = self.viewport {
viewport.fit_to_image(self.width, self.height);
}
}
#[wasm_bindgen(js_name = getViewport)]
pub fn get_viewport(&self) -> Option<String> {
self.viewport
.as_ref()
.and_then(|v| serde_json::to_string(v).ok())
}
#[wasm_bindgen(js_name = getCacheStats)]
pub fn get_cache_stats(&self) -> Option<String> {
self.cache
.as_ref()
.and_then(|c| serde_json::to_string(&c.stats()).ok())
}
#[wasm_bindgen(js_name = clearCache)]
pub fn clear_cache(&mut self) {
if let Some(ref mut cache) = self.cache {
cache.clear();
}
}
#[wasm_bindgen(js_name = setPrefetchStrategy)]
pub fn set_prefetch_strategy(&mut self, strategy: &str) {
self.prefetch_strategy = match strategy {
"none" => PrefetchStrategy::None,
"neighbors" => PrefetchStrategy::Neighbors,
"pyramid" => PrefetchStrategy::Pyramid,
_ => PrefetchStrategy::Neighbors,
};
}
#[wasm_bindgen(js_name = getMetadata)]
pub fn get_metadata(&self) -> String {
let pyramid_info = self.pyramid.as_ref().map(|p| {
serde_json::json!({
"numLevels": p.num_levels,
"totalTiles": p.total_tiles(),
"tilesPerLevel": p.tiles_per_level,
})
});
serde_json::json!({
"url": self.url,
"width": self.width,
"height": self.height,
"tileWidth": self.tile_width,
"tileHeight": self.tile_height,
"bandCount": self.band_count,
"overviewCount": self.overview_count,
"epsgCode": self.epsg_code,
"pyramid": pyramid_info,
})
.to_string()
}
#[wasm_bindgen(js_name = computeStats)]
pub async fn compute_stats(
&self,
level: usize,
tile_x: u32,
tile_y: u32,
) -> Result<String, JsValue> {
let tile_data = self.read_tile_internal(level, tile_x, tile_y).await?;
let pixel_count = (self.tile_width * self.tile_height) as usize;
let mut rgba = vec![0u8; pixel_count * 4];
self.convert_to_rgba(&tile_data, &mut rgba)?;
let stats = ImageStats::from_rgba(&rgba, self.tile_width, self.tile_height)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_json::to_string(&stats).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = computeHistogram)]
pub async fn compute_histogram(
&self,
level: usize,
tile_x: u32,
tile_y: u32,
) -> Result<String, JsValue> {
let tile_data = self.read_tile_internal(level, tile_x, tile_y).await?;
let pixel_count = (self.tile_width * self.tile_height) as usize;
let mut rgba = vec![0u8; pixel_count * 4];
self.convert_to_rgba(&tile_data, &mut rgba)?;
let hist = Histogram::from_rgba(&rgba, self.tile_width, self.tile_height)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
hist.to_json_string(self.tile_width, self.tile_height)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = readTileCached)]
pub async fn read_tile_cached(
&mut self,
level: usize,
tile_x: u32,
tile_y: u32,
) -> Result<Vec<u8>, JsValue> {
let coord = TileCoord::new(level as u32, tile_x, tile_y);
let timestamp = js_sys::Date::now() / 1000.0;
if let Some(ref mut cache) = self.cache {
if let Some(data) = cache.get(&coord, timestamp) {
return Ok(data);
}
}
let data = self.read_tile_internal(level, tile_x, tile_y).await?;
if let Some(ref mut cache) = self.cache {
let _ = cache.put(coord, data.clone(), timestamp);
}
Ok(data)
}
async fn read_tile_internal(
&self,
level: usize,
tile_x: u32,
tile_y: u32,
) -> Result<Vec<u8>, JsValue> {
let url = self
.url
.as_ref()
.ok_or_else(|| JsValue::from_str("No file opened"))?;
let backend = FetchBackend::new(url.clone())
.await
.map_err(|e| to_js_error(&e))?;
let reader = oxigdal_geotiff::CogReader::open(backend).map_err(|e| to_js_error(&e))?;
reader
.read_tile(level, tile_x, tile_y)
.map_err(|e| to_js_error(&e))
}
fn convert_to_rgba(&self, tile_data: &[u8], rgba: &mut [u8]) -> Result<(), JsValue> {
let pixel_count = (self.tile_width * self.tile_height) as usize;
match self.band_count {
1 => {
for (i, &v) in tile_data.iter().take(pixel_count).enumerate() {
rgba[i * 4] = v;
rgba[i * 4 + 1] = v;
rgba[i * 4 + 2] = v;
rgba[i * 4 + 3] = 255;
}
}
3 => {
for i in 0..pixel_count.min(tile_data.len() / 3) {
rgba[i * 4] = tile_data[i * 3];
rgba[i * 4 + 1] = tile_data[i * 3 + 1];
rgba[i * 4 + 2] = tile_data[i * 3 + 2];
rgba[i * 4 + 3] = 255;
}
}
4 => {
for i in 0..pixel_count.min(tile_data.len() / 4) {
rgba[i * 4] = tile_data[i * 4];
rgba[i * 4 + 1] = tile_data[i * 4 + 1];
rgba[i * 4 + 2] = tile_data[i * 4 + 2];
rgba[i * 4 + 3] = tile_data[i * 4 + 3];
}
}
_ => {
for (i, &v) in tile_data.iter().take(pixel_count).enumerate() {
rgba[i * 4] = v;
rgba[i * 4 + 1] = v;
rgba[i * 4 + 2] = v;
rgba[i * 4 + 3] = 255;
}
}
}
Ok(())
}
#[wasm_bindgen(js_name = readTileAsImageData)]
pub async fn read_tile_as_image_data(
&mut self,
level: usize,
tile_x: u32,
tile_y: u32,
) -> Result<ImageData, JsValue> {
let tile_data = self.read_tile_cached(level, tile_x, tile_y).await?;
let pixel_count = (self.tile_width * self.tile_height) as usize;
let mut rgba = vec![0u8; pixel_count * 4];
self.convert_to_rgba(&tile_data, &mut rgba)?;
let clamped = wasm_bindgen::Clamped(rgba.as_slice());
ImageData::new_with_u8_clamped_array_and_sh(clamped, self.tile_width, self.tile_height)
}
#[wasm_bindgen(js_name = readTileWithContrast)]
pub async fn read_tile_with_contrast(
&mut self,
level: usize,
tile_x: u32,
tile_y: u32,
method: &str,
) -> Result<ImageData, JsValue> {
let tile_data = self.read_tile_cached(level, tile_x, tile_y).await?;
let pixel_count = (self.tile_width * self.tile_height) as usize;
let mut rgba = vec![0u8; pixel_count * 4];
self.convert_to_rgba(&tile_data, &mut rgba)?;
use crate::canvas::ContrastMethod;
let contrast_method = match method {
"linear" => ContrastMethod::LinearStretch,
"histogram" => ContrastMethod::HistogramEqualization,
"adaptive" => ContrastMethod::AdaptiveHistogramEqualization,
_ => ContrastMethod::LinearStretch,
};
ImageProcessor::enhance_contrast(
&mut rgba,
self.tile_width,
self.tile_height,
contrast_method,
)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let clamped = wasm_bindgen::Clamped(rgba.as_slice());
ImageData::new_with_u8_clamped_array_and_sh(clamped, self.tile_width, self.tile_height)
}
}
impl Default for AdvancedCogViewer {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
pub struct BatchTileLoader {
viewer: AdvancedCogViewer,
max_parallel: usize,
}
#[wasm_bindgen]
impl BatchTileLoader {
#[wasm_bindgen(constructor)]
pub fn new(max_parallel: usize) -> Self {
Self {
viewer: AdvancedCogViewer::new(),
max_parallel,
}
}
#[wasm_bindgen]
pub async fn open(&mut self, url: &str, cache_size_mb: usize) -> Result<(), JsValue> {
self.viewer.open(url, cache_size_mb).await
}
#[wasm_bindgen(js_name = loadTilesBatch)]
pub async fn load_tiles_batch(
&mut self,
level: usize,
tile_coords: Vec<u32>, ) -> Result<Vec<JsValue>, JsValue> {
let mut results = Vec::new();
for chunk in tile_coords.chunks_exact(2).take(self.max_parallel) {
let tile_x = chunk[0];
let tile_y = chunk[1];
match self
.viewer
.read_tile_as_image_data(level, tile_x, tile_y)
.await
{
Ok(image_data) => results.push(image_data.into()),
Err(e) => results.push(e),
}
}
Ok(results)
}
}
#[wasm_bindgen]
pub struct GeoJsonExporter;
#[wasm_bindgen]
impl GeoJsonExporter {
#[wasm_bindgen(js_name = exportBounds)]
pub fn export_bounds(
west: f64,
south: f64,
east: f64,
north: f64,
epsg: Option<u32>,
) -> String {
serde_json::json!({
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[
[west, south],
[east, south],
[east, north],
[west, north],
[west, south]
]]
},
"properties": {
"epsg": epsg
}
})
.to_string()
}
#[wasm_bindgen(js_name = exportPoint)]
pub fn export_point(x: f64, y: f64, properties: &str) -> String {
let props: serde_json::Value =
serde_json::from_str(properties).unwrap_or(serde_json::json!({}));
serde_json::json!({
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [x, y]
},
"properties": props
})
.to_string()
}
}