ios: add basic support for setting camera roll (#4061)
This commit is contained in:
@@ -26,6 +26,9 @@ MLN_EXPORT
|
||||
/** Heading measured in degrees clockwise from true north. */
|
||||
@property (nonatomic) CLLocationDirection heading;
|
||||
|
||||
/** Roll measured in degrees clockwise around the viewing axis. */
|
||||
@property (nonatomic) CLLocationDirection roll;
|
||||
|
||||
/**
|
||||
Pitch toward the horizon measured in degrees, with 0 degrees resulting in a
|
||||
two-dimensional map.
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
_altitude = altitude;
|
||||
_pitch = pitch;
|
||||
_heading = heading;
|
||||
_roll = 0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -108,6 +109,7 @@
|
||||
_altitude = [coder decodeDoubleForKey:@"altitude"];
|
||||
_pitch = [coder decodeDoubleForKey:@"pitch"];
|
||||
_heading = [coder decodeDoubleForKey:@"heading"];
|
||||
_roll = [coder decodeDoubleForKey:@"roll"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -119,14 +121,17 @@
|
||||
[coder encodeDouble:_altitude forKey:@"altitude"];
|
||||
[coder encodeDouble:_pitch forKey:@"pitch"];
|
||||
[coder encodeDouble:_heading forKey:@"heading"];
|
||||
[coder encodeDouble:_roll forKey:@"roll"];
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(nullable NSZone *)zone
|
||||
{
|
||||
return [[[self class] allocWithZone:zone] initWithCenterCoordinate:_centerCoordinate
|
||||
altitude:_altitude
|
||||
pitch:_pitch
|
||||
heading:_heading];
|
||||
MLNMapCamera *copy = [[[self class] allocWithZone:zone] initWithCenterCoordinate:_centerCoordinate
|
||||
altitude:_altitude
|
||||
pitch:_pitch
|
||||
heading:_heading];
|
||||
copy.roll = _roll;
|
||||
return copy;
|
||||
}
|
||||
|
||||
+ (NSSet<NSString *> *)keyPathsForValuesAffectingViewingDistance {
|
||||
@@ -146,8 +151,8 @@
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"<%@: %p; centerCoordinate = %f, %f; altitude = %.0fm; heading = %.0f°; pitch = %.0f°>",
|
||||
NSStringFromClass([self class]), (void *)self, _centerCoordinate.latitude, _centerCoordinate.longitude, _altitude, _heading, _pitch];
|
||||
return [NSString stringWithFormat:@"<%@: %p; centerCoordinate = %f, %f; altitude = %.0fm; heading = %.0f°; pitch = %.0f°; roll = %.0f°>",
|
||||
NSStringFromClass([self class]), (void *)self, _centerCoordinate.latitude, _centerCoordinate.longitude, _altitude, _heading, _pitch, _roll];
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)other
|
||||
@@ -165,7 +170,8 @@
|
||||
return (_centerCoordinate.latitude == otherCamera.centerCoordinate.latitude
|
||||
&& _centerCoordinate.longitude == otherCamera.centerCoordinate.longitude
|
||||
&& _altitude == otherCamera.altitude
|
||||
&& _pitch == otherCamera.pitch && _heading == otherCamera.heading);
|
||||
&& _pitch == otherCamera.pitch && _heading == otherCamera.heading
|
||||
&& _roll == otherCamera.roll);
|
||||
}
|
||||
|
||||
- (BOOL)isEqualToMapCamera:(MLNMapCamera *)otherCamera
|
||||
@@ -179,13 +185,14 @@
|
||||
&& MLNEqualFloatWithAccuracy(_centerCoordinate.longitude, otherCamera.centerCoordinate.longitude, 1e-8)
|
||||
&& MLNEqualFloatWithAccuracy(_altitude, otherCamera.altitude, 1e-6)
|
||||
&& MLNEqualFloatWithAccuracy(_pitch, otherCamera.pitch, 1e-2)
|
||||
&& MLNEqualFloatWithAccuracy(_heading, otherCamera.heading, 1e-2));
|
||||
&& MLNEqualFloatWithAccuracy(_heading, otherCamera.heading, 1e-2)
|
||||
&& MLNEqualFloatWithAccuracy(_roll, otherCamera.roll, 1e-2));
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return (@(_centerCoordinate.latitude).hash + @(_centerCoordinate.longitude).hash
|
||||
+ @(_altitude).hash + @(_pitch).hash + @(_heading).hash);
|
||||
+ @(_altitude).hash + @(_pitch).hash + @(_heading).hash + @(_roll).hash);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -827,6 +827,9 @@ NSArray<MLNAttributionInfo *> *MLNAttributionInfosFromAttributions(mbgl::MapSnap
|
||||
if (options.camera.pitch >= 0) {
|
||||
cameraOptions.pitch = MAX(0, options.camera.pitch);
|
||||
}
|
||||
if (options.camera.roll != 0) {
|
||||
cameraOptions.roll = options.camera.roll;
|
||||
}
|
||||
if (cameraOptions != mbgl::CameraOptions()) {
|
||||
_mbglMapSnapshotter->setCameraOptions(cameraOptions);
|
||||
}
|
||||
|
||||
@@ -104,4 +104,72 @@
|
||||
XCTAssertEqual(camera.viewingDistance, 10000);
|
||||
}
|
||||
|
||||
- (void)testRollProperty {
|
||||
MLNMapCamera *camera = [MLNMapCamera camera];
|
||||
XCTAssertEqual(camera.roll, 0, @"Default camera should have zero roll.");
|
||||
|
||||
camera.roll = 45;
|
||||
XCTAssertEqual(camera.roll, 45, @"Roll should be set correctly.");
|
||||
|
||||
camera.roll = -30;
|
||||
XCTAssertEqual(camera.roll, -30, @"Roll should accept negative values.");
|
||||
|
||||
camera.roll = 180;
|
||||
XCTAssertEqual(camera.roll, 180, @"Roll should accept 180 degrees.");
|
||||
}
|
||||
|
||||
- (void)testRollInCopy {
|
||||
MLNMapCamera *camera = [MLNMapCamera camera];
|
||||
camera.roll = 30;
|
||||
|
||||
MLNMapCamera *copy = [camera copy];
|
||||
XCTAssertEqual(copy.roll, 30, @"Copied camera should preserve roll value.");
|
||||
}
|
||||
|
||||
- (void)testRollInEncoding {
|
||||
CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(40.0, -80.0);
|
||||
MLNMapCamera *camera = [MLNMapCamera cameraLookingAtCenterCoordinate:coord
|
||||
altitude:1000
|
||||
pitch:45
|
||||
heading:90];
|
||||
camera.roll = 25;
|
||||
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:camera requiringSecureCoding:YES error:NULL];
|
||||
MLNMapCamera *decodedCamera = [NSKeyedUnarchiver unarchivedObjectOfClass:[MLNMapCamera class] fromData:data error:NULL];
|
||||
|
||||
XCTAssertEqual(decodedCamera.roll, 25, @"Decoded camera should preserve roll value.");
|
||||
}
|
||||
|
||||
- (void)testRollInEquality {
|
||||
CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(40.0, -80.0);
|
||||
MLNMapCamera *camera1 = [MLNMapCamera cameraLookingAtCenterCoordinate:coord
|
||||
altitude:1000
|
||||
pitch:45
|
||||
heading:90];
|
||||
camera1.roll = 30;
|
||||
|
||||
MLNMapCamera *camera2 = [MLNMapCamera cameraLookingAtCenterCoordinate:coord
|
||||
altitude:1000
|
||||
pitch:45
|
||||
heading:90];
|
||||
camera2.roll = 30;
|
||||
|
||||
XCTAssertTrue([camera1 isEqual:camera2], @"Cameras with same roll should be equal.");
|
||||
XCTAssertTrue([camera1 isEqualToMapCamera:camera2], @"Cameras with same roll should be equal (isEqualToMapCamera).");
|
||||
|
||||
camera2.roll = 35;
|
||||
XCTAssertFalse([camera1 isEqual:camera2], @"Cameras with different roll should not be equal.");
|
||||
XCTAssertFalse([camera1 isEqualToMapCamera:camera2], @"Cameras with different roll should not be equal (isEqualToMapCamera).");
|
||||
}
|
||||
|
||||
- (void)testRollInHash {
|
||||
MLNMapCamera *camera1 = [MLNMapCamera camera];
|
||||
camera1.roll = 45;
|
||||
|
||||
MLNMapCamera *camera2 = [MLNMapCamera camera];
|
||||
camera2.roll = 45;
|
||||
|
||||
XCTAssertEqual(camera1.hash, camera2.hash, @"Cameras with same properties including roll should have same hash.");
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
128
platform/ios/app-swift/Sources/CameraSliderExample.swift
Normal file
128
platform/ios/app-swift/Sources/CameraSliderExample.swift
Normal file
@@ -0,0 +1,128 @@
|
||||
import CoreLocation
|
||||
import MapLibre
|
||||
import SwiftUI
|
||||
|
||||
let maxPitch: Double = 90
|
||||
|
||||
struct CameraSliderExample: View {
|
||||
@State private var pitch: Double = 0
|
||||
@State private var roll: Double = 0
|
||||
private let pitchRange: ClosedRange<Double> = 0 ... maxPitch
|
||||
private let rollRange: ClosedRange<Double> = -180 ... 180
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
OrientationAdjustableMap(pitch: $pitch, roll: $roll)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
sliderRow(label: "Pitch", value: $pitch, range: pitchRange)
|
||||
sliderRow(label: "Roll", value: $roll, range: rollRange)
|
||||
}
|
||||
.padding()
|
||||
.background(.ultraThinMaterial)
|
||||
.cornerRadius(12)
|
||||
.shadow(color: Color.black.opacity(0.15), radius: 6, x: 0, y: 3)
|
||||
.padding([.top, .horizontal])
|
||||
}
|
||||
.navigationTitle("Pitch & Roll")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
private func sliderRow(label: String, value: Binding<Double>, range: ClosedRange<Double>) -> some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("\(label): \(Int(value.wrappedValue)) deg")
|
||||
.font(.headline)
|
||||
Slider(value: value, in: range, step: 1)
|
||||
.accentColor(MapLibreColors.accent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct OrientationAdjustableMap: UIViewRepresentable {
|
||||
@Binding var pitch: Double
|
||||
@Binding var roll: Double
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(pitch: $pitch, roll: $roll)
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> MLNMapView {
|
||||
let styleJSON = """
|
||||
{
|
||||
"version": 8,
|
||||
"name": "Satellite",
|
||||
"sources": {
|
||||
"satellite-source": {
|
||||
"type": "raster",
|
||||
"tiles": ["https://versatiles-satellite.b-cdn.net/tiles/orthophotos/{z}/{x}/{y}"],
|
||||
"tileSize": 256,
|
||||
"attribution": "Versatiles"
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "satellite-layer",
|
||||
"type": "raster",
|
||||
"source": "satellite-source"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
let mapView = MLNMapView(frame: .zero)
|
||||
mapView.styleJSON = styleJSON
|
||||
mapView.delegate = context.coordinator
|
||||
mapView.maximumPitch = CGFloat(maxPitch)
|
||||
let center = CLLocationCoordinate2D(latitude: 40.7128, longitude: -74.0060)
|
||||
mapView.setCenter(center, zoomLevel: 10, animated: false)
|
||||
context.coordinator.sync(from: mapView.camera)
|
||||
return mapView
|
||||
}
|
||||
|
||||
func updateUIView(_ mapView: MLNMapView, context _: Context) {
|
||||
let currentCamera = mapView.camera
|
||||
let targetPitch = CGFloat(pitch)
|
||||
let targetRoll = CGFloat(roll)
|
||||
|
||||
let pitchDelta = abs(currentCamera.pitch - targetPitch)
|
||||
let rollDelta = abs(currentCamera.roll - targetRoll)
|
||||
|
||||
if pitchDelta < 0.1, rollDelta < 0.1 {
|
||||
return
|
||||
}
|
||||
|
||||
let currentZoom = mapView.zoomLevel
|
||||
let targetAltitude = MLNAltitudeForZoomLevel(currentZoom, targetPitch, currentCamera.centerCoordinate.latitude, mapView.bounds.size)
|
||||
|
||||
let updatedCamera = MLNMapCamera(
|
||||
lookingAtCenter: currentCamera.centerCoordinate,
|
||||
altitude: targetAltitude,
|
||||
pitch: targetPitch,
|
||||
heading: currentCamera.heading
|
||||
)
|
||||
updatedCamera.roll = targetRoll
|
||||
mapView.setCamera(updatedCamera, animated: false)
|
||||
}
|
||||
|
||||
final class Coordinator: NSObject, MLNMapViewDelegate {
|
||||
@Binding private var pitch: Double
|
||||
@Binding private var roll: Double
|
||||
|
||||
init(pitch: Binding<Double>, roll: Binding<Double>) {
|
||||
_pitch = pitch
|
||||
_roll = roll
|
||||
}
|
||||
|
||||
func sync(from camera: MLNMapCamera) {
|
||||
let newPitch = Double(camera.pitch)
|
||||
let newRoll = Double(camera.roll)
|
||||
if abs(pitch - newPitch) >= 0.1 {
|
||||
pitch = newPitch
|
||||
}
|
||||
if abs(roll - newRoll) >= 0.1 {
|
||||
roll = newRoll
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ struct MapLibreNavigationView: View {
|
||||
NavigationLink("SimpleMap") {
|
||||
SimpleMap().edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
NavigationLink("Change Camera Pitch & Roll") {
|
||||
CameraSliderExample()
|
||||
}
|
||||
#if MLN_RENDER_BACKEND_METAL
|
||||
NavigationLink("CustomStyleLayer (Metal)") {
|
||||
CustomStyleLayerExample().edgesIgnoringSafeArea(.all)
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
{
|
||||
cameraOptions.pitch = camera.pitch;
|
||||
}
|
||||
cameraOptions.roll = camera.roll;
|
||||
|
||||
_mbglProjection->setCamera(cameraOptions);
|
||||
}
|
||||
|
||||
@@ -4590,7 +4590,14 @@ static void *windowScreenContext = &windowScreenContext;
|
||||
CLLocationDirection direction = cameraOptions.bearing ? mbgl::util::wrap(*cameraOptions.bearing, 0., 360.) : self.direction;
|
||||
CGFloat pitch = cameraOptions.pitch ? *cameraOptions.pitch : *mapCamera.pitch;
|
||||
CLLocationDistance altitude = MLNAltitudeForZoomLevel(zoomLevel, pitch, centerCoordinate.latitude, self.frame.size);
|
||||
return [MLNMapCamera cameraLookingAtCenterCoordinate:centerCoordinate altitude:altitude pitch:pitch heading:direction];
|
||||
|
||||
auto camera = [MLNMapCamera cameraLookingAtCenterCoordinate:centerCoordinate altitude:altitude pitch:pitch heading:direction];
|
||||
|
||||
if (cameraOptions.roll) {
|
||||
camera.roll = cameraOptions.roll.value();
|
||||
}
|
||||
|
||||
return camera;
|
||||
}
|
||||
|
||||
/// Returns a CameraOptions object that specifies parameters for animating to
|
||||
@@ -4614,6 +4621,7 @@ static void *windowScreenContext = &windowScreenContext;
|
||||
{
|
||||
options.pitch = camera.pitch;
|
||||
}
|
||||
options.roll = camera.roll;
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user