366 lines
12 KiB
Mathematica
366 lines
12 KiB
Mathematica
|
//
|
||
|
// AIRGoogleMap.m
|
||
|
// AirMaps
|
||
|
//
|
||
|
// Created by Gil Birman on 9/1/16.
|
||
|
//
|
||
|
|
||
|
#import "AIRGoogleMap.h"
|
||
|
#import "AIRGoogleMapMarker.h"
|
||
|
#import "AIRGoogleMapPolygon.h"
|
||
|
#import "AIRGoogleMapPolyline.h"
|
||
|
#import "AIRGoogleMapCircle.h"
|
||
|
#import "AIRGoogleMapUrlTile.h"
|
||
|
#import <GoogleMaps/GoogleMaps.h>
|
||
|
#import <MapKit/MapKit.h>
|
||
|
#import <React/UIView+React.h>
|
||
|
#import "RCTConvert+AirMap.h"
|
||
|
|
||
|
id regionAsJSON(MKCoordinateRegion region) {
|
||
|
return @{
|
||
|
@"latitude": [NSNumber numberWithDouble:region.center.latitude],
|
||
|
@"longitude": [NSNumber numberWithDouble:region.center.longitude],
|
||
|
@"latitudeDelta": [NSNumber numberWithDouble:region.span.latitudeDelta],
|
||
|
@"longitudeDelta": [NSNumber numberWithDouble:region.span.longitudeDelta],
|
||
|
};
|
||
|
}
|
||
|
|
||
|
@interface AIRGoogleMap ()
|
||
|
|
||
|
- (id)eventFromCoordinate:(CLLocationCoordinate2D)coordinate;
|
||
|
|
||
|
@end
|
||
|
|
||
|
@implementation AIRGoogleMap
|
||
|
{
|
||
|
NSMutableArray<UIView *> *_reactSubviews;
|
||
|
BOOL _initialRegionSet;
|
||
|
}
|
||
|
|
||
|
- (instancetype)init
|
||
|
{
|
||
|
if ((self = [super init])) {
|
||
|
_reactSubviews = [NSMutableArray new];
|
||
|
_markers = [NSMutableArray array];
|
||
|
_polygons = [NSMutableArray array];
|
||
|
_polylines = [NSMutableArray array];
|
||
|
_circles = [NSMutableArray array];
|
||
|
_tiles = [NSMutableArray array];
|
||
|
_initialRegionSet = false;
|
||
|
}
|
||
|
return self;
|
||
|
}
|
||
|
- (id)eventFromCoordinate:(CLLocationCoordinate2D)coordinate {
|
||
|
|
||
|
CGPoint touchPoint = [self.projection pointForCoordinate:coordinate];
|
||
|
|
||
|
return @{
|
||
|
@"coordinate": @{
|
||
|
@"latitude": @(coordinate.latitude),
|
||
|
@"longitude": @(coordinate.longitude),
|
||
|
},
|
||
|
@"position": @{
|
||
|
@"x": @(touchPoint.x),
|
||
|
@"y": @(touchPoint.y),
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
|
||
|
#pragma clang diagnostic push
|
||
|
#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
|
||
|
- (void)insertReactSubview:(id<RCTComponent>)subview atIndex:(NSInteger)atIndex {
|
||
|
// Our desired API is to pass up markers/overlays as children to the mapview component.
|
||
|
// This is where we intercept them and do the appropriate underlying mapview action.
|
||
|
if ([subview isKindOfClass:[AIRGoogleMapMarker class]]) {
|
||
|
AIRGoogleMapMarker *marker = (AIRGoogleMapMarker*)subview;
|
||
|
marker.realMarker.map = self;
|
||
|
[self.markers addObject:marker];
|
||
|
} else if ([subview isKindOfClass:[AIRGoogleMapPolygon class]]) {
|
||
|
AIRGoogleMapPolygon *polygon = (AIRGoogleMapPolygon*)subview;
|
||
|
polygon.polygon.map = self;
|
||
|
[self.polygons addObject:polygon];
|
||
|
} else if ([subview isKindOfClass:[AIRGoogleMapPolyline class]]) {
|
||
|
AIRGoogleMapPolyline *polyline = (AIRGoogleMapPolyline*)subview;
|
||
|
polyline.polyline.map = self;
|
||
|
[self.polylines addObject:polyline];
|
||
|
} else if ([subview isKindOfClass:[AIRGoogleMapCircle class]]) {
|
||
|
AIRGoogleMapCircle *circle = (AIRGoogleMapCircle*)subview;
|
||
|
circle.circle.map = self;
|
||
|
[self.circles addObject:circle];
|
||
|
} else if ([subview isKindOfClass:[AIRGoogleMapUrlTile class]]) {
|
||
|
AIRGoogleMapUrlTile *tile = (AIRGoogleMapUrlTile*)subview;
|
||
|
tile.tileLayer.map = self;
|
||
|
[self.tiles addObject:tile];
|
||
|
} else {
|
||
|
NSArray<id<RCTComponent>> *childSubviews = [subview reactSubviews];
|
||
|
for (int i = 0; i < childSubviews.count; i++) {
|
||
|
[self insertReactSubview:(UIView *)childSubviews[i] atIndex:atIndex];
|
||
|
}
|
||
|
}
|
||
|
[_reactSubviews insertObject:(UIView *)subview atIndex:(NSUInteger) atIndex];
|
||
|
}
|
||
|
#pragma clang diagnostic pop
|
||
|
|
||
|
|
||
|
#pragma clang diagnostic push
|
||
|
#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
|
||
|
- (void)removeReactSubview:(id<RCTComponent>)subview {
|
||
|
// similarly, when the children are being removed we have to do the appropriate
|
||
|
// underlying mapview action here.
|
||
|
if ([subview isKindOfClass:[AIRGoogleMapMarker class]]) {
|
||
|
AIRGoogleMapMarker *marker = (AIRGoogleMapMarker*)subview;
|
||
|
marker.realMarker.map = nil;
|
||
|
[self.markers removeObject:marker];
|
||
|
} else if ([subview isKindOfClass:[AIRGoogleMapPolygon class]]) {
|
||
|
AIRGoogleMapPolygon *polygon = (AIRGoogleMapPolygon*)subview;
|
||
|
polygon.polygon.map = nil;
|
||
|
[self.polygons removeObject:polygon];
|
||
|
} else if ([subview isKindOfClass:[AIRGoogleMapPolyline class]]) {
|
||
|
AIRGoogleMapPolyline *polyline = (AIRGoogleMapPolyline*)subview;
|
||
|
polyline.polyline.map = nil;
|
||
|
[self.polylines removeObject:polyline];
|
||
|
} else if ([subview isKindOfClass:[AIRGoogleMapCircle class]]) {
|
||
|
AIRGoogleMapCircle *circle = (AIRGoogleMapCircle*)subview;
|
||
|
circle.circle.map = nil;
|
||
|
[self.circles removeObject:circle];
|
||
|
} else if ([subview isKindOfClass:[AIRGoogleMapUrlTile class]]) {
|
||
|
AIRGoogleMapUrlTile *tile = (AIRGoogleMapUrlTile*)subview;
|
||
|
tile.tileLayer.map = nil;
|
||
|
[self.tiles removeObject:tile];
|
||
|
} else {
|
||
|
NSArray<id<RCTComponent>> *childSubviews = [subview reactSubviews];
|
||
|
for (int i = 0; i < childSubviews.count; i++) {
|
||
|
[self removeReactSubview:(UIView *)childSubviews[i]];
|
||
|
}
|
||
|
}
|
||
|
[_reactSubviews removeObject:(UIView *)subview];
|
||
|
}
|
||
|
#pragma clang diagnostic pop
|
||
|
|
||
|
#pragma clang diagnostic push
|
||
|
#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
|
||
|
- (NSArray<id<RCTComponent>> *)reactSubviews {
|
||
|
return _reactSubviews;
|
||
|
}
|
||
|
#pragma clang diagnostic pop
|
||
|
|
||
|
- (void)setInitialRegion:(MKCoordinateRegion)initialRegion {
|
||
|
if (_initialRegionSet) return;
|
||
|
_initialRegionSet = true;
|
||
|
self.camera = [AIRGoogleMap makeGMSCameraPositionFromMap:self andMKCoordinateRegion:initialRegion];
|
||
|
}
|
||
|
|
||
|
- (void)setRegion:(MKCoordinateRegion)region {
|
||
|
// TODO: The JS component is repeatedly setting region unnecessarily. We might want to deal with that in here.
|
||
|
self.camera = [AIRGoogleMap makeGMSCameraPositionFromMap:self andMKCoordinateRegion:region];
|
||
|
}
|
||
|
|
||
|
- (void)didPrepareMap {
|
||
|
if (self.onMapReady) self.onMapReady(@{});
|
||
|
}
|
||
|
|
||
|
- (BOOL)didTapMarker:(GMSMarker *)marker {
|
||
|
AIRGMSMarker *airMarker = (AIRGMSMarker *)marker;
|
||
|
|
||
|
id event = @{@"action": @"marker-press",
|
||
|
@"id": airMarker.identifier ?: @"unknown",
|
||
|
};
|
||
|
|
||
|
if (airMarker.onPress) airMarker.onPress(event);
|
||
|
if (self.onMarkerPress) self.onMarkerPress(event);
|
||
|
|
||
|
// TODO: not sure why this is necessary
|
||
|
[self setSelectedMarker:marker];
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
- (void)didTapPolyline:(GMSOverlay *)polyline {
|
||
|
AIRGMSPolyline *airPolyline = (AIRGMSPolyline *)polyline;
|
||
|
|
||
|
id event = @{@"action": @"polyline-press",
|
||
|
@"id": airPolyline.identifier ?: @"unknown",
|
||
|
};
|
||
|
|
||
|
if (airPolyline.onPress) airPolyline.onPress(event);
|
||
|
}
|
||
|
|
||
|
- (void)didTapPolygon:(GMSOverlay *)polygon {
|
||
|
AIRGMSPolygon *airPolygon = (AIRGMSPolygon *)polygon;
|
||
|
|
||
|
id event = @{@"action": @"polygon-press",
|
||
|
@"id": airPolygon.identifier ?: @"unknown",
|
||
|
};
|
||
|
|
||
|
if (airPolygon.onPress) airPolygon.onPress(event);
|
||
|
}
|
||
|
|
||
|
- (void)didTapAtCoordinate:(CLLocationCoordinate2D)coordinate {
|
||
|
if (!self.onPress) return;
|
||
|
self.onPress([self eventFromCoordinate:coordinate]);
|
||
|
}
|
||
|
|
||
|
- (void)didLongPressAtCoordinate:(CLLocationCoordinate2D)coordinate {
|
||
|
if (!self.onLongPress) return;
|
||
|
self.onLongPress([self eventFromCoordinate:coordinate]);
|
||
|
}
|
||
|
|
||
|
- (void)didChangeCameraPosition:(GMSCameraPosition *)position {
|
||
|
id event = @{@"continuous": @YES,
|
||
|
@"region": regionAsJSON([AIRGoogleMap makeGMSCameraPositionFromMap:self andGMSCameraPosition:position]),
|
||
|
};
|
||
|
|
||
|
if (self.onChange) self.onChange(event);
|
||
|
}
|
||
|
|
||
|
- (void)idleAtCameraPosition:(GMSCameraPosition *)position {
|
||
|
id event = @{@"continuous": @NO,
|
||
|
@"region": regionAsJSON([AIRGoogleMap makeGMSCameraPositionFromMap:self andGMSCameraPosition:position]),
|
||
|
};
|
||
|
if (self.onChange) self.onChange(event); // complete
|
||
|
}
|
||
|
|
||
|
- (void)setMapPadding:(UIEdgeInsets)mapPadding {
|
||
|
self.padding = mapPadding;
|
||
|
}
|
||
|
|
||
|
- (UIEdgeInsets)mapPadding {
|
||
|
return self.padding;
|
||
|
}
|
||
|
|
||
|
- (void)setScrollEnabled:(BOOL)scrollEnabled {
|
||
|
self.settings.scrollGestures = scrollEnabled;
|
||
|
}
|
||
|
|
||
|
- (BOOL)scrollEnabled {
|
||
|
return self.settings.scrollGestures;
|
||
|
}
|
||
|
|
||
|
- (void)setZoomEnabled:(BOOL)zoomEnabled {
|
||
|
self.settings.zoomGestures = zoomEnabled;
|
||
|
}
|
||
|
|
||
|
- (BOOL)zoomEnabled {
|
||
|
return self.settings.zoomGestures;
|
||
|
}
|
||
|
|
||
|
- (void)setRotateEnabled:(BOOL)rotateEnabled {
|
||
|
self.settings.rotateGestures = rotateEnabled;
|
||
|
}
|
||
|
|
||
|
- (BOOL)rotateEnabled {
|
||
|
return self.settings.rotateGestures;
|
||
|
}
|
||
|
|
||
|
- (void)setPitchEnabled:(BOOL)pitchEnabled {
|
||
|
self.settings.tiltGestures = pitchEnabled;
|
||
|
}
|
||
|
|
||
|
- (BOOL)pitchEnabled {
|
||
|
return self.settings.tiltGestures;
|
||
|
}
|
||
|
|
||
|
- (void)setShowsTraffic:(BOOL)showsTraffic {
|
||
|
self.trafficEnabled = showsTraffic;
|
||
|
}
|
||
|
|
||
|
- (BOOL)showsTraffic {
|
||
|
return self.trafficEnabled;
|
||
|
}
|
||
|
|
||
|
- (void)setShowsBuildings:(BOOL)showsBuildings {
|
||
|
self.buildingsEnabled = showsBuildings;
|
||
|
}
|
||
|
|
||
|
- (BOOL)showsBuildings {
|
||
|
return self.buildingsEnabled;
|
||
|
}
|
||
|
|
||
|
- (void)setShowsCompass:(BOOL)showsCompass {
|
||
|
self.settings.compassButton = showsCompass;
|
||
|
}
|
||
|
|
||
|
- (void)setCustomMapStyleString:(NSString *)customMapStyleString {
|
||
|
NSError *error;
|
||
|
|
||
|
GMSMapStyle *style = [GMSMapStyle styleWithJSONString:customMapStyleString error:&error];
|
||
|
|
||
|
if (!style) {
|
||
|
NSLog(@"The style definition could not be loaded: %@", error);
|
||
|
}
|
||
|
|
||
|
self.mapStyle = style;
|
||
|
}
|
||
|
|
||
|
- (BOOL)showsCompass {
|
||
|
return self.settings.compassButton;
|
||
|
}
|
||
|
|
||
|
- (void)setShowsUserLocation:(BOOL)showsUserLocation {
|
||
|
self.myLocationEnabled = showsUserLocation;
|
||
|
}
|
||
|
|
||
|
- (BOOL)showsUserLocation {
|
||
|
return self.myLocationEnabled;
|
||
|
}
|
||
|
|
||
|
- (void)setShowsMyLocationButton:(BOOL)showsMyLocationButton {
|
||
|
self.settings.myLocationButton = showsMyLocationButton;
|
||
|
}
|
||
|
|
||
|
- (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 ];
|
||
|
}
|
||
|
|
||
|
- (void)setShowsIndoorLevelPicker:(BOOL)showsIndoorLevelPicker {
|
||
|
self.settings.indoorPicker = showsIndoorLevelPicker;
|
||
|
}
|
||
|
|
||
|
- (BOOL)showsIndoorLevelPicker {
|
||
|
return self.settings.indoorPicker;
|
||
|
}
|
||
|
|
||
|
+ (MKCoordinateRegion) makeGMSCameraPositionFromMap:(GMSMapView *)map andGMSCameraPosition:(GMSCameraPosition *)position {
|
||
|
// solution from here: http://stackoverflow.com/a/16587735/1102215
|
||
|
GMSVisibleRegion visibleRegion = map.projection.visibleRegion;
|
||
|
GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithRegion: visibleRegion];
|
||
|
CLLocationCoordinate2D center;
|
||
|
CLLocationDegrees longitudeDelta;
|
||
|
CLLocationDegrees latitudeDelta = bounds.northEast.latitude - bounds.southWest.latitude;
|
||
|
|
||
|
if(bounds.northEast.longitude >= bounds.southWest.longitude) {
|
||
|
//Standard case
|
||
|
center = CLLocationCoordinate2DMake((bounds.southWest.latitude + bounds.northEast.latitude) / 2,
|
||
|
(bounds.southWest.longitude + bounds.northEast.longitude) / 2);
|
||
|
longitudeDelta = bounds.northEast.longitude - bounds.southWest.longitude;
|
||
|
} else {
|
||
|
//Region spans the international dateline
|
||
|
center = CLLocationCoordinate2DMake((bounds.southWest.latitude + bounds.northEast.latitude) / 2,
|
||
|
(bounds.southWest.longitude + bounds.northEast.longitude + 360) / 2);
|
||
|
longitudeDelta = bounds.northEast.longitude + 360 - bounds.southWest.longitude;
|
||
|
}
|
||
|
MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
|
||
|
return MKCoordinateRegionMake(center, span);
|
||
|
}
|
||
|
|
||
|
+ (GMSCameraPosition*) makeGMSCameraPositionFromMap:(GMSMapView *)map andMKCoordinateRegion:(MKCoordinateRegion)region {
|
||
|
float latitudeDelta = region.span.latitudeDelta * 0.5;
|
||
|
float longitudeDelta = region.span.longitudeDelta * 0.5;
|
||
|
|
||
|
CLLocationCoordinate2D a = CLLocationCoordinate2DMake(region.center.latitude + latitudeDelta,
|
||
|
region.center.longitude + longitudeDelta);
|
||
|
CLLocationCoordinate2D b = CLLocationCoordinate2DMake(region.center.latitude - latitudeDelta,
|
||
|
region.center.longitude - longitudeDelta);
|
||
|
GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithCoordinate:a coordinate:b];
|
||
|
return [map cameraForBounds:bounds insets:UIEdgeInsetsZero];
|
||
|
}
|
||
|
|
||
|
@end
|