1use crate::camera_projection::CameraProjection;
24use crate::layer::{Layer, LayerId, LayerKind};
25use rustial_math::GeoCoord;
26use std::any::Any;
27use std::sync::Arc;
28
29#[derive(Debug, Clone)]
38pub struct ImageOverlayData {
39 pub layer_id: LayerId,
41 pub corners: [[f64; 3]; 4],
43 pub width: u32,
45 pub height: u32,
47 pub data: Arc<Vec<u8>>,
49 pub opacity: f32,
51}
52
53#[derive(Clone)]
68pub struct ImageOverlayLayer {
69 id: LayerId,
70 name: String,
71 visible: bool,
72 opacity: f32,
73 coordinates: [GeoCoord; 4],
75 width: u32,
77 height: u32,
79 data: Arc<Vec<u8>>,
81 generation: u64,
83}
84
85impl std::fmt::Debug for ImageOverlayLayer {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.debug_struct("ImageOverlayLayer")
88 .field("id", &self.id)
89 .field("name", &self.name)
90 .field("visible", &self.visible)
91 .field("opacity", &self.opacity)
92 .field("width", &self.width)
93 .field("height", &self.height)
94 .field("data_len", &self.data.len())
95 .finish()
96 }
97}
98
99impl ImageOverlayLayer {
100 pub fn new(
105 name: impl Into<String>,
106 coordinates: [GeoCoord; 4],
107 width: u32,
108 height: u32,
109 data: Vec<u8>,
110 ) -> Self {
111 debug_assert_eq!(
112 data.len(),
113 (width * height * 4) as usize,
114 "RGBA8 data length must equal width * height * 4"
115 );
116 Self {
117 id: LayerId::next(),
118 name: name.into(),
119 visible: true,
120 opacity: 1.0,
121 coordinates,
122 width,
123 height,
124 data: Arc::new(data),
125 generation: 0,
126 }
127 }
128
129 #[inline]
131 pub fn coordinates(&self) -> &[GeoCoord; 4] {
132 &self.coordinates
133 }
134
135 pub fn set_coordinates(&mut self, coordinates: [GeoCoord; 4]) {
137 self.coordinates = coordinates;
138 self.generation = self.generation.wrapping_add(1);
139 }
140
141 pub fn update_image(&mut self, width: u32, height: u32, data: Vec<u8>) {
145 debug_assert_eq!(
146 data.len(),
147 (width * height * 4) as usize,
148 "RGBA8 data length must equal width * height * 4"
149 );
150 self.width = width;
151 self.height = height;
152 self.data = Arc::new(data);
153 self.generation = self.generation.wrapping_add(1);
154 }
155
156 #[inline]
158 pub fn generation(&self) -> u64 {
159 self.generation
160 }
161
162 #[inline]
164 pub fn dimensions(&self) -> (u32, u32) {
165 (self.width, self.height)
166 }
167
168 pub fn to_overlay_data(&self, projection: CameraProjection) -> ImageOverlayData {
171 let corners = [
172 project_corner(&self.coordinates[0], projection),
173 project_corner(&self.coordinates[1], projection),
174 project_corner(&self.coordinates[2], projection),
175 project_corner(&self.coordinates[3], projection),
176 ];
177 ImageOverlayData {
178 layer_id: self.id,
179 corners,
180 width: self.width,
181 height: self.height,
182 data: Arc::clone(&self.data),
183 opacity: self.opacity,
184 }
185 }
186}
187
188fn project_corner(coord: &GeoCoord, projection: CameraProjection) -> [f64; 3] {
189 let w = projection.project(coord);
190 [w.position.x, w.position.y, w.position.z]
191}
192
193impl Layer for ImageOverlayLayer {
198 fn id(&self) -> LayerId {
199 self.id
200 }
201
202 fn name(&self) -> &str {
203 &self.name
204 }
205
206 fn kind(&self) -> LayerKind {
207 LayerKind::Custom
208 }
209
210 fn visible(&self) -> bool {
211 self.visible
212 }
213
214 fn set_visible(&mut self, visible: bool) {
215 self.visible = visible;
216 }
217
218 fn opacity(&self) -> f32 {
219 self.opacity
220 }
221
222 fn set_opacity(&mut self, opacity: f32) {
223 self.opacity = opacity.clamp(0.0, 1.0);
224 }
225
226 fn as_any(&self) -> &dyn Any {
227 self
228 }
229
230 fn as_any_mut(&mut self) -> &mut dyn Any {
231 self
232 }
233}
234
235#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn sample_corners() -> [GeoCoord; 4] {
244 [
245 GeoCoord::from_lat_lon(40.0, -74.0),
246 GeoCoord::from_lat_lon(40.0, -73.0),
247 GeoCoord::from_lat_lon(39.0, -73.0),
248 GeoCoord::from_lat_lon(39.0, -74.0),
249 ]
250 }
251
252 fn sample_rgba(w: u32, h: u32) -> Vec<u8> {
253 vec![128u8; (w * h * 4) as usize]
254 }
255
256 #[test]
257 fn new_layer_has_correct_dimensions() {
258 let layer = ImageOverlayLayer::new("test", sample_corners(), 64, 64, sample_rgba(64, 64));
259 assert_eq!(layer.dimensions(), (64, 64));
260 assert_eq!(layer.data.len(), 64 * 64 * 4);
261 assert!(layer.visible());
262 assert_eq!(layer.opacity(), 1.0);
263 }
264
265 #[test]
266 fn set_coordinates_bumps_generation() {
267 let mut layer = ImageOverlayLayer::new("test", sample_corners(), 4, 4, sample_rgba(4, 4));
268 let g0 = layer.generation();
269 layer.set_coordinates([
270 GeoCoord::from_lat_lon(50.0, -75.0),
271 GeoCoord::from_lat_lon(50.0, -74.0),
272 GeoCoord::from_lat_lon(49.0, -74.0),
273 GeoCoord::from_lat_lon(49.0, -75.0),
274 ]);
275 assert_eq!(layer.generation(), g0 + 1);
276 }
277
278 #[test]
279 fn update_image_bumps_generation() {
280 let mut layer = ImageOverlayLayer::new("test", sample_corners(), 4, 4, sample_rgba(4, 4));
281 let g0 = layer.generation();
282 layer.update_image(8, 8, sample_rgba(8, 8));
283 assert_eq!(layer.generation(), g0 + 1);
284 assert_eq!(layer.dimensions(), (8, 8));
285 }
286
287 #[test]
288 fn to_overlay_data_produces_world_space_corners() {
289 let layer = ImageOverlayLayer::new("test", sample_corners(), 4, 4, sample_rgba(4, 4));
290 let data = layer.to_overlay_data(CameraProjection::WebMercator);
291 for i in 0..4 {
293 for j in (i + 1)..4 {
294 let dx = (data.corners[i][0] - data.corners[j][0]).abs();
295 let dy = (data.corners[i][1] - data.corners[j][1]).abs();
296 assert!(
297 dx > 1.0 || dy > 1.0,
298 "corners {i} and {j} are too close: {dx}, {dy}"
299 );
300 }
301 }
302 }
303
304 #[test]
305 fn overlay_data_shares_arc_with_layer() {
306 let layer = ImageOverlayLayer::new("test", sample_corners(), 4, 4, sample_rgba(4, 4));
307 let data = layer.to_overlay_data(CameraProjection::WebMercator);
308 assert!(Arc::ptr_eq(&layer.data, &data.data));
309 }
310
311 #[test]
312 fn opacity_clamps_to_valid_range() {
313 let mut layer = ImageOverlayLayer::new("test", sample_corners(), 4, 4, sample_rgba(4, 4));
314 layer.set_opacity(2.0);
315 assert_eq!(layer.opacity(), 1.0);
316 layer.set_opacity(-1.0);
317 assert_eq!(layer.opacity(), 0.0);
318 }
319
320 #[test]
321 fn equirectangular_projection_produces_different_coordinates() {
322 let layer = ImageOverlayLayer::new("test", sample_corners(), 4, 4, sample_rgba(4, 4));
323 let merc = layer.to_overlay_data(CameraProjection::WebMercator);
324 let eq = layer.to_overlay_data(CameraProjection::Equirectangular);
325 let differs = merc
327 .corners
328 .iter()
329 .zip(eq.corners.iter())
330 .any(|(a, b)| (a[0] - b[0]).abs() > 0.01 || (a[1] - b[1]).abs() > 0.01);
331 assert!(differs, "WebMercator and Equirectangular should produce different corner positions");
332 }
333}