use super::MapObserver;
use crate::renderer::bridge::ffi;
use crate::renderer::bridge::ffi::BridgeImage;
use crate::renderer::MapDebugOptions;
use crate::{Latitude, Longitude, ScreenCoordinate, Size};
use cxx::UniquePtr;
use image::{ImageBuffer, Rgba};
use std::f64::consts::PI;
use std::fmt::Debug;
use std::marker::PhantomData;
use std::path::Path;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Image(ImageBuffer<Rgba<u8>, Vec<u8>>);
impl Image {
pub(crate) fn from_raw(bytes: &[u8]) -> Option<Self> {
if bytes.len() < 8 {
return None;
}
let width = u32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let height = u32::from_ne_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
let data = bytes[8..].to_vec();
ImageBuffer::from_vec(width, height, data).map(Image)
}
#[must_use]
pub fn as_image(&self) -> &ImageBuffer<Rgba<u8>, Vec<u8>> {
&self.0
}
}
#[derive(Debug)]
pub struct Static;
#[derive(Debug)]
pub struct Tile;
#[derive(Debug)]
pub struct Continuous;
pub struct ImageRenderer<S> {
pub(crate) instance: UniquePtr<ffi::MapRenderer>,
pub(crate) _marker: PhantomData<S>,
pub(crate) style_specified: bool,
}
impl<S> Debug for ImageRenderer<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ImageRenderer")
.field("style_specified", &self.style_specified)
.finish_non_exhaustive()
}
}
impl<S> ImageRenderer<S> {
pub fn load_style_from_url(&mut self, url: &url::Url) -> &mut Self {
self.style_specified = true;
self.instance.pin_mut().style_load_from_url(url.as_str());
self
}
pub fn load_style_from_path(
&mut self,
path: impl AsRef<Path>,
) -> Result<&mut Self, std::io::Error> {
let path = path.as_ref();
if !path.is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Path {} is not a file", path.display()),
));
}
let Some(path) = path.to_str() else {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Path {} is not valid UTF-8", path.display()),
));
};
self.style_specified = true;
self.instance.pin_mut().style_load_from_url(&format!("file://{path}"));
Ok(self)
}
pub fn set_debug_flags(&mut self, flags: MapDebugOptions) -> &mut Self {
self.instance.pin_mut().setDebugFlags(flags);
self
}
}
impl ImageRenderer<Static> {
pub fn render_static(
&mut self,
lat: f64,
lon: f64,
zoom: f64,
bearing: f64,
pitch: f64,
) -> Result<Image, RenderingError> {
if !self.style_specified {
return Err(RenderingError::StyleNotSpecified);
}
self.instance.pin_mut().setCamera(lat, lon, zoom, bearing, pitch);
let data = self.instance.pin_mut().render();
let bytes = data.as_bytes();
let image = Image::from_raw(bytes).ok_or(RenderingError::InvalidImageData)?;
Ok(image)
}
}
impl ImageRenderer<Tile> {
pub fn render_tile(&mut self, zoom: u8, x: u32, y: u32) -> Result<Image, RenderingError> {
if !self.style_specified {
return Err(RenderingError::StyleNotSpecified);
}
let (lat, lon) = coords_to_lat_lon(f64::from(zoom), x, y);
self.instance.pin_mut().setCamera(lat.0, lon.0, f64::from(zoom), 0.0, 0.0);
let data = self.instance.pin_mut().render();
let bytes = data.as_bytes();
let image = Image::from_raw(bytes).ok_or(RenderingError::InvalidImageData)?;
Ok(image)
}
}
pub struct ImagePtr {
instance: UniquePtr<BridgeImage>,
}
impl Debug for ImagePtr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ImagePtr")
}
}
impl ImagePtr {
fn new(image: UniquePtr<BridgeImage>) -> Self {
Self { instance: image }
}
pub fn size(&self) -> Size {
self.instance.size()
}
pub fn buffer(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.instance.get(), self.instance.bufferLength()) }
}
}
impl ImageRenderer<Continuous> {
pub fn set_camera(
&mut self,
latitude: Latitude,
longitude: Longitude,
zoom: f64,
bearing: f64,
pitch: f64,
) {
self.instance.pin_mut().setCamera(latitude.0, longitude.0, zoom, bearing, pitch);
}
pub fn map_observer(&mut self) -> MapObserver {
MapObserver::new(self.instance.pin_mut().observer())
}
pub fn move_by(&mut self, delta: ScreenCoordinate) {
self.instance.pin_mut().moveBy(&delta);
}
pub fn scale_by(&mut self, scale: f64, pos: ScreenCoordinate) {
self.instance.pin_mut().scaleBy(scale, &pos);
}
pub fn set_map_size(&mut self, size: Size) {
self.instance.pin_mut().setSize(&size);
}
pub fn render_once(&mut self) {
self.instance.pin_mut().render_once();
}
pub fn read_still_image(&mut self) -> ImagePtr {
ImagePtr::new(self.instance.pin_mut().readStillImage())
}
}
#[allow(clippy::cast_precision_loss)]
fn coords_to_lat_lon(zoom: f64, x: u32, y: u32) -> (Latitude, Longitude) {
let zz = 2_f64.powf(zoom);
let lng = (f64::from(x) + 0.5) / zz * 360_f64 - 180_f64;
let lat = ((PI * (1_f64 - 2_f64 * (f64::from(y) + 0.5) / zz)).sinh()).atan().to_degrees();
(Latitude(lat), Longitude(lng))
}
#[derive(thiserror::Error, Debug)]
pub enum RenderingError {
#[error("Style must be specified to render a tile")]
StyleNotSpecified,
#[error("Invalid image data received from renderer")]
InvalidImageData,
}
#[cfg(test)]
mod tests {
use super::{coords_to_lat_lon, Latitude, Longitude};
#[test]
fn converts_tile_zero_to_geographic_center() {
let (lat, lon) = coords_to_lat_lon(0.0, 0, 0);
assert!(matches!(lat, Latitude(v) if v.abs() < f64::EPSILON));
assert!(matches!(lon, Longitude(v) if v.abs() < f64::EPSILON));
}
#[test]
fn tile_coordinate_conversion_returns_typed_coordinates() {
let (lat, lon) = coords_to_lat_lon(1.0, 1, 1);
let Latitude(lat) = lat;
let Longitude(lon) = lon;
assert!((-90.0..=90.0).contains(&lat));
assert!((-180.0..=180.0).contains(&lon));
}
}