Skip to content
This repository was archived by the owner on Mar 20, 2024. It is now read-only.

Commit d83e1a9

Browse files
jan-kozinskiJan Kozinski
and
Jan Kozinski
authored
fix(AIRMap): support iOS MapKit zoomConstraints for better zoom handling especially for 3d maps (react-native-maps#4905)
* fix(AIRMap): include rotation in zoom calculations * fix(AIRMapManager): fix zoom constrains feature * fix(MapView): add flyover maps to MapView types * docs(MapView): format docs & add flyover maps * fix(AIRMapManager): fix flyover maps zoom constrains * fix(AIRMap): optimize camera zoom range assignment * fix(AIRMap): remove private API use * fix(AIRMap): remove NSLogs * fix(AIRMap): change affected map types * fix(AIRMap): add cameraZoomRange property to mapView * docs(AIRMap): add documentation for cameraZoomRange property * fix(AIRMap): remove unused variable * fix(AIRMap): move conditional statement to another function * fix(AIRMap): fix typo * fix(AIRMap): remove redundant assertion * fix(AIRMap): change variable name * fix(AIRMap): change variable name --------- Co-authored-by: Jan Kozinski <[email protected]>
1 parent 4f7c3bd commit d83e1a9

File tree

6 files changed

+166
-72
lines changed

6 files changed

+166
-72
lines changed

docs/mapview.md

+57-48
Large diffs are not rendered by default.

ios/AirMaps/AIRMap.h

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ extern const NSInteger AIRMapMaxZoomLevel;
3333
@property (nonatomic, assign) BOOL hasStartedRendering;
3434
@property (nonatomic, assign) BOOL cacheEnabled;
3535
@property (nonatomic, assign) BOOL loadingEnabled;
36+
@property (nonatomic, assign) BOOL legacyZoomConstraintsEnabled;
3637
@property (nonatomic, strong) UIColor *loadingBackgroundColor;
3738
@property (nonatomic, strong) UIColor *loadingIndicatorColor;
3839
@property (nonatomic, assign) BOOL hasShownInitialLoading;
@@ -64,13 +65,15 @@ extern const NSInteger AIRMapMaxZoomLevel;
6465
@property (nonatomic, copy) RCTDirectEventBlock onMarkerDragStart;
6566
@property (nonatomic, copy) RCTDirectEventBlock onMarkerDrag;
6667
@property (nonatomic, copy) RCTDirectEventBlock onMarkerDragEnd;
68+
6769
@property (nonatomic, copy) RCTDirectEventBlock onCalloutPress;
6870
@property (nonatomic, copy) RCTDirectEventBlock onRegionChange;
6971
@property (nonatomic, copy) RCTBubblingEventBlock onUserLocationChange;
7072

7173
- (void)cacheViewIfNeeded;
7274
- (void)beginLoading;
7375
- (void)finishLoading;
76+
- (double)getZoomLevel;
7477
- (NSArray *)getMapBoundaries;
7578

7679
- (AIRMapMarker*) markerAtPoint:(CGPoint)point;

ios/AirMaps/AIRMap.m

+60
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ - (instancetype)init
8686
self.minZoomLevel = 0;
8787
self.maxZoomLevel = AIRMapMaxZoomLevel;
8888
self.compassOffset = CGPointMake(0, 0);
89+
self.legacyZoomConstraintsEnabled = YES;
8990
}
9091
return self;
9192
}
@@ -466,6 +467,43 @@ - (void)setLoadingIndicatorColor:(UIColor *)loadingIndicatorColor {
466467
self.activityIndicatorView.color = loadingIndicatorColor;
467468
}
468469

470+
- (void)setCameraZoomRange:(NSDictionary *)cameraZoomRange {
471+
if (!@available(iOS 13.0, *)) {
472+
return;
473+
}
474+
475+
if (cameraZoomRange == nil) {
476+
cameraZoomRange = @{};
477+
}
478+
479+
NSNumber *minValue = cameraZoomRange[@"minCenterCoordinateDistance"];
480+
NSNumber *maxValue = cameraZoomRange[@"maxCenterCoordinateDistance"];
481+
482+
if (minValue == nil && maxValue == nil) {
483+
self.legacyZoomConstraintsEnabled = YES;
484+
485+
MKMapCameraZoomRange *defaultZoomRange = [[MKMapCameraZoomRange alloc] initWithMinCenterCoordinateDistance:MKMapCameraZoomDefault maxCenterCoordinateDistance:MKMapCameraZoomDefault];
486+
[super setCameraZoomRange:defaultZoomRange animated:NO];
487+
488+
return;
489+
}
490+
491+
MKMapCameraZoomRange *zoomRange = nil;
492+
493+
if (minValue != nil && maxValue != nil) {
494+
zoomRange = [[MKMapCameraZoomRange alloc] initWithMinCenterCoordinateDistance:[minValue doubleValue] maxCenterCoordinateDistance:[maxValue doubleValue]];
495+
} else if (minValue != nil) {
496+
zoomRange = [[MKMapCameraZoomRange alloc] initWithMinCenterCoordinateDistance:[minValue doubleValue]];
497+
} else if (maxValue != nil) {
498+
zoomRange = [[MKMapCameraZoomRange alloc] initWithMaxCenterCoordinateDistance:[maxValue doubleValue]];
499+
}
500+
501+
BOOL animated = [cameraZoomRange[@"animated"] boolValue];
502+
503+
self.legacyZoomConstraintsEnabled = NO;
504+
[super setCameraZoomRange:zoomRange animated:animated];
505+
}
506+
469507
// Include properties of MKMapView which are only available on iOS 9+
470508
// and check if their selector is available before calling super method.
471509

@@ -675,4 +713,26 @@ - (void)layoutSubviews {
675713
}
676714
}
677715

716+
// based on https://medium.com/@dmytrobabych/getting-actual-rotation-and-zoom-level-for-mapkit-mkmapview-e7f03f430aa9
717+
- (CGFloat)getZoomLevel {
718+
CGFloat cameraAngle = self.camera.heading;
719+
720+
if (cameraAngle > 270) {
721+
cameraAngle = 360 - cameraAngle;
722+
} else if (cameraAngle > 90) {
723+
cameraAngle = fabs(cameraAngle - 180);
724+
}
725+
726+
CGFloat angleRad = M_PI * cameraAngle / 180; // map rotation in radians
727+
CGFloat width = self.frame.size.width;
728+
CGFloat height = self.frame.size.height;
729+
CGFloat heightOffset = 20; // the offset (status bar height) which is taken by MapKit into consideration to calculate visible area height
730+
731+
// calculating Longitude span corresponding to normal (non-rotated) width
732+
CGFloat spanStraight = width * self.region.span.longitudeDelta / (width * cos(angleRad) + (height - heightOffset) * sin(angleRad));
733+
int normalizingFactor = 512;
734+
735+
return log2(360 * ((width / normalizingFactor) / spanStraight));
736+
}
737+
678738
@end

ios/AirMaps/AIRMapManager.m

+19-22
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ - (UIView *)view
110110
RCT_EXPORT_VIEW_PROPERTY(legalLabelInsets, UIEdgeInsets)
111111
RCT_EXPORT_VIEW_PROPERTY(mapPadding, UIEdgeInsets)
112112
RCT_EXPORT_VIEW_PROPERTY(mapType, MKMapType)
113+
RCT_EXPORT_VIEW_PROPERTY(cameraZoomRange, NSDictionary)
113114
RCT_EXPORT_VIEW_PROPERTY(onMapReady, RCTBubblingEventBlock)
114115
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
115116
RCT_EXPORT_VIEW_PROPERTY(onPanDrag, RCTBubblingEventBlock)
@@ -941,19 +942,14 @@ - (void)mapViewDidChangeVisibleRegion:(AIRMap *)mapView
941942

942943
- (void)mapView:(AIRMap *)mapView regionDidChangeAnimated:(__unused BOOL)animated
943944
{
944-
CGFloat zoomLevel = [self zoomLevel:mapView];
945-
946945
// Don't send region did change events until map has
947946
// started rendering, as these won't represent the final location
948947
if(mapView.hasStartedRendering){
949948
[self _regionChanged:mapView];
950949
}
951950

952-
if (zoomLevel < mapView.minZoomLevel) {
953-
[self setCenterCoordinate:[mapView centerCoordinate] zoomLevel:mapView.minZoomLevel animated:TRUE mapView:mapView];
954-
}
955-
else if (zoomLevel > mapView.maxZoomLevel) {
956-
[self setCenterCoordinate:[mapView centerCoordinate] zoomLevel:mapView.maxZoomLevel animated:TRUE mapView:mapView];
951+
if (mapView.legacyZoomConstraintsEnabled == YES) {
952+
[self applyLegacyZoomConstrains:mapView];
957953
}
958954

959955
// Don't send region did change events until map has
@@ -1165,6 +1161,22 @@ - (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
11651161
[mapView setRegion:region animated:animated];
11661162
}
11671163

1164+
-(void)applyLegacyZoomConstrains:(AIRMap *)mapView {
1165+
// flyover maps don't use mercator projection so we can't calculate their zoom level.
1166+
if (mapView.mapType == MKMapTypeHybridFlyover || mapView.mapType == MKMapTypeSatelliteFlyover) {
1167+
return;
1168+
}
1169+
1170+
CGFloat zoomLevel = [mapView getZoomLevel];
1171+
1172+
if (zoomLevel < mapView.minZoomLevel) {
1173+
[self setCenterCoordinate:[mapView centerCoordinate] zoomLevel:mapView.minZoomLevel animated:TRUE mapView:mapView];
1174+
}
1175+
else if (zoomLevel > mapView.maxZoomLevel) {
1176+
[self setCenterCoordinate:[mapView centerCoordinate] zoomLevel:mapView.maxZoomLevel animated:TRUE mapView:mapView];
1177+
}
1178+
}
1179+
11681180
//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).
11691181
-(MKCoordinateRegion)coordinateRegionWithMapView:(AIRMap *)mapView
11701182
centerCoordinate:(CLLocationCoordinate2D)centerCoordinate
@@ -1223,21 +1235,6 @@ -(MKCoordinateRegion)coordinateRegionWithMapView:(AIRMap *)mapView
12231235
return region;
12241236
}
12251237

1226-
- (double) zoomLevel:(AIRMap *)mapView {
1227-
MKCoordinateRegion region = mapView.region;
1228-
1229-
double centerPixelX = [AIRMapManager longitudeToPixelSpaceX: region.center.longitude];
1230-
double topLeftPixelX = [AIRMapManager longitudeToPixelSpaceX: region.center.longitude - region.span.longitudeDelta / 2];
1231-
1232-
double scaledMapWidth = (centerPixelX - topLeftPixelX) * 2;
1233-
CGSize mapSizeInPixels = mapView.bounds.size;
1234-
double zoomScale = scaledMapWidth / mapSizeInPixels.width;
1235-
double zoomExponent = log(zoomScale) / log(2);
1236-
double zoomLevel = AIRMapMaxZoomLevel - zoomExponent;
1237-
1238-
return zoomLevel;
1239-
}
1240-
12411238
#pragma mark MKMapViewDelegate - Tracking the User Location
12421239

12431240
- (void)mapView:(AIRMap *)mapView didFailToLocateUserWithError:(NSError *)error {

src/MapView.tsx

+16-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
Address,
3737
BoundingBox,
3838
Camera,
39+
CameraZoomRange,
3940
ChangeEvent,
4041
Details,
4142
EdgePadding,
@@ -64,6 +65,8 @@ export const MAP_TYPES: MapTypes = {
6465
TERRAIN: 'terrain',
6566
NONE: 'none',
6667
MUTEDSTANDARD: 'mutedStandard',
68+
SATELLITE_FLYOVER: 'satelliteFlyover',
69+
HYBRID_FLYOVER: 'hybridFlyover',
6770
};
6871

6972
const GOOGLE_MAPS_ONLY_TYPES: MapType[] = [MAP_TYPES.TERRAIN, MAP_TYPES.NONE];
@@ -213,7 +216,7 @@ export type MapViewProps = ViewProps & {
213216
* The map type to be displayed
214217
*
215218
* @default `standard`
216-
* @platform iOS: hybrid | mutedStandard | satellite | standard | terrain
219+
* @platform iOS: hybrid | mutedStandard | satellite | standard | terrain | hybridFlyover | satelliteFlyover
217220
* @platform Android: hybrid | none | satellite | standard | terrain
218221
*/
219222
mapType?: MapType;
@@ -232,6 +235,7 @@ export type MapViewProps = ViewProps & {
232235
* @default 20
233236
* @platform iOS: Supported
234237
* @platform Android: Supported
238+
* @deprecated on Apple Maps, use `cameraZoomRange` instead
235239
*/
236240
maxZoomLevel?: number;
237241

@@ -249,6 +253,7 @@ export type MapViewProps = ViewProps & {
249253
* @default 0
250254
* @platform iOS: Supported
251255
* @platform Android: Supported
256+
* @deprecated on Apple Maps, use `cameraZoomRange` instead
252257
*/
253258
minZoomLevel?: number;
254259

@@ -690,6 +695,16 @@ export type MapViewProps = ViewProps & {
690695
* @platform Android: Not supported
691696
*/
692697
zoomTapEnabled?: boolean;
698+
699+
/**
700+
* Map camera distance limits. `minCenterCoordinateDistance` for minimum distance, `maxCenterCoordinateDistance` for maximum.
701+
* `animated` for animated zoom changes.
702+
* Takes precedence if conflicting with `minZoomLevel`, `maxZoomLevel`.
703+
*
704+
* @platform iOS: 13.0+
705+
* @platform Android: Not supported
706+
*/
707+
cameraZoomRange?: CameraZoomRange;
693708
};
694709

695710
type ModifiedProps = Modify<

src/MapView.types.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ export type MapType =
3838
| 'none'
3939
| 'satellite'
4040
| 'standard'
41-
| 'terrain';
41+
| 'terrain'
42+
| 'satelliteFlyover'
43+
| 'hybridFlyover';
4244

4345
export type MapTypes = {
4446
STANDARD: 'standard';
@@ -47,6 +49,8 @@ export type MapTypes = {
4749
TERRAIN: 'terrain';
4850
NONE: 'none';
4951
MUTEDSTANDARD: 'mutedStandard';
52+
SATELLITE_FLYOVER: 'satelliteFlyover';
53+
HYBRID_FLYOVER: 'hybridFlyover';
5054
};
5155

5256
export type IndoorLevel = {
@@ -184,6 +188,12 @@ export type Address = {
184188
subThoroughfare?: string;
185189
};
186190

191+
export type CameraZoomRange = {
192+
minCenterCoordinateDistance?: number;
193+
maxCenterCoordinateDistance?: number;
194+
animated?: boolean;
195+
};
196+
187197
export type NativeCommandName =
188198
| 'animateCamera'
189199
| 'animateToRegion'

0 commit comments

Comments
 (0)