Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions include/map_renderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ inline void MapRenderer_setCamera(
self.map->jumpTo(cameraOptions);
}

inline void MapRenderer_setMapProjection(MapRenderer& self, mbgl::MapProjectionType projection) {
self.map->setMapProjection(projection);
}

inline void MapRenderer_getStyle_loadURL(MapRenderer& self, const rust::Str styleUrl) {
self.map->getStyle().loadURL((std::string)styleUrl);
}
Expand Down
13 changes: 13 additions & 0 deletions src/renderer/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ pub mod ffi {
Tile,
}

#[repr(u32)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
/// Map projection type configuration.
pub enum MapProjectionType {
/// Render in standard Web Mercator.
#[default]
Mercator,
/// Render using the globe projection.
Globe,
}

#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// Debug visualization options for map rendering.
Expand Down Expand Up @@ -107,6 +118,7 @@ pub mod ffi {

type MapMode;
type MapDebugOptions;
type MapProjectionType;
pub type EventSeverity;
pub type Event;
}
Expand Down Expand Up @@ -146,6 +158,7 @@ pub mod ffi {
bearing: f64,
pitch: f64,
);
fn MapRenderer_setMapProjection(obj: Pin<&mut MapRenderer>, projection: MapProjectionType);
fn MapRenderer_getStyle_loadURL(obj: Pin<&mut MapRenderer>, url: &str);
}

Expand Down
19 changes: 17 additions & 2 deletions src/renderer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::num::NonZeroU32;
use std::path::PathBuf;

use crate::renderer::bridge::ffi;
use crate::renderer::{ImageRenderer, MapMode, Static, Tile};
use crate::renderer::{ImageRenderer, MapMode, MapProjectionType, Static, Tile};

/// Builder for configuring [`ImageRenderer`] instances
///
Expand All @@ -26,6 +26,8 @@ pub struct ImageRendererBuilder {
height: u32,
/// Pixel ratio for high-DPI displays
pixel_ratio: f32,
/// Map projection mode
map_projection: MapProjectionType,

/// Cache database file path
cache_path: Option<PathBuf>,
Expand Down Expand Up @@ -64,6 +66,7 @@ impl Default for ImageRendererBuilder {
width: 512,
height: 512,
pixel_ratio: 1.0,
map_projection: MapProjectionType::Mercator,

cache_path: None,
asset_root: std::env::current_dir().ok(),
Expand Down Expand Up @@ -114,6 +117,16 @@ impl ImageRendererBuilder {
self
}

/// Sets map projection mode.
///
/// Default: `MapProjectionType::Mercator`
#[must_use]
#[allow(clippy::needless_pass_by_value, reason = "false positive")]
pub fn with_projection(mut self, map_projection: MapProjectionType) -> Self {
self.map_projection = map_projection;
self
}

/// Sets cache database file path
///
/// Default: no cache
Expand Down Expand Up @@ -250,7 +263,7 @@ impl ImageRendererBuilder {
impl<S> ImageRenderer<S> {
/// Creates a new renderer instance
fn new(map_mode: MapMode, opts: ImageRendererBuilder) -> Self {
let map = ffi::MapRenderer_new(
let mut map = ffi::MapRenderer_new(
map_mode,
opts.width,
opts.height,
Expand All @@ -273,10 +286,12 @@ impl<S> ImageRenderer<S> {
&opts.tile_template,
opts.requires_api_key,
);
ffi::MapRenderer_setMapProjection(map.pin_mut(), opts.map_projection);

Self {
instance: map,
style_specified: false,
map_projection: opts.map_projection,
_marker: PhantomData,
}
}
Expand Down
19 changes: 19 additions & 0 deletions src/renderer/image_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub struct ImageRenderer<S> {
pub(crate) instance: UniquePtr<ffi::MapRenderer>,
pub(crate) _marker: PhantomData<S>,
pub(crate) style_specified: bool,
pub(crate) map_projection: ffi::MapProjectionType,
}

impl<S> Debug for ImageRenderer<S> {
Expand Down Expand Up @@ -121,6 +122,19 @@ impl<S> ImageRenderer<S> {
ffi::MapRenderer_setDebugFlags(self.instance.pin_mut(), flags);
self
}

/// Sets map projection mode.
pub fn set_projection(&mut self, projection: ffi::MapProjectionType) -> &mut Self {
self.map_projection = projection;
ffi::MapRenderer_setMapProjection(self.instance.pin_mut(), projection);
self
}

/// Returns map projection mode.
#[must_use]
pub fn projection(&self) -> ffi::MapProjectionType {
self.map_projection
}
}

impl ImageRenderer<Static> {
Expand All @@ -141,6 +155,7 @@ impl ImageRenderer<Static> {
return Err(RenderingError::StyleNotSpecified);
}

ffi::MapRenderer_setMapProjection(self.instance.pin_mut(), self.map_projection);
ffi::MapRenderer_setCamera(self.instance.pin_mut(), lat, lon, zoom, bearing, pitch);
let data = ffi::MapRenderer_render(self.instance.pin_mut());
let bytes = data.as_bytes();
Expand All @@ -161,6 +176,10 @@ impl ImageRenderer<Tile> {
return Err(RenderingError::StyleNotSpecified);
}

ffi::MapRenderer_setMapProjection(
self.instance.pin_mut(),
ffi::MapProjectionType::Mercator,
);
let (lat, lon) = coords_to_lat_lon(f64::from(zoom), x, y);
ffi::MapRenderer_setCamera(self.instance.pin_mut(), lat, lon, f64::from(zoom), 0.0, 0.0);

Expand Down
2 changes: 1 addition & 1 deletion src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ mod bridge;
mod builder;
mod image_renderer;

pub use bridge::ffi::{MapDebugOptions, MapMode};
pub use bridge::ffi::{MapDebugOptions, MapMode, MapProjectionType};
pub use bridge::set_log_thread_enabled;
pub use builder::ImageRendererBuilder;
pub use image_renderer::{Image, ImageRenderer, RenderingError, Static, Tile};
92 changes: 92 additions & 0 deletions tests/projection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//! Integration tests for native projection behavior in static rendering mode.

use std::num::NonZeroU32;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};

use maplibre_native::{ImageRendererBuilder, MapProjectionType};

fn test_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.expect("projection test lock should be available")
}

fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join(name)
}

fn alpha_at_corners(image: &image::ImageBuffer<image::Rgba<u8>, Vec<u8>>) -> [u8; 4] {
let max_x = image.width().saturating_sub(1);
let max_y = image.height().saturating_sub(1);
[
image.get_pixel(0, 0)[3],
image.get_pixel(max_x, 0)[3],
image.get_pixel(0, max_y)[3],
image.get_pixel(max_x, max_y)[3],
]
}

#[test]
fn projection_setter_roundtrips() {
let _guard = test_lock();

let mut renderer = ImageRendererBuilder::new()
.with_size(
NonZeroU32::new(128).expect("constant non-zero width"),
NonZeroU32::new(128).expect("constant non-zero height"),
)
.with_projection(MapProjectionType::Mercator)
.build_static_renderer();

assert_eq!(renderer.projection(), MapProjectionType::Mercator);
renderer.set_projection(MapProjectionType::Globe);
assert_eq!(renderer.projection(), MapProjectionType::Globe);
renderer.set_projection(MapProjectionType::Mercator);
assert_eq!(renderer.projection(), MapProjectionType::Mercator);
}

#[test]
fn projection_mode_switch_keeps_rendering_valid() {
let _guard = test_lock();

let mut renderer = ImageRendererBuilder::new()
.with_size(
NonZeroU32::new(128).expect("constant non-zero width"),
NonZeroU32::new(128).expect("constant non-zero height"),
)
.with_projection(MapProjectionType::Mercator)
.build_static_renderer();
renderer
.load_style_from_path(fixture_path("test-style.json"))
.expect("fixture style should load");

renderer.set_projection(MapProjectionType::Mercator);
let _ = renderer
.render_static(0.0, 0.0, 1.5, 0.0, 0.0)
.expect("warmup render should succeed");
let mercator = renderer
.render_static(0.0, 0.0, 1.5, 0.0, 0.0)
.expect("mercator render should succeed");
let mercator_corners = alpha_at_corners(mercator.as_image());

renderer.set_projection(MapProjectionType::Globe);
assert_eq!(renderer.projection(), MapProjectionType::Globe);
let _ = renderer
.render_static(0.0, 0.0, 1.5, 0.0, 0.0)
.expect("warmup globe render should succeed");
let globe = renderer
.render_static(0.0, 0.0, 1.5, 0.0, 0.0)
.expect("globe render should succeed");
let globe_corners = alpha_at_corners(globe.as_image());

assert_eq!(
mercator.as_image().dimensions(),
globe.as_image().dimensions()
);
assert_eq!(mercator_corners.len(), globe_corners.len());
}