Skip to content

Commit

Permalink
Add minZoom and maxZoom properties for android and ios (react-native-…
Browse files Browse the repository at this point in the history
…maps#1360)

* Add minZoom and maxZoom properties for android and ios

* Add min/max zoom level properties to AIRMap.h

* Fix assignation for zoom levels for gmaps ios

* Resolve conflicts, fix potential nil value on ios

* Resolve conflicts
  • Loading branch information
foyarash authored and patricio committed Sep 27, 2017
1 parent f9daee2 commit 681e7d2
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,16 @@ public void setPitchEnabled(AirMapView view, boolean pitchEnabled) {
view.map.getUiSettings().setTiltGesturesEnabled(pitchEnabled);
}

@ReactProp(name = "minZoomLevel")
public void setMinZoomLevel(AirMapView view, float minZoomLevel) {
view.map.setMinZoomPreference(minZoomLevel);
}

@ReactProp(name = "maxZoomLevel")
public void setMaxZoomLevel(AirMapView view, float maxZoomLevel) {
view.map.setMaxZoomPreference(maxZoomLevel);
}

@Override
public void receiveCommand(AirMapView view, int commandId, @Nullable ReadableArray args) {
Integer duration;
Expand Down
10 changes: 10 additions & 0 deletions lib/components/MapView.js
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,16 @@ const propTypes = {
*/
onMarkerDragEnd: PropTypes.func,

/**
* Minimum zoom value for the map, must be between 0 and 20
*/
minZoomLevel: PropTypes.number,

/**
* Maximum zoom value for the map, must be between 0 and 20
*/
maxZoomLevel: PropTypes.number,

};

class MapView extends React.Component {
Expand Down
7 changes: 7 additions & 0 deletions lib/ios/AirGoogleMaps/AIRGoogleMap.m
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,13 @@ - (BOOL)showsMyLocationButton {
return self.settings.myLocationButton;
}

- (void)setMinZoomLevel:(CGFloat)minZoomLevel {
[self setMinZoom:minZoomLevel maxZoom:self.maxZoom ];
}

- (void)setMaxZoomLevel:(CGFloat)maxZoomLevel {
[self setMinZoom:self.minZoom maxZoom:maxZoomLevel ];
}

+ (MKCoordinateRegion) makeGMSCameraPositionFromMap:(GMSMapView *)map andGMSCameraPosition:(GMSCameraPosition *)position {
// solution from here: http://stackoverflow.com/a/16587735/1102215
Expand Down
2 changes: 2 additions & 0 deletions lib/ios/AirGoogleMaps/AIRGoogleMapManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onRegionChangeComplete, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(mapType, GMSMapViewType)
RCT_EXPORT_VIEW_PROPERTY(minZoomLevel, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(maxZoomLevel, CGFloat)

RCT_EXPORT_METHOD(animateToRegion:(nonnull NSNumber *)reactTag
withRegion:(MKCoordinateRegion)region
Expand Down
2 changes: 2 additions & 0 deletions lib/ios/AirMaps/AIRMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ extern const CGFloat AIRMapZoomBoundBuffer;
@property (nonatomic, assign) UIEdgeInsets legalLabelInsets;
@property (nonatomic, strong) NSTimer *regionChangeObserveTimer;
@property (nonatomic, assign) MKCoordinateRegion initialRegion;
@property (nonatomic, assign) CGFloat minZoomLevel;
@property (nonatomic, assign) CGFloat maxZoomLevel;

@property (nonatomic, assign) CLLocationCoordinate2D pendingCenter;
@property (nonatomic, assign) MKCoordinateSpan pendingSpan;
Expand Down
16 changes: 16 additions & 0 deletions lib/ios/AirMaps/AIRMapManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,23 @@
*/

#import <React/RCTViewManager.h>
#import "AIRMap.h"

#define MERCATOR_RADIUS 85445659.44705395
#define MERCATOR_OFFSET 268435456
#define MAX_GOOGLE_LEVELS 20

@interface AIRMapManager : RCTViewManager


- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
zoomLevel:(double)zoomLevel
animated:(BOOL)animated
mapView:(AIRMap *)mapView;

- (MKCoordinateRegion)coordinateRegionWithMapView:(AIRMap *)mapView
centerCoordinate:(CLLocationCoordinate2D)centerCoordinate
andZoomLevel:(double)zoomLevel;
- (double) zoomLevel:(AIRMap *)mapView;

@end
169 changes: 169 additions & 0 deletions lib/ios/AirMaps/AIRMapManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(onMarkerDragEnd, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onCalloutPress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(initialRegion, MKCoordinateRegion)
RCT_EXPORT_VIEW_PROPERTY(minZoomLevel, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(maxZoomLevel, CGFloat)


RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, AIRMap)
{
Expand Down Expand Up @@ -625,11 +628,19 @@ - (void)mapView:(AIRMap *)mapView regionWillChangeAnimated:(__unused BOOL)animat

- (void)mapView:(AIRMap *)mapView regionDidChangeAnimated:(__unused BOOL)animated
{
CGFloat zoomLevel = [self zoomLevel:mapView];
[mapView.regionChangeObserveTimer invalidate];
mapView.regionChangeObserveTimer = nil;

[self _regionChanged:mapView];

if (mapView.minZoomLevel != nil && zoomLevel < mapView.minZoomLevel) {
[self setCenterCoordinate:[mapView centerCoordinate] zoomLevel:mapView.minZoomLevel animated:TRUE mapView:mapView];
}
else if (mapView.maxZoomLevel != nil && zoomLevel > mapView.maxZoomLevel) {
[self setCenterCoordinate:[mapView centerCoordinate] zoomLevel:mapView.maxZoomLevel animated:TRUE mapView:mapView];
}

// Don't send region did change events until map has
// started rendering, as these won't represent the final location
if (mapView.hasStartedRendering) {
Expand Down Expand Up @@ -665,6 +676,7 @@ - (void)_regionChanged:(AIRMap *)mapView
BOOL needZoom = NO;
CGFloat newLongitudeDelta = 0.0f;
MKCoordinateRegion region = mapView.region;
CGFloat zoomLevel = [self zoomLevel:mapView];
// On iOS 7, it's possible that we observe invalid locations during initialization of the map.
// Filter those out.
if (!CLLocationCoordinate2DIsValid(region.center)) {
Expand Down Expand Up @@ -759,4 +771,161 @@ - (double)metersFromPixel:(NSUInteger)px atPoint:(CGPoint)pt forMap:(AIRMap *)ma
return MKMetersBetweenMapPoints(MKMapPointForCoordinate(coordA), MKMapPointForCoordinate(coordB));
}

+ (double)longitudeToPixelSpaceX:(double)longitude
{
return round(MERCATOR_OFFSET + MERCATOR_RADIUS * longitude * M_PI / 180.0);
}

+ (double)latitudeToPixelSpaceY:(double)latitude
{
if (latitude == 90.0) {
return 0;
} else if (latitude == -90.0) {
return MERCATOR_OFFSET * 2;
} else {
return round(MERCATOR_OFFSET - MERCATOR_RADIUS * logf((1 + sinf(latitude * M_PI / 180.0)) / (1 - sinf(latitude * M_PI / 180.0))) / 2.0);
}
}

+ (double)pixelSpaceXToLongitude:(double)pixelX
{
return ((round(pixelX) - MERCATOR_OFFSET) / MERCATOR_RADIUS) * 180.0 / M_PI;
}

+ (double)pixelSpaceYToLatitude:(double)pixelY
{
return (M_PI / 2.0 - 2.0 * atan(exp((round(pixelY) - MERCATOR_OFFSET) / MERCATOR_RADIUS))) * 180.0 / M_PI;
}

#pragma mark -
#pragma mark Helper methods

- (MKCoordinateSpan)coordinateSpanWithMapView:(AIRMap *)mapView
centerCoordinate:(CLLocationCoordinate2D)centerCoordinate
andZoomLevel:(double)zoomLevel
{
// convert center coordiate to pixel space
double centerPixelX = [AIRMapManager longitudeToPixelSpaceX:centerCoordinate.longitude];
double centerPixelY = [AIRMapManager latitudeToPixelSpaceY:centerCoordinate.latitude];

// determine the scale value from the zoom level
double zoomExponent = 20 - zoomLevel;
double zoomScale = pow(2, zoomExponent);

// scale the map’s size in pixel space
CGSize mapSizeInPixels = mapView.bounds.size;
double scaledMapWidth = mapSizeInPixels.width * zoomScale;
double scaledMapHeight = mapSizeInPixels.height * zoomScale;

// figure out the position of the top-left pixel
double topLeftPixelX = centerPixelX - (scaledMapWidth / 2);
double topLeftPixelY = centerPixelY - (scaledMapHeight / 2);

// find delta between left and right longitudes
CLLocationDegrees minLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX];
CLLocationDegrees maxLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX + scaledMapWidth];
CLLocationDegrees longitudeDelta = maxLng - minLng;

// find delta between top and bottom latitudes
CLLocationDegrees minLat = [AIRMapManager pixelSpaceYToLatitude:topLeftPixelY];
CLLocationDegrees maxLat = [AIRMapManager pixelSpaceYToLatitude:topLeftPixelY + scaledMapHeight];
CLLocationDegrees latitudeDelta = -1 * (maxLat - minLat);

// create and return the lat/lng span
MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
return span;
}

#pragma mark -
#pragma mark Public methods

- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
zoomLevel:(double)zoomLevel
animated:(BOOL)animated
mapView:(AIRMap *)mapView
{
// clamp large numbers to 28
zoomLevel = MIN(zoomLevel, 28);

// use the zoom level to compute the region
MKCoordinateSpan span = [self coordinateSpanWithMapView:mapView centerCoordinate:centerCoordinate andZoomLevel:zoomLevel];
MKCoordinateRegion region = MKCoordinateRegionMake(centerCoordinate, span);

// set the region like normal
[mapView setRegion:region animated:animated];
}

//KMapView cannot display tiles that cross the pole (as these would involve wrapping the map from top to bottom, something that a Mercator projection just cannot do).
-(MKCoordinateRegion)coordinateRegionWithMapView:(AIRMap *)mapView
centerCoordinate:(CLLocationCoordinate2D)centerCoordinate
andZoomLevel:(double)zoomLevel
{
// clamp lat/long values to appropriate ranges
centerCoordinate.latitude = MIN(MAX(-90.0, centerCoordinate.latitude), 90.0);
centerCoordinate.longitude = fmod(centerCoordinate.longitude, 180.0);

// convert center coordiate to pixel space
double centerPixelX = [AIRMapManager longitudeToPixelSpaceX:centerCoordinate.longitude];
double centerPixelY = [AIRMapManager latitudeToPixelSpaceY:centerCoordinate.latitude];

// determine the scale value from the zoom level
double zoomExponent = 20 - zoomLevel;
double zoomScale = pow(2, zoomExponent);

// scale the map’s size in pixel space
CGSize mapSizeInPixels = mapView.bounds.size;
double scaledMapWidth = mapSizeInPixels.width * zoomScale;
double scaledMapHeight = mapSizeInPixels.height * zoomScale;

// figure out the position of the left pixel
double topLeftPixelX = centerPixelX - (scaledMapWidth / 2);

// find delta between left and right longitudes
CLLocationDegrees minLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX];
CLLocationDegrees maxLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX + scaledMapWidth];
CLLocationDegrees longitudeDelta = maxLng - minLng;

// if we’re at a pole then calculate the distance from the pole towards the equator
// as MKMapView doesn’t like drawing boxes over the poles
double topPixelY = centerPixelY - (scaledMapHeight / 2);
double bottomPixelY = centerPixelY + (scaledMapHeight / 2);
BOOL adjustedCenterPoint = NO;
if (topPixelY > MERCATOR_OFFSET * 2) {
topPixelY = centerPixelY - scaledMapHeight;
bottomPixelY = MERCATOR_OFFSET * 2;
adjustedCenterPoint = YES;
}

// find delta between top and bottom latitudes
CLLocationDegrees minLat = [AIRMapManager pixelSpaceYToLatitude:topPixelY];
CLLocationDegrees maxLat = [AIRMapManager pixelSpaceYToLatitude:bottomPixelY];
CLLocationDegrees latitudeDelta = -1 * (maxLat - minLat);

// create and return the lat/lng span
MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
MKCoordinateRegion region = MKCoordinateRegionMake(centerCoordinate, span);
// once again, MKMapView doesn’t like drawing boxes over the poles
// so adjust the center coordinate to the center of the resulting region
if (adjustedCenterPoint) {
region.center.latitude = [AIRMapManager pixelSpaceYToLatitude:((bottomPixelY + topPixelY) / 2.0)];
}

return region;
}

- (double) zoomLevel:(AIRMap *)mapView {
MKCoordinateRegion region = mapView.region;

double centerPixelX = [AIRMapManager longitudeToPixelSpaceX: region.center.longitude];
double topLeftPixelX = [AIRMapManager longitudeToPixelSpaceX: region.center.longitude - region.span.longitudeDelta / 2];

double scaledMapWidth = (centerPixelX - topLeftPixelX) * 2;
CGSize mapSizeInPixels = mapView.bounds.size;
double zoomScale = scaledMapWidth / mapSizeInPixels.width;
double zoomExponent = log(zoomScale) / log(2);
double zoomLevel = 20 - zoomExponent;

return zoomLevel;
}

@end

0 comments on commit 681e7d2

Please sign in to comment.