Skip to main content

rustial_engine/layers/
dynamic_image_overlay_layer.rs

1//! Dynamic image overlay layer — a georeferenced overlay with
2//! frame-provider–driven content updates.
3//!
4//! This is the Rustial equivalent of MapLibre / Mapbox `video` and
5//! `canvas` source types.  Rather than wrapping a DOM `<video>` or
6//! `<canvas>` element, the layer holds a [`FrameProvider`] trait object
7//! that supplies new RGBA8 frames on demand.
8//!
9//! ## Data flow
10//!
11//! ```text
12//! FrameProvider::next_frame()
13//!     |
14//!     v
15//! DynamicImageOverlayLayer   (holds latest frame + generation)
16//!     |
17//!     v
18//! collect_image_overlays()   (polls provider, emits ImageOverlayData)
19//!     |
20//!     v
21//! FrameOutput::image_overlays   (renderer consumes as textured quads)
22//! ```
23//!
24//! ## Usage
25//!
26//! ```rust,ignore
27//! use rustial_engine::{DynamicImageOverlayLayer, FrameProvider, FrameData, GeoCoord};
28//!
29//! struct TestProvider;
30//! impl FrameProvider for TestProvider {
31//!     fn next_frame(&mut self) -> Option<FrameData> {
32//!         Some(FrameData {
33//!             width: 64,
34//!             height: 64,
35//!             data: vec![255u8; 64 * 64 * 4],
36//!         })
37//!     }
38//! }
39//!
40//! let corners = [
41//!     GeoCoord::from_lat_lon(40.0, -74.0),
42//!     GeoCoord::from_lat_lon(40.0, -73.0),
43//!     GeoCoord::from_lat_lon(39.0, -73.0),
44//!     GeoCoord::from_lat_lon(39.0, -74.0),
45//! ];
46//! let layer = DynamicImageOverlayLayer::new("video-feed", corners, Box::new(TestProvider));
47//! ```
48
49use crate::camera_projection::CameraProjection;
50use crate::layer::{Layer, LayerId, LayerKind};
51use crate::layers::image_overlay_layer::ImageOverlayData;
52use rustial_math::GeoCoord;
53use std::any::Any;
54use std::sync::Arc;
55
56// ---------------------------------------------------------------------------
57// FrameProvider trait
58// ---------------------------------------------------------------------------
59
60/// A single RGBA8 frame returned by a [`FrameProvider`].
61#[derive(Debug, Clone)]
62pub struct FrameData {
63    /// Image width in pixels.
64    pub width: u32,
65    /// Image height in pixels.
66    pub height: u32,
67    /// RGBA8 pixel data (length must equal `width * height * 4`).
68    pub data: Vec<u8>,
69}
70
71/// Trait for dynamic frame sources (video decoders, canvas renderers,
72/// procedural generators, etc.).
73///
74/// Implementations supply RGBA8 frames to a
75/// [`DynamicImageOverlayLayer`].  The engine polls [`next_frame`] once
76/// per frame while the layer is visible and [`is_animating`] returns
77/// `true`.
78///
79/// [`next_frame`]: FrameProvider::next_frame
80/// [`is_animating`]: FrameProvider::is_animating
81pub trait FrameProvider: Send + Sync {
82    /// Poll for the next frame.
83    ///
84    /// Return `Some(FrameData)` when new pixel data is available, or
85    /// `None` to keep the previous frame.
86    fn next_frame(&mut self) -> Option<FrameData>;
87
88    /// Whether this source is currently animating.
89    ///
90    /// When `false`, the engine skips polling [`next_frame`] until
91    /// the flag changes.  Defaults to `true`.
92    ///
93    /// [`next_frame`]: FrameProvider::next_frame
94    fn is_animating(&self) -> bool {
95        true
96    }
97}
98
99/// A closure-based [`FrameProvider`] for convenience.
100///
101/// Wraps any `FnMut() -> Option<FrameData>` as a frame provider.
102pub struct CallbackFrameProvider<F> {
103    callback: F,
104    animating: bool,
105}
106
107impl<F: FnMut() -> Option<FrameData> + Send + Sync> CallbackFrameProvider<F> {
108    /// Create a new callback-based frame provider.
109    pub fn new(callback: F) -> Self {
110        Self {
111            callback,
112            animating: true,
113        }
114    }
115
116    /// Set whether the provider is currently animating.
117    pub fn set_animating(&mut self, animating: bool) {
118        self.animating = animating;
119    }
120}
121
122impl<F: FnMut() -> Option<FrameData> + Send + Sync> FrameProvider for CallbackFrameProvider<F> {
123    fn next_frame(&mut self) -> Option<FrameData> {
124        (self.callback)()
125    }
126
127    fn is_animating(&self) -> bool {
128        self.animating
129    }
130}
131
132/// Factory type for creating [`FrameProvider`] instances.
133///
134/// Used by [`VideoSource`](crate::style::VideoSource) and
135/// [`CanvasSource`](crate::style::CanvasSource) in the style system to
136/// produce frame providers when a style document is applied.
137pub type FrameProviderFactory = Arc<dyn Fn() -> Box<dyn FrameProvider> + Send + Sync>;
138
139// ---------------------------------------------------------------------------
140// DynamicImageOverlayLayer
141// ---------------------------------------------------------------------------
142
143/// A georeferenced overlay with dynamic frame content.
144///
145/// This is the Rustial equivalent of MapLibre / Mapbox `video` and
146/// `canvas` source types.  A [`FrameProvider`] supplies RGBA8 frames
147/// which are rendered as a textured quad at the specified geographic
148/// coordinates.
149///
150/// The layer caches the most recent frame and tracks a generation
151/// counter for change detection.  Renderers see this layer through
152/// the same [`ImageOverlayData`] path as [`ImageOverlayLayer`],
153/// so no renderer changes are required.
154///
155/// [`ImageOverlayLayer`]: crate::layers::ImageOverlayLayer
156pub struct DynamicImageOverlayLayer {
157    id: LayerId,
158    name: String,
159    visible: bool,
160    opacity: f32,
161    /// Geographic corner coordinates (TL, TR, BR, BL).
162    coordinates: [GeoCoord; 4],
163    /// Cached frame dimensions.
164    width: u32,
165    height: u32,
166    /// Cached RGBA8 pixel data from the last provider frame.
167    data: Arc<Vec<u8>>,
168    /// Monotonically increasing generation counter.
169    generation: u64,
170    /// Whether the first frame has been received.
171    has_frame: bool,
172    /// The frame provider that supplies dynamic content.
173    provider: Box<dyn FrameProvider>,
174}
175
176impl std::fmt::Debug for DynamicImageOverlayLayer {
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        f.debug_struct("DynamicImageOverlayLayer")
179            .field("id", &self.id)
180            .field("name", &self.name)
181            .field("visible", &self.visible)
182            .field("opacity", &self.opacity)
183            .field("width", &self.width)
184            .field("height", &self.height)
185            .field("has_frame", &self.has_frame)
186            .field("generation", &self.generation)
187            .field("animating", &self.provider.is_animating())
188            .finish()
189    }
190}
191
192impl DynamicImageOverlayLayer {
193    /// Create a new dynamic image overlay layer.
194    ///
195    /// `coordinates` must be in TL → TR → BR → BL order.
196    pub fn new(
197        name: impl Into<String>,
198        coordinates: [GeoCoord; 4],
199        provider: Box<dyn FrameProvider>,
200    ) -> Self {
201        Self {
202            id: LayerId::next(),
203            name: name.into(),
204            visible: true,
205            opacity: 1.0,
206            coordinates,
207            width: 0,
208            height: 0,
209            data: Arc::new(Vec::new()),
210            generation: 0,
211            has_frame: false,
212            provider,
213        }
214    }
215
216    /// Geographic corners (TL, TR, BR, BL).
217    #[inline]
218    pub fn coordinates(&self) -> &[GeoCoord; 4] {
219        &self.coordinates
220    }
221
222    /// Update the geographic corners.
223    pub fn set_coordinates(&mut self, coordinates: [GeoCoord; 4]) {
224        self.coordinates = coordinates;
225        self.generation = self.generation.wrapping_add(1);
226    }
227
228    /// Monotonic generation counter, bumped on coordinate or frame changes.
229    #[inline]
230    pub fn generation(&self) -> u64 {
231        self.generation
232    }
233
234    /// Cached frame dimensions `(width, height)`.
235    #[inline]
236    pub fn dimensions(&self) -> (u32, u32) {
237        (self.width, self.height)
238    }
239
240    /// Whether at least one frame has been received.
241    #[inline]
242    pub fn has_frame(&self) -> bool {
243        self.has_frame
244    }
245
246    /// Poll the frame provider for new data.
247    ///
248    /// Returns `true` if a new frame was received and the cached data
249    /// was updated.
250    pub fn poll_frame(&mut self) -> bool {
251        if !self.provider.is_animating() {
252            return false;
253        }
254        if let Some(frame) = self.provider.next_frame() {
255            debug_assert_eq!(
256                frame.data.len(),
257                (frame.width * frame.height * 4) as usize,
258                "FrameData RGBA8 length must equal width * height * 4"
259            );
260            self.width = frame.width;
261            self.height = frame.height;
262            self.data = Arc::new(frame.data);
263            self.generation = self.generation.wrapping_add(1);
264            self.has_frame = true;
265            true
266        } else {
267            false
268        }
269    }
270
271    /// Produce renderer-ready overlay data by projecting geographic
272    /// corners into the active world-space coordinate system.
273    ///
274    /// Returns `None` if no frame has been received yet.
275    pub fn to_overlay_data(&self, projection: CameraProjection) -> Option<ImageOverlayData> {
276        if !self.has_frame || self.width == 0 || self.height == 0 {
277            return None;
278        }
279        let corners = [
280            project_corner(&self.coordinates[0], projection),
281            project_corner(&self.coordinates[1], projection),
282            project_corner(&self.coordinates[2], projection),
283            project_corner(&self.coordinates[3], projection),
284        ];
285        Some(ImageOverlayData {
286            layer_id: self.id,
287            corners,
288            width: self.width,
289            height: self.height,
290            data: Arc::clone(&self.data),
291            opacity: self.opacity,
292        })
293    }
294
295    /// Access the underlying frame provider.
296    pub fn provider(&self) -> &dyn FrameProvider {
297        &*self.provider
298    }
299
300    /// Access the underlying frame provider mutably.
301    pub fn provider_mut(&mut self) -> &mut dyn FrameProvider {
302        &mut *self.provider
303    }
304}
305
306fn project_corner(coord: &GeoCoord, projection: CameraProjection) -> [f64; 3] {
307    let w = projection.project(coord);
308    [w.position.x, w.position.y, w.position.z]
309}
310
311// ---------------------------------------------------------------------------
312// Layer trait implementation
313// ---------------------------------------------------------------------------
314
315impl Layer for DynamicImageOverlayLayer {
316    fn id(&self) -> LayerId {
317        self.id
318    }
319
320    fn name(&self) -> &str {
321        &self.name
322    }
323
324    fn kind(&self) -> LayerKind {
325        LayerKind::Custom
326    }
327
328    fn visible(&self) -> bool {
329        self.visible
330    }
331
332    fn set_visible(&mut self, visible: bool) {
333        self.visible = visible;
334    }
335
336    fn opacity(&self) -> f32 {
337        self.opacity
338    }
339
340    fn set_opacity(&mut self, opacity: f32) {
341        self.opacity = opacity.clamp(0.0, 1.0);
342    }
343
344    fn as_any(&self) -> &dyn Any {
345        self
346    }
347
348    fn as_any_mut(&mut self) -> &mut dyn Any {
349        self
350    }
351}
352
353// ---------------------------------------------------------------------------
354// Tests
355// ---------------------------------------------------------------------------
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    struct CountingProvider {
362        frame_count: u32,
363        width: u32,
364        height: u32,
365    }
366
367    impl CountingProvider {
368        fn new(width: u32, height: u32) -> Self {
369            Self {
370                frame_count: 0,
371                width,
372                height,
373            }
374        }
375    }
376
377    impl FrameProvider for CountingProvider {
378        fn next_frame(&mut self) -> Option<FrameData> {
379            self.frame_count += 1;
380            // Each frame has pixels filled with the frame count (mod 256).
381            let fill = (self.frame_count % 256) as u8;
382            Some(FrameData {
383                width: self.width,
384                height: self.height,
385                data: vec![fill; (self.width * self.height * 4) as usize],
386            })
387        }
388    }
389
390    struct PausableProvider {
391        animating: bool,
392    }
393
394    impl FrameProvider for PausableProvider {
395        fn next_frame(&mut self) -> Option<FrameData> {
396            Some(FrameData {
397                width: 2,
398                height: 2,
399                data: vec![128; 16],
400            })
401        }
402
403        fn is_animating(&self) -> bool {
404            self.animating
405        }
406    }
407
408    struct OneShotProvider {
409        sent: bool,
410    }
411
412    impl FrameProvider for OneShotProvider {
413        fn next_frame(&mut self) -> Option<FrameData> {
414            if self.sent {
415                None
416            } else {
417                self.sent = true;
418                Some(FrameData {
419                    width: 4,
420                    height: 4,
421                    data: vec![255; 64],
422                })
423            }
424        }
425
426        fn is_animating(&self) -> bool {
427            !self.sent
428        }
429    }
430
431    fn sample_corners() -> [GeoCoord; 4] {
432        [
433            GeoCoord::from_lat_lon(40.0, -74.0),
434            GeoCoord::from_lat_lon(40.0, -73.0),
435            GeoCoord::from_lat_lon(39.0, -73.0),
436            GeoCoord::from_lat_lon(39.0, -74.0),
437        ]
438    }
439
440    #[test]
441    fn new_layer_starts_without_frame() {
442        let layer = DynamicImageOverlayLayer::new(
443            "test",
444            sample_corners(),
445            Box::new(CountingProvider::new(8, 8)),
446        );
447        assert!(!layer.has_frame());
448        assert_eq!(layer.dimensions(), (0, 0));
449        assert_eq!(layer.generation(), 0);
450    }
451
452    #[test]
453    fn poll_frame_receives_first_frame() {
454        let mut layer = DynamicImageOverlayLayer::new(
455            "test",
456            sample_corners(),
457            Box::new(CountingProvider::new(8, 8)),
458        );
459        assert!(layer.poll_frame());
460        assert!(layer.has_frame());
461        assert_eq!(layer.dimensions(), (8, 8));
462        assert_eq!(layer.generation(), 1);
463    }
464
465    #[test]
466    fn consecutive_polls_bump_generation() {
467        let mut layer = DynamicImageOverlayLayer::new(
468            "test",
469            sample_corners(),
470            Box::new(CountingProvider::new(4, 4)),
471        );
472        layer.poll_frame();
473        layer.poll_frame();
474        layer.poll_frame();
475        assert_eq!(layer.generation(), 3);
476    }
477
478    #[test]
479    fn paused_provider_skips_poll() {
480        let mut layer = DynamicImageOverlayLayer::new(
481            "test",
482            sample_corners(),
483            Box::new(PausableProvider { animating: false }),
484        );
485        assert!(!layer.poll_frame());
486        assert!(!layer.has_frame());
487        assert_eq!(layer.generation(), 0);
488    }
489
490    #[test]
491    fn one_shot_provider_stops_after_first() {
492        let mut layer = DynamicImageOverlayLayer::new(
493            "test",
494            sample_corners(),
495            Box::new(OneShotProvider { sent: false }),
496        );
497        assert!(layer.poll_frame());
498        assert_eq!(layer.generation(), 1);
499        // Provider is no longer animating.
500        assert!(!layer.poll_frame());
501        assert_eq!(layer.generation(), 1);
502    }
503
504    #[test]
505    fn to_overlay_data_returns_none_without_frame() {
506        let layer = DynamicImageOverlayLayer::new(
507            "test",
508            sample_corners(),
509            Box::new(CountingProvider::new(4, 4)),
510        );
511        assert!(layer
512            .to_overlay_data(CameraProjection::WebMercator)
513            .is_none());
514    }
515
516    #[test]
517    fn to_overlay_data_returns_some_after_poll() {
518        let mut layer = DynamicImageOverlayLayer::new(
519            "test",
520            sample_corners(),
521            Box::new(CountingProvider::new(4, 4)),
522        );
523        layer.poll_frame();
524        let data = layer.to_overlay_data(CameraProjection::WebMercator);
525        assert!(data.is_some());
526        let data = data.unwrap();
527        assert_eq!(data.width, 4);
528        assert_eq!(data.height, 4);
529        assert_eq!(data.data.len(), 64);
530    }
531
532    #[test]
533    fn set_coordinates_bumps_generation() {
534        let mut layer = DynamicImageOverlayLayer::new(
535            "test",
536            sample_corners(),
537            Box::new(CountingProvider::new(4, 4)),
538        );
539        let g0 = layer.generation();
540        layer.set_coordinates([
541            GeoCoord::from_lat_lon(50.0, -75.0),
542            GeoCoord::from_lat_lon(50.0, -74.0),
543            GeoCoord::from_lat_lon(49.0, -74.0),
544            GeoCoord::from_lat_lon(49.0, -75.0),
545        ]);
546        assert_eq!(layer.generation(), g0 + 1);
547    }
548
549    #[test]
550    fn opacity_clamps_to_valid_range() {
551        let mut layer = DynamicImageOverlayLayer::new(
552            "test",
553            sample_corners(),
554            Box::new(CountingProvider::new(4, 4)),
555        );
556        layer.set_opacity(2.0);
557        assert_eq!(layer.opacity(), 1.0);
558        layer.set_opacity(-0.5);
559        assert_eq!(layer.opacity(), 0.0);
560    }
561
562    #[test]
563    fn callback_frame_provider_works() {
564        let counter = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
565        let counter_clone = counter.clone();
566        let provider = CallbackFrameProvider::new(move || {
567            let n = counter_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
568            Some(FrameData {
569                width: 2,
570                height: 2,
571                data: vec![(n % 256) as u8; 16],
572            })
573        });
574        let mut layer = DynamicImageOverlayLayer::new("test", sample_corners(), Box::new(provider));
575        layer.poll_frame();
576        assert!(layer.has_frame());
577        assert_eq!(counter.load(std::sync::atomic::Ordering::Relaxed), 1);
578        layer.poll_frame();
579        assert_eq!(counter.load(std::sync::atomic::Ordering::Relaxed), 2);
580    }
581}