diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java index 404f6a1274..e8f98a7663 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java @@ -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; diff --git a/lib/components/MapView.js b/lib/components/MapView.js index a6660719a4..2e8ba4e845 100644 --- a/lib/components/MapView.js +++ b/lib/components/MapView.js @@ -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 { diff --git a/lib/ios/AirGoogleMaps/AIRGoogleMap.m b/lib/ios/AirGoogleMaps/AIRGoogleMap.m index ef73067b61..f87d34c633 100644 --- a/lib/ios/AirGoogleMaps/AIRGoogleMap.m +++ b/lib/ios/AirGoogleMaps/AIRGoogleMap.m @@ -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 diff --git a/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m b/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m index 6a3b92e90e..c482305b87 100644 --- a/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m +++ b/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m @@ -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 diff --git a/lib/ios/AirMaps/AIRMap.h b/lib/ios/AirMaps/AIRMap.h index 18c9a0bc05..4a88617e65 100644 --- a/lib/ios/AirMaps/AIRMap.h +++ b/lib/ios/AirMaps/AIRMap.h @@ -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; diff --git a/lib/ios/AirMaps/AIRMapManager.h b/lib/ios/AirMaps/AIRMapManager.h index cc9a8c75b5..29df98bfc8 100644 --- a/lib/ios/AirMaps/AIRMapManager.h +++ b/lib/ios/AirMaps/AIRMapManager.h @@ -8,7 +8,23 @@ */ #import +#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 diff --git a/lib/ios/AirMaps/AIRMapManager.m b/lib/ios/AirMaps/AIRMapManager.m index 619142bd66..8ea2472445 100644 --- a/lib/ios/AirMaps/AIRMapManager.m +++ b/lib/ios/AirMaps/AIRMapManager.m @@ -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) { @@ -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) { @@ -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)) { @@ -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