Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Fix fit to bounds #1783

Merged
merged 4 commits into from
Jun 26, 2015
Merged
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
2 changes: 2 additions & 0 deletions gyp/platform-ios.gypi
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
'../platform/ios/MGLUserLocationAnnotationView.m',
'../include/mbgl/ios/MGLTypes.h',
'../platform/ios/MGLTypes.m',
'../include/mbgl/ios/MGLGeometry.h',
'../platform/ios/MGLGeometry.m',
'../include/mbgl/ios/MGLMultiPoint.h',
'../platform/ios/MGLMultiPoint_Private.h',
'../platform/ios/MGLMultiPoint.mm',
Expand Down
85 changes: 85 additions & 0 deletions include/mbgl/ios/MGLGeometry.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#pragma once

#import "MGLTypes.h"

#import <CoreLocation/CoreLocation.h>

NS_ASSUME_NONNULL_BEGIN

/** Defines the area spanned by an MGLCoordinateBounds. */
typedef struct {
CLLocationDegrees latitudeDelta;
CLLocationDegrees longitudeDelta;
} MGLCoordinateSpan;

/** Creates a new MGLCoordinateSpan from the given latitudinal and longitudinal deltas. */
NS_INLINE MGLCoordinateSpan MGLCoordinateSpanMake(CLLocationDegrees latitudeDelta, CLLocationDegrees longitudeDelta) {
MGLCoordinateSpan span;
span.latitudeDelta = latitudeDelta;
span.longitudeDelta = longitudeDelta;
return span;
}

/** Returns `YES` if the two coordinate spans represent the same latitudinal change and the same longitudinal change. */
NS_INLINE BOOL MGLCoordinateSpanEqualToCoordinateSpan(MGLCoordinateSpan span1, MGLCoordinateSpan span2) {
return (span1.latitudeDelta == span2.latitudeDelta &&
span1.longitudeDelta == span2.longitudeDelta);
}

/** An area of zero width and zero height. */
extern const MGLCoordinateSpan MGLCoordinateSpanZero;

/** A rectangular area as measured on a two-dimensional map projection. */
typedef struct {
CLLocationCoordinate2D sw;
CLLocationCoordinate2D ne;
} MGLCoordinateBounds;

/** Creates a new MGLCoordinateBounds structure from the given southwest and northeast coordinates. */
NS_INLINE MGLCoordinateBounds MGLCoordinateBoundsMake(CLLocationCoordinate2D sw, CLLocationCoordinate2D ne) {
MGLCoordinateBounds bounds;
bounds.sw = sw;
bounds.ne = ne;
return bounds;
}

/** Returns `YES` if the two coordinate bounds are equal to each other. */
NS_INLINE BOOL MGLCoordinateBoundsEqualToCoordinateBounds(MGLCoordinateBounds bounds1, MGLCoordinateBounds bounds2) {
return (bounds1.sw.latitude == bounds2.sw.latitude &&
bounds1.sw.longitude == bounds2.sw.longitude &&
bounds1.ne.latitude == bounds2.ne.latitude &&
bounds1.ne.longitude == bounds2.ne.longitude);
}

/** Returns the area spanned by the coordinate bounds. */
NS_INLINE MGLCoordinateSpan MGLCoordinateBoundsGetCoordinateSpan(MGLCoordinateBounds bounds) {
return MGLCoordinateSpanMake(bounds.ne.latitude - bounds.sw.latitude,
bounds.ne.longitude - bounds.sw.longitude);
}

/** Returns a coordinate bounds with southwest and northeast coordinates that are offset from those of the source bounds. */
NS_INLINE MGLCoordinateBounds MGLCoordinateBoundsOffset(MGLCoordinateBounds bounds, MGLCoordinateSpan offset) {
MGLCoordinateBounds offsetBounds = bounds;
offsetBounds.sw.latitude += offset.latitudeDelta;
offsetBounds.sw.longitude += offset.longitudeDelta;
offsetBounds.ne.latitude += offset.latitudeDelta;
offsetBounds.ne.longitude += offset.longitudeDelta;
return offsetBounds;
}

/** Returns `YES` if the coordinate bounds covers no area.
*
* Note that a bounds may be empty but have a non-zero coordinate span (e.g., when its northeast point lies due north of its southwest point). */
NS_INLINE BOOL MGLCoordinateBoundsIsEmpty(MGLCoordinateBounds bounds) {
MGLCoordinateSpan span = MGLCoordinateBoundsGetCoordinateSpan(bounds);
return span.latitudeDelta == 0 || span.longitudeDelta == 0;
}

/** Returns a formatted string for the given coordinate bounds. */
NS_INLINE NSString *MGLStringFromCoordinateBounds(MGLCoordinateBounds bounds) {
return [NSString stringWithFormat:@"{{%.1f, %.1f}, {%.1f, %.1f}}",
bounds.sw.latitude, bounds.sw.longitude,
bounds.ne.latitude, bounds.ne.longitude];
}

NS_ASSUME_NONNULL_END
16 changes: 15 additions & 1 deletion include/mbgl/ios/MGLMapView.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#import "MGLTypes.h"
#import "MGLGeometry.h"

#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
Expand Down Expand Up @@ -118,6 +118,20 @@ IB_DESIGNABLE
* @param animated Specify `YES` if you want the map view to animate scrolling and zooming to the new location or `NO` if you want the map to display the new location immediately. */
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel animated:(BOOL)animated;

/** Returns the coordinate bounds visible in the receiver’s viewport. */
- (MGLCoordinateBounds)visibleCoordinateBounds;

/** Changes the receiver’s viewport to fit the given coordinate bounds, optionally animating the change.
* @param bounds The bounds that the viewport will show in its entirety.
* @param animated Specify `YES` to animate the change by smoothly scrolling and zooming or `NO` to immediately display the given bounds. */
- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds animated:(BOOL)animated;

/** Changes the receiver’s viewport to fit the given coordinate bounds, optionally animating the change.
* @param bounds The bounds that the viewport will show in its entirety.
* @param insets The minimum padding (in screen points) that will be visible around the given coordinate bounds.
* @param animated Specify `YES` to animate the change by smoothly scrolling and zooming or `NO` to immediately display the given bounds. */
- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated;

/** The heading of the map (measured in degrees) relative to true north.
*
* The value `0` means that the top edge of the map view corresponds to true north. The value `90` means the top of the map is pointing due east. The value `180` means the top of the map points due south, and so on. */
Expand Down
2 changes: 1 addition & 1 deletion include/mbgl/ios/MGLOverlay.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#import <CoreLocation/CoreLocation.h>

#import "MGLAnnotation.h"
#import "MGLTypes.h"
#import "MGLGeometry.h"

NS_ASSUME_NONNULL_BEGIN

Expand Down
8 changes: 2 additions & 6 deletions include/mbgl/ios/MGLTypes.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>

#pragma once

#if !__has_feature(nullability)
#define NS_ASSUME_NONNULL_BEGIN
Expand All @@ -23,11 +24,6 @@ typedef NS_ENUM(NSUInteger, MGLUserTrackingMode) {
MGLUserTrackingModeFollowWithHeading
};

typedef struct {
CLLocationCoordinate2D sw;
CLLocationCoordinate2D ne;
} MGLCoordinateBounds;

NS_ASSUME_NONNULL_END

#pragma clang diagnostic push
Expand Down
1 change: 1 addition & 0 deletions include/mbgl/ios/MapboxGL.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import "MGLAccountManager.h"
#import "MGLAnnotation.h"
#import "MGLGeometry.h"
#import "MGLMapView.h"
#import "MGLMultiPoint.h"
#import "MGLOverlay.h"
Expand Down
5 changes: 5 additions & 0 deletions include/mbgl/map/map.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ using AnnotationIDs = std::vector<uint32_t>;
using AnnotationSegment = std::vector<LatLng>;
using AnnotationSegments = std::vector<AnnotationSegment>;

using EdgeInsets = struct {
double top, left, bottom, right;
};

class Map : private util::noncopyable {
friend class View;

Expand Down Expand Up @@ -95,6 +99,7 @@ class Map : private util::noncopyable {
void setZoom(double zoom, Duration = Duration::zero());
double getZoom() const;
void setLatLngZoom(LatLng latLng, double zoom, Duration = Duration::zero());
void fitBounds(LatLngBounds bounds, EdgeInsets padding, Duration duration = Duration::zero());
void resetZoom();
double getMinZoom() const;
double getMaxZoom() const;
Expand Down
3 changes: 3 additions & 0 deletions platform/ios/MGLGeometry.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#import "MGLGeometry.h"

const MGLCoordinateSpan MGLCoordinateSpanZero = {0, 0};
40 changes: 29 additions & 11 deletions platform/ios/MGLMapView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1445,20 +1445,38 @@ - (void)setZoomLevel:(double)zoomLevel
[self setZoomLevel:zoomLevel animated:NO];
}

- (void)zoomToSouthWestCoordinate:(CLLocationCoordinate2D)southWestCoordinate northEastCoordinate:(CLLocationCoordinate2D)northEastCoordinate animated:(BOOL)animated
MGLCoordinateBounds MGLCoordinateBoundsFromLatLngBounds(mbgl::LatLngBounds latLngBounds)
{
// NOTE: does not disrupt tracking mode
return MGLCoordinateBoundsMake(MGLLocationCoordinate2DFromLatLng(latLngBounds.sw),
MGLLocationCoordinate2DFromLatLng(latLngBounds.ne));
}

mbgl::LatLngBounds MGLLatLngBoundsFromCoordinateBounds(MGLCoordinateBounds coordinateBounds)
{
return mbgl::LatLngBounds(MGLLatLngFromLocationCoordinate2D(coordinateBounds.sw), MGLLatLngFromLocationCoordinate2D(coordinateBounds.ne));
}

CLLocationCoordinate2D center = CLLocationCoordinate2DMake((northEastCoordinate.latitude + southWestCoordinate.latitude) / 2, (northEastCoordinate.longitude + southWestCoordinate.longitude) / 2);
- (MGLCoordinateBounds)visibleCoordinateBounds
{
return MGLCoordinateBoundsFromLatLngBounds(self.viewportBounds);
}

CGFloat scale = _mbglMap->getScale();
CGFloat scaleX = _mbglMap->getWidth() / (northEastCoordinate.longitude - southWestCoordinate.longitude);
CGFloat scaleY = _mbglMap->getHeight() / (northEastCoordinate.latitude - southWestCoordinate.latitude);
CGFloat minZoom = _mbglMap->getMinZoom();
CGFloat maxZoom = _mbglMap->getMaxZoom();
CGFloat zoomLevel = MAX(MIN(log(scale * MIN(scaleX, scaleY)) / log(2), maxZoom), minZoom);
- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds animated:(BOOL)animated
{
[self setVisibleCoordinateBounds:bounds edgePadding:UIEdgeInsetsZero animated:animated];
}

[self setCenterCoordinate:center zoomLevel:zoomLevel animated:animated];
- (void)setVisibleCoordinateBounds:(MGLCoordinateBounds)bounds edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated
{
// NOTE: does not disrupt tracking mode
CGFloat duration = animated ? MGLAnimationDuration : 0;

mbgl::EdgeInsets mbglInsets = {insets.top, insets.left, insets.bottom, insets.right};
_mbglMap->fitBounds(MGLLatLngBoundsFromCoordinateBounds(bounds), mbglInsets, secondsAsDuration(duration));

[self unrotateIfNeededAnimated:animated];

[self notifyMapChange:@(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)];
}

- (CLLocationDirection)direction
Expand Down Expand Up @@ -2212,7 +2230,7 @@ - (void)locationManager:(__unused CLLocationManager *)manager didUpdateToLocatio
desiredSouthWest.longitude != actualSouthWest.longitude)
{
// assumes we won't disrupt tracking mode
[self zoomToSouthWestCoordinate:desiredSouthWest northEastCoordinate:desiredNorthEast animated:YES];
[self setVisibleCoordinateBounds:MGLCoordinateBoundsMake(desiredSouthWest, desiredNorthEast) animated:YES];
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion platform/ios/MGLMultiPoint.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#import "MGLMultiPoint.h"
#import "MGLTypes.h"
#import "MGLGeometry.h"

#import <mbgl/util/geo.hpp>

Expand Down
39 changes: 39 additions & 0 deletions src/mbgl/map/map.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,45 @@ void Map::setLatLngZoom(LatLng latLng, double zoom, Duration duration) {
update(Update::Zoom);
}

void Map::fitBounds(LatLngBounds bounds, EdgeInsets padding, Duration duration) {
// Calculate the bounds of the possibly rotated `bounds` parameter with respect to the viewport.
vec2<> nwPixel = pixelForLatLng({bounds.ne.latitude, bounds.sw.longitude});
vec2<> swPixel = pixelForLatLng(bounds.sw);
vec2<> sePixel = pixelForLatLng({bounds.sw.latitude, bounds.ne.longitude});
vec2<> nePixel = pixelForLatLng(bounds.ne);
vec2<> visualBounds = {
(std::max(std::max(nwPixel.x, swPixel.x),
std::max(sePixel.x, nePixel.x)) -
std::min(std::min(nwPixel.x, swPixel.x),
std::min(sePixel.x, nePixel.x))),
(std::max(std::max(nwPixel.y, swPixel.y),
std::max(sePixel.y, nePixel.y)) -
std::min(std::min(nwPixel.y, swPixel.y),
std::min(sePixel.y, nePixel.y))),
};

// Calculate the zoom level.
double scaleX = (getWidth() - padding.left - padding.right) / visualBounds.x;
double scaleY = (getHeight() - padding.top - padding.bottom) / visualBounds.y;
double minScale = std::fmin(scaleX, scaleY);
double zoom = std::log2(getScale() * minScale);
zoom = std::fmax(std::fmin(zoom, getMaxZoom()), getMinZoom());

// Calculate the center point of a virtual bounds that is extended in all directions by padding.
vec2<> paddedNEPixel = {
nePixel.x + padding.right / minScale,
nePixel.y + padding.top / minScale,
};
vec2<> paddedSWPixel = {
swPixel.x - padding.left / minScale,
swPixel.y - padding.bottom / minScale,
};
vec2<> centerPixel = (paddedNEPixel + paddedSWPixel) * 0.5;
LatLng centerLatLng = latLngForPixel(centerPixel);

setLatLngZoom(centerLatLng, zoom, duration);
}

void Map::resetZoom() {
setZoom(0);
}
Expand Down
35 changes: 35 additions & 0 deletions test/ios/MapViewTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,41 @@ - (void)testZoomDisabled {
@"disabling zoom gesture should disallow pinching");
}

- (void)testFitToBounds {
// No-op
MGLCoordinateBounds initialBounds = tester.mapView.visibleCoordinateBounds;
[tester.mapView setVisibleCoordinateBounds:initialBounds animated:NO];
XCTAssertEqualObjects(MGLStringFromCoordinateBounds(initialBounds),
MGLStringFromCoordinateBounds(tester.mapView.visibleCoordinateBounds),
@"setting visible coordinate bounds to currently visible coordinate bounds should be a no-op");

// Roundtrip after zooming
tester.mapView.zoomLevel -= 3;
[tester.mapView setVisibleCoordinateBounds:initialBounds animated:NO];
XCTAssertEqualObjects(MGLStringFromCoordinateBounds(initialBounds),
MGLStringFromCoordinateBounds(tester.mapView.visibleCoordinateBounds),
@"after zooming out, setting visible coordinate bounds back to %@ should not leave them at %@",
MGLStringFromCoordinateBounds(initialBounds),
MGLStringFromCoordinateBounds(tester.mapView.visibleCoordinateBounds));
tester.mapView.zoomLevel += 3;
[tester.mapView setVisibleCoordinateBounds:initialBounds animated:NO];
XCTAssertEqualObjects(MGLStringFromCoordinateBounds(initialBounds),
MGLStringFromCoordinateBounds(tester.mapView.visibleCoordinateBounds),
@"after zooming in, setting visible coordinate bounds back to %@ should not leave them at %@",
MGLStringFromCoordinateBounds(initialBounds),
MGLStringFromCoordinateBounds(tester.mapView.visibleCoordinateBounds));

// Roundtrip after panning
MGLCoordinateBounds offsetBounds = MGLCoordinateBoundsOffset(initialBounds, MGLCoordinateSpanMake(0, 30));
[tester.mapView setVisibleCoordinateBounds:offsetBounds animated:NO];
[tester.mapView setVisibleCoordinateBounds:initialBounds animated:NO];
XCTAssertEqualObjects(MGLStringFromCoordinateBounds(initialBounds),
MGLStringFromCoordinateBounds(tester.mapView.visibleCoordinateBounds),
@"after panning 30° to the east, setting visible coordinate bounds back to %@ should not leave them at %@",
MGLStringFromCoordinateBounds(initialBounds),
MGLStringFromCoordinateBounds(tester.mapView.visibleCoordinateBounds));
}

- (void)testPan {
CLLocationCoordinate2D centerCoordinate = tester.mapView.centerCoordinate;

Expand Down