use std::cell::RefCell;
use std::f64::consts::PI;
use std::fmt::Debug;
use std::marker::PhantomData;
use std::path::Path;
use std::rc::Rc;
use cxx::UniquePtr;
use image::{ImageBuffer, Rgba};
use super::MapObserver;
use crate::bridge::ffi;
use crate::bridge::ffi::BridgeImage;
use crate::renderer::map_observer::MapObserverCallbacks;
use crate::renderer::{MapDebugOptions, MapLoadError, MapLoadErrorKind};
use crate::{
CameraUpdate, EdgeInsets, GeoJson, LatLng, LatLngBounds, RunLoopHandle, ScreenCoordinate, Size,
StyleRef,
};
#[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) observer_callbacks: Rc<MapObserverCallbacks>,
pub(crate) _marker: PhantomData<S>,
pub(crate) _not_send: PhantomData<*mut ()>,
pub(crate) style_specified: bool,
}
#[must_use = "render requests must be finished or waited on to complete the render"]
pub struct RenderRequest<'a, S> {
instance: UniquePtr<ffi::RenderRequest>,
_renderer: PhantomData<&'a mut ImageRenderer<S>>,
_not_send: PhantomData<*mut ()>,
}
impl<S> Debug for RenderRequest<'_, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RenderRequest").field("ready", &self.is_ready()).finish_non_exhaustive()
}
}
impl<S> RenderRequest<'_, S> {
#[must_use]
pub fn is_ready(&self) -> bool {
self.instance.isReady()
}
pub fn finish(mut self) -> Result<Image, RenderingError> {
assert!(self.is_ready(), "render request is not ready");
if self.instance.hasError() {
return Err(RenderingError::Native(self.instance.errorMessage()));
}
let data = self.instance.pin_mut().takeImage();
let bytes = data.as_bytes();
Image::from_raw(bytes).ok_or(RenderingError::InvalidImageData)
}
pub fn wait(self) -> Result<Image, RenderingError> {
let run_loop = RunLoopHandle::current();
while !self.is_ready() {
run_loop.wait_for_event();
}
self.finish()
}
}
enum StyleLoadState {
Pending,
Loaded,
Failed(StyleLoadError),
}
pub struct StyleLoadRequest<'a, S> {
state: Rc<RefCell<StyleLoadState>>,
_renderer: PhantomData<&'a mut ImageRenderer<S>>,
_not_send: PhantomData<*mut ()>,
}
impl<S> Debug for StyleLoadRequest<'_, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StyleLoadRequest").field("ready", &self.is_ready()).finish_non_exhaustive()
}
}
impl<S> StyleLoadRequest<'_, S> {
fn new(state: Rc<RefCell<StyleLoadState>>) -> Self {
Self { state, _renderer: PhantomData, _not_send: PhantomData }
}
#[must_use]
pub fn is_ready(&self) -> bool {
!matches!(*self.state.borrow(), StyleLoadState::Pending)
}
pub fn finish(self) -> Result<(), StyleLoadError> {
match std::mem::replace(&mut *self.state.borrow_mut(), StyleLoadState::Pending) {
StyleLoadState::Loaded => Ok(()),
StyleLoadState::Failed(e) => Err(e),
StyleLoadState::Pending => panic!("style load request is not ready"),
}
}
pub fn wait(self) -> Result<(), StyleLoadError> {
let run_loop = RunLoopHandle::current();
while !self.is_ready() {
run_loop.wait_for_event();
}
self.finish()
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("{kind}: {message}")]
#[non_exhaustive]
pub struct StyleLoadError {
pub kind: MapLoadErrorKind,
pub message: String,
}
impl StyleLoadError {
fn new(error: MapLoadError) -> Self {
Self { kind: error.kind, message: error.message }
}
}
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) -> StyleLoadRequest<'_, S> {
let state = self.begin_style_load();
self.instance.pin_mut().style_load_from_url(url.as_str());
StyleLoadRequest::new(state)
}
pub fn load_style_from_json_str(&mut self, json: impl AsRef<str>) -> StyleLoadRequest<'_, S> {
let state = self.begin_style_load();
self.instance.pin_mut().style_load_from_json(json.as_ref());
StyleLoadRequest::new(state)
}
#[cfg(feature = "json")]
pub fn load_style_from_json_value(
&mut self,
value: &serde_json::Value,
) -> Result<StyleLoadRequest<'_, S>, serde_json::Error> {
let json = serde_json::to_string(value)?;
Ok(self.load_style_from_json_str(json))
}
pub fn load_style_from_path(
&mut self,
path: impl AsRef<Path>,
) -> Result<StyleLoadRequest<'_, S>, 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()),
));
};
let state = self.begin_style_load();
self.instance.pin_mut().style_load_from_url(&format!("file://{path}"));
Ok(StyleLoadRequest::new(state))
}
fn begin_style_load(&mut self) -> Rc<RefCell<StyleLoadState>> {
self.style_specified = true;
let state = Rc::new(RefCell::new(StyleLoadState::Pending));
self.map_observer().set_style_load_request_callbacks(
{
let weak = Rc::downgrade(&state);
move || {
if let Some(s) = weak.upgrade() {
let mut state = s.borrow_mut();
if matches!(*state, StyleLoadState::Pending) {
*state = StyleLoadState::Loaded;
}
}
RunLoopHandle::current().stop();
}
},
{
let weak = Rc::downgrade(&state);
move |error| {
if let Some(s) = weak.upgrade() {
let mut state = s.borrow_mut();
if matches!(*state, StyleLoadState::Pending) {
*state = StyleLoadState::Failed(StyleLoadError::new(error));
}
}
RunLoopHandle::current().stop();
}
},
);
state
}
pub fn set_debug_flags(&mut self, flags: MapDebugOptions) -> &mut Self {
self.instance.pin_mut().setDebugFlags(flags);
self
}
pub fn set_map_size(&mut self, size: Size) {
self.instance.pin_mut().setSize(&size);
}
pub fn map_observer(&mut self) -> MapObserver {
MapObserver::new(self.instance.pin_mut().observer(), Rc::clone(&self.observer_callbacks))
}
pub fn style(&mut self) -> StyleRef<'_, S> {
StyleRef::new(self)
}
#[must_use]
pub fn camera_for_bounds(
&mut self,
bounds: LatLngBounds,
padding: Option<EdgeInsets>,
bearing: f64,
pitch: f64,
) -> CameraUpdate {
let padding = padding.unwrap_or_default();
let camera =
self.instance.pin_mut().cameraForLatLngBounds(&bounds, &padding, bearing, pitch);
CameraUpdate::from_camera_options(camera)
}
#[must_use]
pub fn camera_for_lat_lngs(
&mut self,
lat_lngs: &[LatLng],
padding: Option<EdgeInsets>,
bearing: f64,
pitch: f64,
) -> Option<CameraUpdate> {
let padding = padding.unwrap_or_default();
let camera = self.instance.pin_mut().cameraForLatLngs(lat_lngs, &padding, bearing, pitch);
(camera.has_center || camera.has_zoom).then(|| CameraUpdate::from_camera_options(camera))
}
#[must_use]
pub fn camera_for_geojson(
&mut self,
geojson: &GeoJson,
padding: Option<EdgeInsets>,
bearing: f64,
pitch: f64,
) -> Option<CameraUpdate> {
let padding = padding.unwrap_or_default();
let camera =
self.instance.pin_mut().cameraForGeoJson(geojson.as_inner(), &padding, bearing, pitch);
(camera.has_center || camera.has_zoom).then(|| CameraUpdate::from_camera_options(camera))
}
fn submit_with_camera(
&mut self,
camera: &CameraUpdate,
) -> Result<RenderRequest<'_, S>, RenderingError> {
if !self.style_specified {
return Err(RenderingError::StyleNotSpecified);
}
self.instance.pin_mut().jumpTo(&camera.to_camera_options());
let request = self.instance.pin_mut().submitRender();
Ok(RenderRequest { instance: request, _renderer: PhantomData, _not_send: PhantomData })
}
}
impl ImageRenderer<Static> {
pub fn render_static(&mut self, camera: &CameraUpdate) -> Result<Image, RenderingError> {
self.submit_render_static(camera)?.wait()
}
pub fn submit_render_static(
&mut self,
camera: &CameraUpdate,
) -> Result<RenderRequest<'_, Static>, RenderingError> {
self.submit_with_camera(camera)
}
}
impl ImageRenderer<Tile> {
pub fn render_tile(&mut self, zoom: u8, x: u32, y: u32) -> Result<Image, RenderingError> {
self.submit_render_tile(zoom, x, y)?.wait()
}
pub fn submit_render_tile(
&mut self,
zoom: u8,
x: u32,
y: u32,
) -> Result<RenderRequest<'_, Tile>, RenderingError> {
let center = tile_coords_to_latlng(f64::from(zoom), x, y);
self.submit_with_camera(
&CameraUpdate::new().center(center).zoom(f64::from(zoom)).bearing(0.0).pitch(0.0),
)
}
}
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 update_camera(&mut self, camera: &CameraUpdate) {
self.instance.pin_mut().jumpTo(&camera.to_camera_options());
}
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 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 tile_coords_to_latlng(zoom: f64, x: u32, y: u32) -> LatLng {
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();
LatLng { lat, lng }
}
#[derive(thiserror::Error, Debug)]
pub enum RenderingError {
#[error("Style must be specified before rendering")]
StyleNotSpecified,
#[error("Invalid image data received from renderer")]
InvalidImageData,
#[error("Native rendering error: {0}")]
Native(String),
}
#[cfg(test)]
mod tests {
use std::num::NonZeroU32;
use super::tile_coords_to_latlng;
use crate::ImageRendererBuilder;
#[test]
fn converts_tile_zero_to_geographic_center() {
let center = tile_coords_to_latlng(0.0, 0, 0);
assert!(center.lat.abs() < f64::EPSILON);
assert!(center.lng.abs() < f64::EPSILON);
}
#[test]
fn tile_coordinate_conversion_returns_typed_coordinates() {
let center = tile_coords_to_latlng(1.0, 1, 1);
assert!((-90.0..=90.0).contains(¢er.lat));
assert!((-180.0..=180.0).contains(¢er.lng));
}
#[test]
fn load_style_from_path_rejects_non_files() {
let size = NonZeroU32::new(64).unwrap();
let mut renderer =
ImageRendererBuilder::new().with_size(size, size).build_static_renderer();
let missing = renderer.load_style_from_path("does-not-exist.json").unwrap_err();
assert_eq!(missing.kind(), std::io::ErrorKind::NotFound);
let empty = renderer.load_style_from_path("").unwrap_err();
assert_eq!(empty.kind(), std::io::ErrorKind::NotFound);
}
}