pub mod cache;
pub mod tiles;
use crate::ffi::types::*;
use std::sync::atomic::{AtomicUsize, Ordering};
static TOTAL_BYTES_READ: AtomicUsize = AtomicUsize::new(0);
static TOTAL_TILES_CACHED: AtomicUsize = AtomicUsize::new(0);
static CACHE_HITS: AtomicUsize = AtomicUsize::new(0);
static CACHE_MISSES: AtomicUsize = AtomicUsize::new(0);
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct MobileStats {
pub total_bytes_read: usize,
pub tiles_cached: usize,
pub cache_hits: usize,
pub cache_misses: usize,
pub cache_hit_ratio: f64,
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oxigdal_mobile_get_stats(out_stats: *mut MobileStats) -> OxiGdalErrorCode {
if out_stats.is_null() {
crate::ffi::error::set_last_error("Null pointer for out_stats".to_string());
return OxiGdalErrorCode::NullPointer;
}
let hits = CACHE_HITS.load(Ordering::Relaxed);
let misses = CACHE_MISSES.load(Ordering::Relaxed);
let total = hits + misses;
let hit_ratio = if total > 0 {
hits as f64 / total as f64
} else {
0.0
};
let stats = MobileStats {
total_bytes_read: TOTAL_BYTES_READ.load(Ordering::Relaxed),
tiles_cached: TOTAL_TILES_CACHED.load(Ordering::Relaxed),
cache_hits: hits,
cache_misses: misses,
cache_hit_ratio: hit_ratio,
};
unsafe {
*out_stats = stats;
}
OxiGdalErrorCode::Success
}
#[unsafe(no_mangle)]
pub extern "C" fn oxigdal_mobile_reset_stats() -> OxiGdalErrorCode {
TOTAL_BYTES_READ.store(0, Ordering::Relaxed);
TOTAL_TILES_CACHED.store(0, Ordering::Relaxed);
CACHE_HITS.store(0, Ordering::Relaxed);
CACHE_MISSES.store(0, Ordering::Relaxed);
OxiGdalErrorCode::Success
}
pub(crate) fn record_bytes_read(bytes: usize) {
TOTAL_BYTES_READ.fetch_add(bytes, Ordering::Relaxed);
}
pub(crate) fn record_cache_hit() {
CACHE_HITS.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_cache_miss() {
CACHE_MISSES.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn set_tiles_cached(count: usize) {
TOTAL_TILES_CACHED.store(count, Ordering::Relaxed);
}
#[unsafe(no_mangle)]
pub extern "C" fn oxigdal_mobile_optimize_memory(
available_mb: std::os::raw::c_int,
) -> OxiGdalErrorCode {
if available_mb <= 0 {
crate::ffi::error::set_last_error("Invalid available memory".to_string());
return OxiGdalErrorCode::InvalidArgument;
}
let cache_size_mb = if available_mb < 100 {
10
} else if available_mb < 300 {
50
} else if available_mb < 500 {
100
} else {
200
};
if let Err(e) = cache::set_max_cache_size_mb(cache_size_mb) {
crate::ffi::error::set_last_error(e.to_string());
return OxiGdalErrorCode::AllocationFailed;
}
OxiGdalErrorCode::Success
}
#[unsafe(no_mangle)]
pub extern "C" fn oxigdal_mobile_set_offline_mode(
enabled: std::os::raw::c_int,
) -> OxiGdalErrorCode {
tiles::set_offline_mode(enabled != 0);
OxiGdalErrorCode::Success
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oxigdal_mobile_prefetch_tiles(
_dataset: *const OxiGdalDataset,
bbox: *const OxiGdalBbox,
min_zoom: std::os::raw::c_int,
max_zoom: std::os::raw::c_int,
) -> std::os::raw::c_int {
if bbox.is_null() {
crate::ffi::error::set_last_error("Null pointer for bbox".to_string());
return -1;
}
if min_zoom < 0 || max_zoom < min_zoom || max_zoom > 22 {
crate::ffi::error::set_last_error("Invalid zoom levels".to_string());
return -1;
}
let bbox_ref = unsafe { &*bbox };
if bbox_ref.min_x > bbox_ref.max_x || bbox_ref.min_y > bbox_ref.max_y {
crate::ffi::error::set_last_error("Invalid bounding box: min > max".to_string());
return -1;
}
if bbox_ref.min_x < -180.0
|| bbox_ref.max_x > 180.0
|| bbox_ref.min_y < -85.051129
|| bbox_ref.max_y > 85.051129
{
crate::ffi::error::set_last_error(
"Bounding box extends beyond valid Web Mercator range".to_string(),
);
return -1;
}
let mut total_tile_count: i64 = 0;
for zoom in min_zoom..=max_zoom {
let zoom_tiles = tiles::tiles_for_bbox(bbox_ref, zoom);
total_tile_count += zoom_tiles.len() as i64;
}
const MAX_PREFETCH_TILES: i64 = 10_000;
if total_tile_count > MAX_PREFETCH_TILES {
crate::ffi::error::set_last_error(format!(
"Too many tiles to prefetch: {} (max: {}). \
Reduce the bounding box or zoom range.",
total_tile_count, MAX_PREFETCH_TILES
));
return -1;
}
let mut prefetched_count: i32 = 0;
for zoom in min_zoom..=max_zoom {
let zoom_tiles = tiles::tiles_for_bbox(bbox_ref, zoom);
for (tile_x, tile_y, tile_z) in &zoom_tiles {
let cache_key = format!("tile_{}_{}_{}", tile_z, tile_x, tile_y);
if cache::get_cached_tile(&cache_key).is_some() {
prefetched_count += 1;
continue;
}
if tiles::is_offline_mode() {
continue;
}
let (min_lon, min_lat, max_lon, max_lat) =
tiles::tile_to_bbox(*tile_x, *tile_y, *tile_z);
let tile_size = tiles::TILE_SIZE;
let channels = 3; let tile_data_size = (tile_size * tile_size * channels) as usize;
let tile_data = vec![0u8; tile_data_size];
cache::put_cached_tile(cache_key, tile_data, tile_size, tile_size, channels);
prefetched_count += 1;
record_bytes_read(tile_data_size);
}
}
prefetched_count
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stats() {
oxigdal_mobile_reset_stats();
record_bytes_read(1024);
record_cache_hit();
record_cache_miss();
set_tiles_cached(10);
let mut stats = MobileStats {
total_bytes_read: 0,
tiles_cached: 0,
cache_hits: 0,
cache_misses: 0,
cache_hit_ratio: 0.0,
};
let result = unsafe { oxigdal_mobile_get_stats(&mut stats) };
assert_eq!(result, OxiGdalErrorCode::Success);
assert_eq!(stats.total_bytes_read, 1024);
assert_eq!(stats.tiles_cached, 10);
assert_eq!(stats.cache_hits, 1);
assert_eq!(stats.cache_misses, 1);
assert!((stats.cache_hit_ratio - 0.5).abs() < 0.01);
}
#[test]
fn test_memory_optimization() {
let result = oxigdal_mobile_optimize_memory(100);
assert_eq!(result, OxiGdalErrorCode::Success);
let result = oxigdal_mobile_optimize_memory(500);
assert_eq!(result, OxiGdalErrorCode::Success);
let result = oxigdal_mobile_optimize_memory(-1);
assert_eq!(result, OxiGdalErrorCode::InvalidArgument);
}
#[test]
fn test_offline_mode() {
let result = oxigdal_mobile_set_offline_mode(1);
assert_eq!(result, OxiGdalErrorCode::Success);
let result = oxigdal_mobile_set_offline_mode(0);
assert_eq!(result, OxiGdalErrorCode::Success);
}
#[test]
fn test_prefetch_tiles_invalid_bbox() {
let result =
unsafe { oxigdal_mobile_prefetch_tiles(std::ptr::null(), std::ptr::null(), 0, 5) };
assert_eq!(result, -1);
}
#[test]
fn test_prefetch_tiles_invalid_zoom() {
let bbox = OxiGdalBbox {
min_x: -1.0,
min_y: -1.0,
max_x: 1.0,
max_y: 1.0,
};
let result = unsafe { oxigdal_mobile_prefetch_tiles(std::ptr::null(), &bbox, -1, 5) };
assert_eq!(result, -1);
let result = unsafe { oxigdal_mobile_prefetch_tiles(std::ptr::null(), &bbox, 5, 2) };
assert_eq!(result, -1);
}
#[test]
fn test_prefetch_tiles_small_bbox() {
let _ = cache::init_cache(50);
oxigdal_mobile_reset_stats();
let bbox = OxiGdalBbox {
min_x: -0.5,
min_y: -0.5,
max_x: 0.5,
max_y: 0.5,
};
let _ = oxigdal_mobile_set_offline_mode(0);
let result = unsafe { oxigdal_mobile_prefetch_tiles(std::ptr::null(), &bbox, 0, 2) };
assert!(result >= 0);
}
#[test]
fn test_prefetch_tiles_invalid_geo_bbox() {
let bbox = OxiGdalBbox {
min_x: 10.0,
min_y: 5.0,
max_x: 5.0, max_y: 10.0,
};
let result = unsafe { oxigdal_mobile_prefetch_tiles(std::ptr::null(), &bbox, 0, 2) };
assert_eq!(result, -1);
}
#[test]
fn test_prefetch_tiles_offline_mode() {
let _ = cache::init_cache(50);
oxigdal_mobile_reset_stats();
let bbox = OxiGdalBbox {
min_x: -0.5,
min_y: -0.5,
max_x: 0.5,
max_y: 0.5,
};
let _ = oxigdal_mobile_set_offline_mode(1);
let result = unsafe { oxigdal_mobile_prefetch_tiles(std::ptr::null(), &bbox, 0, 1) };
assert_eq!(result, 0);
let _ = oxigdal_mobile_set_offline_mode(0);
}
}