use crate::camera_projection::CameraProjection;
use crate::layer::{Layer, LayerId, LayerKind};
use rustial_math::GeoCoord;
use std::any::Any;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ImageOverlayData {
pub layer_id: LayerId,
pub corners: [[f64; 3]; 4],
pub width: u32,
pub height: u32,
pub data: Arc<Vec<u8>>,
pub opacity: f32,
}
#[derive(Clone)]
pub struct ImageOverlayLayer {
id: LayerId,
name: String,
visible: bool,
opacity: f32,
coordinates: [GeoCoord; 4],
width: u32,
height: u32,
data: Arc<Vec<u8>>,
generation: u64,
}
impl std::fmt::Debug for ImageOverlayLayer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ImageOverlayLayer")
.field("id", &self.id)
.field("name", &self.name)
.field("visible", &self.visible)
.field("opacity", &self.opacity)
.field("width", &self.width)
.field("height", &self.height)
.field("data_len", &self.data.len())
.finish()
}
}
impl ImageOverlayLayer {
pub fn new(
name: impl Into<String>,
coordinates: [GeoCoord; 4],
width: u32,
height: u32,
data: Vec<u8>,
) -> Self {
debug_assert_eq!(
data.len(),
(width * height * 4) as usize,
"RGBA8 data length must equal width * height * 4"
);
Self {
id: LayerId::next(),
name: name.into(),
visible: true,
opacity: 1.0,
coordinates,
width,
height,
data: Arc::new(data),
generation: 0,
}
}
#[inline]
pub fn coordinates(&self) -> &[GeoCoord; 4] {
&self.coordinates
}
pub fn set_coordinates(&mut self, coordinates: [GeoCoord; 4]) {
self.coordinates = coordinates;
self.generation = self.generation.wrapping_add(1);
}
pub fn update_image(&mut self, width: u32, height: u32, data: Vec<u8>) {
debug_assert_eq!(
data.len(),
(width * height * 4) as usize,
"RGBA8 data length must equal width * height * 4"
);
self.width = width;
self.height = height;
self.data = Arc::new(data);
self.generation = self.generation.wrapping_add(1);
}
#[inline]
pub fn generation(&self) -> u64 {
self.generation
}
#[inline]
pub fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
pub fn to_overlay_data(&self, projection: CameraProjection) -> ImageOverlayData {
let corners = [
project_corner(&self.coordinates[0], projection),
project_corner(&self.coordinates[1], projection),
project_corner(&self.coordinates[2], projection),
project_corner(&self.coordinates[3], projection),
];
ImageOverlayData {
layer_id: self.id,
corners,
width: self.width,
height: self.height,
data: Arc::clone(&self.data),
opacity: self.opacity,
}
}
}
fn project_corner(coord: &GeoCoord, projection: CameraProjection) -> [f64; 3] {
let w = projection.project(coord);
[w.position.x, w.position.y, w.position.z]
}
impl Layer for ImageOverlayLayer {
fn id(&self) -> LayerId {
self.id
}
fn name(&self) -> &str {
&self.name
}
fn kind(&self) -> LayerKind {
LayerKind::Custom
}
fn visible(&self) -> bool {
self.visible
}
fn set_visible(&mut self, visible: bool) {
self.visible = visible;
}
fn opacity(&self) -> f32 {
self.opacity
}
fn set_opacity(&mut self, opacity: f32) {
self.opacity = opacity.clamp(0.0, 1.0);
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_corners() -> [GeoCoord; 4] {
[
GeoCoord::from_lat_lon(40.0, -74.0),
GeoCoord::from_lat_lon(40.0, -73.0),
GeoCoord::from_lat_lon(39.0, -73.0),
GeoCoord::from_lat_lon(39.0, -74.0),
]
}
fn sample_rgba(w: u32, h: u32) -> Vec<u8> {
vec![128u8; (w * h * 4) as usize]
}
#[test]
fn new_layer_has_correct_dimensions() {
let layer = ImageOverlayLayer::new("test", sample_corners(), 64, 64, sample_rgba(64, 64));
assert_eq!(layer.dimensions(), (64, 64));
assert_eq!(layer.data.len(), 64 * 64 * 4);
assert!(layer.visible());
assert_eq!(layer.opacity(), 1.0);
}
#[test]
fn set_coordinates_bumps_generation() {
let mut layer = ImageOverlayLayer::new("test", sample_corners(), 4, 4, sample_rgba(4, 4));
let g0 = layer.generation();
layer.set_coordinates([
GeoCoord::from_lat_lon(50.0, -75.0),
GeoCoord::from_lat_lon(50.0, -74.0),
GeoCoord::from_lat_lon(49.0, -74.0),
GeoCoord::from_lat_lon(49.0, -75.0),
]);
assert_eq!(layer.generation(), g0 + 1);
}
#[test]
fn update_image_bumps_generation() {
let mut layer = ImageOverlayLayer::new("test", sample_corners(), 4, 4, sample_rgba(4, 4));
let g0 = layer.generation();
layer.update_image(8, 8, sample_rgba(8, 8));
assert_eq!(layer.generation(), g0 + 1);
assert_eq!(layer.dimensions(), (8, 8));
}
#[test]
fn to_overlay_data_produces_world_space_corners() {
let layer = ImageOverlayLayer::new("test", sample_corners(), 4, 4, sample_rgba(4, 4));
let data = layer.to_overlay_data(CameraProjection::WebMercator);
for i in 0..4 {
for j in (i + 1)..4 {
let dx = (data.corners[i][0] - data.corners[j][0]).abs();
let dy = (data.corners[i][1] - data.corners[j][1]).abs();
assert!(
dx > 1.0 || dy > 1.0,
"corners {i} and {j} are too close: {dx}, {dy}"
);
}
}
}
#[test]
fn overlay_data_shares_arc_with_layer() {
let layer = ImageOverlayLayer::new("test", sample_corners(), 4, 4, sample_rgba(4, 4));
let data = layer.to_overlay_data(CameraProjection::WebMercator);
assert!(Arc::ptr_eq(&layer.data, &data.data));
}
#[test]
fn opacity_clamps_to_valid_range() {
let mut layer = ImageOverlayLayer::new("test", sample_corners(), 4, 4, sample_rgba(4, 4));
layer.set_opacity(2.0);
assert_eq!(layer.opacity(), 1.0);
layer.set_opacity(-1.0);
assert_eq!(layer.opacity(), 0.0);
}
#[test]
fn equirectangular_projection_produces_different_coordinates() {
let layer = ImageOverlayLayer::new("test", sample_corners(), 4, 4, sample_rgba(4, 4));
let merc = layer.to_overlay_data(CameraProjection::WebMercator);
let eq = layer.to_overlay_data(CameraProjection::Equirectangular);
let differs = merc
.corners
.iter()
.zip(eq.corners.iter())
.any(|(a, b)| (a[0] - b[0]).abs() > 0.01 || (a[1] - b[1]).abs() > 0.01);
assert!(
differs,
"WebMercator and Equirectangular should produce different corner positions"
);
}
}