feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,90 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import "SMCalloutView.h"
#import "RCTConvert+AirMap.h"
#import "AIRMapCalloutSubview.h"
@class AIRMapCoordinate;
@class AIRMapMarker;
extern const NSTimeInterval AIRMapRegionChangeObserveInterval;
extern const CGFloat AIRMapZoomBoundBuffer;
extern const NSInteger AIRMapMaxZoomLevel;
@interface AIRMap: MKMapView<SMCalloutViewDelegate>
@property (nonatomic, strong) SMCalloutView *calloutView;
@property (nonatomic, strong) UIImageView *cacheImageView;
@property (nonatomic, strong) UIView *loadingView;
@property (nonatomic, copy) NSString *userLocationAnnotationTitle;
@property (nonatomic, assign) BOOL followsUserLocation;
@property (nonatomic, assign) BOOL userLocationCalloutEnabled;
@property (nonatomic, assign) BOOL hasStartedRendering;
@property (nonatomic, assign) BOOL cacheEnabled;
@property (nonatomic, assign) BOOL loadingEnabled;
@property (nonatomic, assign) BOOL handlePanDrag;
@property (nonatomic, assign) BOOL legacyZoomConstraintsEnabled;
@property (nonatomic, strong) UIColor *loadingBackgroundColor;
@property (nonatomic, strong) UIColor *loadingIndicatorColor;
@property (nonatomic, assign) BOOL hasShownInitialLoading;
@property (nonatomic, assign) CGFloat minDelta;
@property (nonatomic, assign) CGFloat maxDelta;
@property (nonatomic, assign) UIEdgeInsets legalLabelInsets;
@property (nonatomic, assign) UIEdgeInsets appleLogoInsets;
@property (nonatomic, assign) MKCoordinateRegion initialRegion;
@property (nonatomic, retain) MKMapCamera *initialCamera;
@property (nonatomic, assign) CGFloat minZoom;
@property (nonatomic, assign) CGFloat maxZoom;
@property (nonatomic, assign) CGPoint compassOffset;
@property (nonatomic, assign) UIEdgeInsets mapPadding;
@property (nonatomic, assign) BOOL showsPointsOfInterests;
@property (nonatomic, copy) NSArray<NSString *> *pointsOfInterestFilter;
@property (nonatomic, assign) CLLocationCoordinate2D pendingCenter;
@property (nonatomic, assign) MKCoordinateSpan pendingSpan;
@property (nonatomic, assign) BOOL ignoreRegionChanges;
@property (nonatomic, copy) RCTDirectEventBlock onMapReady;
@property (nonatomic, copy) RCTDirectEventBlock onRegionChangeStart;
@property (nonatomic, copy) RCTDirectEventBlock onRegionChange;
@property (nonatomic, copy) RCTDirectEventBlock onRegionChangeComplete;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@property (nonatomic, copy) RCTBubblingEventBlock onLongPress;
@property (nonatomic, copy) RCTDirectEventBlock onPanDrag;
@property (nonatomic, copy) RCTDirectEventBlock onDoublePress;
@property (nonatomic, copy) RCTDirectEventBlock onMarkerPress;
@property (nonatomic, copy) RCTDirectEventBlock onMarkerSelect;
@property (nonatomic, copy) RCTDirectEventBlock onMarkerDeselect;
@property (nonatomic, copy) RCTDirectEventBlock onMarkerDragStart;
@property (nonatomic, copy) RCTDirectEventBlock onMarkerDrag;
@property (nonatomic, copy) RCTDirectEventBlock onMarkerDragEnd;
@property (nonatomic, copy) RCTDirectEventBlock onCalloutPress;
@property (nonatomic, copy) RCTDirectEventBlock onUserLocationChange;
- (void)cacheViewIfNeeded;
- (void)beginLoading;
- (void)finishLoading;
- (double)getZoomLevel;
- (NSDictionary *)getMapBoundaries;
- (AIRMapMarker*) markerAtPoint:(CGPoint)point;
- (NSDictionary*) getMarkersFramesWithOnlyVisible:(BOOL)onlyVisible;
- (void)insertReactSubview:(id<RCTComponent>)subview atIndex:(NSInteger)atIndex;
-(void) fitToCoordinates:(NSArray<AIRMapCoordinate*>*) coordinates edgePadding:(UIEdgeInsets) edgeInsets animated:(Boolean) animated;
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <React/RCTView.h>
@interface AIRMapCallout : RCTView
@property (nonatomic, assign) BOOL tooltip;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@property (nonatomic, assign) BOOL alphaHitTest;
- (BOOL) isPointInside:(CGPoint)pointInCallout;
@end

View File

@@ -0,0 +1,30 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import "AIRMapCallout.h"
@implementation AIRMapCallout
- (BOOL) isPointInside:(CGPoint)pointInCallout {
if (!self.alphaHitTest)
return TRUE;
CGFloat alpha = [self alphaOfPoint:pointInCallout];
return alpha >= 0.01;
}
- (CGFloat) alphaOfPoint:(CGPoint)point {
unsigned char pixel[4] = {0};
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(pixel, 1, 1, 8, 4, colorSpace, kCGBitmapAlphaInfoMask & kCGImageAlphaPremultipliedLast);
CGContextTranslateCTM(context, -point.x, -point.y);
[self.layer renderInContext:context];
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
return pixel[3]/255.0;
}
@end

View File

@@ -0,0 +1,10 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface AIRMapCalloutManager : RCTViewManager
@end

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMapCalloutManager.h"
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "AIRMapMarker.h"
#import "AIRMapCallout.h"
@interface AIRMapCalloutManager()
@end
@implementation AIRMapCalloutManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
return [AIRMapCallout new];
}
RCT_EXPORT_VIEW_PROPERTY(tooltip, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(alphaHitTest, BOOL)
@end

View File

@@ -0,0 +1,15 @@
//
// AIRMapCalloutSubview.h
// AirMaps
//
// Created by Denis Oblogin on 10/8/18.
//
//
#import <UIKit/UIKit.h>
#import <React/RCTView.h>
@interface AIRMapCalloutSubview : UIView
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@end

View File

@@ -0,0 +1,15 @@
//
// AIRMapCalloutSubview.m
// AirMaps
//
// Created by Denis Oblogin on 10/8/18.
//
//
#import "AIRMapCalloutSubview.h"
#import <React/RCTUtils.h>
#import <React/RCTView.h>
#import <React/RCTBridge.h>
@implementation AIRMapCalloutSubview
@end

View File

@@ -0,0 +1,14 @@
//
// AIRMapCalloutSubviewManager.h
// AirMaps
//
// Created by Denis Oblogin on 10/8/18.
//
//
#import <React/RCTViewManager.h>
@interface AIRMapCalloutSubviewManager : RCTViewManager
@end

View File

@@ -0,0 +1,24 @@
//
// AIRMapCalloutSubviewManager.m
// AirMaps
//
// Created by Denis Oblogin on 10/8/18.
//
//
#import "AIRMapCalloutSubviewManager.h"
#import "AIRMapCalloutSubview.h"
#import <React/RCTView.h>
@implementation AIRMapCalloutSubviewManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapCalloutSubview *calloutSubview = [AIRMapCalloutSubview new];
return calloutSubview;
}
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
@end

View File

@@ -0,0 +1,44 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import <React/RCTView.h>
#import "AIRMapCoordinate.h"
#import "AIRMap.h"
#import "RCTConvert+AirMap.h"
@interface AIRMapCircle: MKAnnotationView <MKOverlay>
@property (nonatomic, weak) AIRMap *map;
@property (nonatomic, strong) MKCircle *circle;
@property (nonatomic, strong) MKCircleRenderer *renderer;
@property (nonatomic, assign) CLLocationCoordinate2D centerCoordinate;
@property (nonatomic, assign) CLLocationDistance radius;
@property (nonatomic, strong) UIColor *fillColor;
@property (nonatomic, strong) UIColor *strokeColor;
@property (nonatomic, assign) CGFloat strokeWidth;
@property (nonatomic, assign) CGFloat miterLimit;
@property (nonatomic, assign) CGLineCap lineCap;
@property (nonatomic, assign) CGLineJoin lineJoin;
@property (nonatomic, assign) CGFloat lineDashPhase;
@property (nonatomic, strong) NSArray <NSNumber *> *lineDashPattern;
#pragma mark MKOverlay protocol
@property(nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property(nonatomic, readonly) MKMapRect boundingMapRect;
- (BOOL)intersectsMapRect:(MKMapRect)mapRect;
- (BOOL)canReplaceMapContent;
@end

View File

@@ -0,0 +1,117 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import "AIRMapCircle.h"
#import <React/UIView+React.h>
@implementation AIRMapCircle {
BOOL _radiusSet;
BOOL _centerSet;
}
- (void)setFillColor:(UIColor *)fillColor {
_fillColor = fillColor;
[self update];
}
- (void)setStrokeColor:(UIColor *)strokeColor {
_strokeColor = strokeColor;
[self update];
}
- (void)setStrokeWidth:(CGFloat)strokeWidth {
_strokeWidth = strokeWidth;
[self update];
}
- (void)setLineJoin:(CGLineJoin)lineJoin {
_lineJoin = lineJoin;
[self update];
}
- (void)setLineCap:(CGLineCap)lineCap {
_lineCap = lineCap;
[self update];
}
- (void)setMiterLimit:(CGFloat)miterLimit {
_miterLimit = miterLimit;
[self update];
}
- (void)setLineDashPhase:(CGFloat)lineDashPhase {
_lineDashPhase = lineDashPhase;
[self update];
}
- (void)setLineDashPattern:(NSArray <NSNumber *> *)lineDashPattern {
_lineDashPattern = lineDashPattern;
[self update];
}
- (void)setRadius:(CLLocationDistance)radius {
_radius = radius;
_radiusSet = YES;
[self createCircleAndRendererIfPossible];
[self update];
}
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate{
_centerCoordinate = centerCoordinate;
_centerSet = YES;
[self createCircleAndRendererIfPossible];
[self update];
}
- (void) createCircleAndRendererIfPossible
{
if (!_centerSet || !_radiusSet) return;
self.circle = [MKCircle circleWithCenterCoordinate:_centerCoordinate radius:_radius];
self.renderer = [[MKCircleRenderer alloc] initWithCircle:self.circle];
}
- (void) update
{
if (!_renderer) return;
_renderer.fillColor = _fillColor;
_renderer.strokeColor = _strokeColor;
_renderer.lineWidth = _strokeWidth;
_renderer.lineCap = _lineCap;
_renderer.lineJoin = _lineJoin;
_renderer.miterLimit = _miterLimit;
_renderer.lineDashPhase = _lineDashPhase;
_renderer.lineDashPattern = _lineDashPattern;
if (_map == nil) return;
[_map removeOverlay:self];
[_map addOverlay:self];
}
#pragma mark MKOverlay implementation
- (CLLocationCoordinate2D) coordinate
{
return self.circle.coordinate;
}
- (MKMapRect) boundingMapRect
{
return self.circle.boundingMapRect;
}
- (BOOL)intersectsMapRect:(MKMapRect)mapRect
{
BOOL answer = [self.circle intersectsMapRect:mapRect];
return answer;
}
- (BOOL)canReplaceMapContent
{
return NO;
}
@end

View File

@@ -0,0 +1,10 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface AIRMapCircleManager : RCTViewManager
@end

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMapCircleManager.h"
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "AIRMapMarker.h"
#import "AIRMapCircle.h"
@interface AIRMapCircleManager()
@end
@implementation AIRMapCircleManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapCircle *circle = [AIRMapCircle new];
return circle;
}
RCT_REMAP_VIEW_PROPERTY(center, centerCoordinate, CLLocationCoordinate2D)
RCT_EXPORT_VIEW_PROPERTY(radius, CLLocationDistance)
RCT_EXPORT_VIEW_PROPERTY(fillColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(strokeColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineCap, CGLineCap)
RCT_EXPORT_VIEW_PROPERTY(lineJoin, CGLineJoin)
RCT_EXPORT_VIEW_PROPERTY(miterLimit, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineDashPhase, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineDashPattern, NSArray)
// NOTE(lmr):
// for now, onPress events for overlays will be left unimplemented. Seems it is possible with some work, but
// it is difficult to achieve in both ios and android so I decided to leave it out.
//RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
@end

View File

@@ -0,0 +1,13 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
@interface AIRMapCoordinate : NSObject
@property (nonatomic, assign) CLLocationCoordinate2D coordinate;
@end

View File

@@ -0,0 +1,10 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import "AIRMapCoordinate.h"
@implementation AIRMapCoordinate
@end

View File

@@ -0,0 +1,36 @@
//
// AIRMapLocalTile.h
// AirMaps
//
// Created by Peter Zavadsky on 01/12/2017.
// Copyright © 2017 Christopher. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import <React/RCTView.h>
#import "AIRMapCoordinate.h"
#import "AIRMap.h"
#import "RCTConvert+AirMap.h"
@interface AIRMapLocalTile : MKAnnotationView <MKOverlay>
@property (nonatomic, weak) AIRMap *map;
@property (nonatomic, strong) MKTileOverlay *tileOverlay;
@property (nonatomic, strong) MKTileOverlayRenderer *renderer;
@property (nonatomic, copy) NSString *pathTemplate;
@property (nonatomic, assign) CGFloat tileSize;
#pragma mark MKOverlay protocol
@property(nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property(nonatomic, readonly) MKMapRect boundingMapRect;
//- (BOOL)intersectsMapRect:(MKMapRect)mapRect;
- (BOOL)canReplaceMapContent;
@end

View File

@@ -0,0 +1,68 @@
//
// AIRMapLocalTile.m
// AirMaps
//
// Created by Peter Zavadsky on 01/12/2017.
// Copyright © 2017 Christopher. All rights reserved.
//
#import "AIRMapLocalTile.h"
#import <React/UIView+React.h>
#import "AIRMapLocalTileOverlay.h"
@implementation AIRMapLocalTile {
BOOL _pathTemplateSet;
BOOL _tileSizeSet;
}
- (void)setPathTemplate:(NSString *)pathTemplate{
_pathTemplate = pathTemplate;
_pathTemplateSet = YES;
[self createTileOverlayAndRendererIfPossible];
[self update];
}
- (void)setTileSize:(CGFloat)tileSize{
_tileSize = tileSize;
_tileSizeSet = YES;
[self createTileOverlayAndRendererIfPossible];
[self update];
}
- (void) createTileOverlayAndRendererIfPossible
{
if (!_pathTemplateSet || !_tileSizeSet) return;
self.tileOverlay = [[AIRMapLocalTileOverlay alloc] initWithURLTemplate:self.pathTemplate];
self.tileOverlay.canReplaceMapContent = YES;
self.tileOverlay.tileSize = CGSizeMake(_tileSize, _tileSize);
self.renderer = [[MKTileOverlayRenderer alloc] initWithTileOverlay:self.tileOverlay];
}
- (void) update
{
if (!_renderer) return;
if (_map == nil) return;
[_map removeOverlay:self];
[_map addOverlay:self level:MKOverlayLevelAboveLabels];
}
#pragma mark MKOverlay implementation
- (CLLocationCoordinate2D) coordinate
{
return self.tileOverlay.coordinate;
}
- (MKMapRect) boundingMapRect
{
return self.tileOverlay.boundingMapRect;
}
- (BOOL)canReplaceMapContent
{
return self.tileOverlay.canReplaceMapContent;
}
@end

View File

@@ -0,0 +1,13 @@
//
// AIRMapLocalTileManager.h
// AirMaps
//
// Created by Peter Zavadsky on 01/12/2017.
// Copyright © 2017 Christopher. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface AIRMapLocalTileManager : RCTViewManager
@end

View File

@@ -0,0 +1,38 @@
//
// AIRMapLocalTileManager.m
// AirMaps
//
// Created by Peter Zavadsky on 01/12/2017.
// Copyright © 2017 Christopher. All rights reserved.
//
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "AIRMapMarker.h"
#import "AIRMapLocalTile.h"
#import "AIRMapLocalTileManager.h"
@interface AIRMapLocalTileManager()
@end
@implementation AIRMapLocalTileManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapLocalTile *tile = [AIRMapLocalTile new];
return tile;
}
RCT_EXPORT_VIEW_PROPERTY(pathTemplate, NSString)
RCT_EXPORT_VIEW_PROPERTY(tileSize, CGFloat)
@end

View File

@@ -0,0 +1,12 @@
//
// AIRMapLocalTileOverlay.h
// Pods
//
// Created by Peter Zavadsky on 04/12/2017.
//
#import <MapKit/MapKit.h>
@interface AIRMapLocalTileOverlay : MKTileOverlay
@end

View File

@@ -0,0 +1,31 @@
//
// AIRMapLocalTileOverlay.m
// Pods-AirMapsExplorer
//
// Created by Peter Zavadsky on 04/12/2017.
//
#import "AIRMapLocalTileOverlay.h"
@interface AIRMapLocalTileOverlay ()
@end
@implementation AIRMapLocalTileOverlay
-(void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result {
NSMutableString *tileFilePath = [self.URLTemplate mutableCopy];
[tileFilePath replaceOccurrencesOfString: @"{x}" withString:[NSString stringWithFormat:@"%li", (long)path.x] options:0 range:NSMakeRange(0, tileFilePath.length)];
[tileFilePath replaceOccurrencesOfString:@"{y}" withString:[NSString stringWithFormat:@"%li", (long)path.y] options:0 range:NSMakeRange(0, tileFilePath.length)];
[tileFilePath replaceOccurrencesOfString:@"{z}" withString:[NSString stringWithFormat:@"%li", (long)path.z] options:0 range:NSMakeRange(0, tileFilePath.length)];
if ([[NSFileManager defaultManager] fileExistsAtPath:tileFilePath]) {
NSData* tile = [NSData dataWithContentsOfFile:tileFilePath];
result(tile,nil);
} else {
result(nil, nil);
}
}
@end

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <React/RCTViewManager.h>
#import "AIRMap.h"
#define MERCATOR_RADIUS 85445659.44705395
#define MERCATOR_OFFSET 268435456
@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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMapMarker.h"
#import "AIRMapCallout.h"
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import "AIRMap.h"
#import "SMCalloutView.h"
#import "RCTConvert+AirMap.h"
@class RCTBridge;
@interface AIRMapMarker : MKAnnotationView <MKAnnotation>
@property (nonatomic, strong) AIRMapCallout *calloutView;
@property (nonatomic, weak) AIRMap *map;
@property (nonatomic, weak) RCTBridge *bridge;
@property (nonatomic, strong) NSString *identifier;
@property (nonatomic, copy) NSString *imageSrc;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *subtitle;
@property (nonatomic, assign) CLLocationCoordinate2D coordinate;
@property (nonatomic, strong) UIColor *pinColor;
@property (nonatomic, assign) NSInteger zIndex;
@property (nonatomic, assign) double opacity;
@property (nonatomic, assign) BOOL isPreselected;
@property (nonatomic, assign) MKFeatureVisibility titleVisibility;
@property (nonatomic, assign) MKFeatureVisibility subtitleVisibility;
@property (nonatomic, assign) BOOL useLegacyPinView;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@property (nonatomic, copy) RCTDirectEventBlock onSelect;
@property (nonatomic, copy) RCTDirectEventBlock onDeselect;
@property (nonatomic, copy) RCTDirectEventBlock onCalloutPress;
@property (nonatomic, copy) RCTDirectEventBlock onDragStart;
@property (nonatomic, copy) RCTDirectEventBlock onDrag;
@property (nonatomic, copy) RCTDirectEventBlock onDragEnd;
- (MKAnnotationView *)getAnnotationView;
- (void)fillCalloutView:(SMCalloutView *)calloutView;
- (BOOL)shouldShowCalloutView;
- (void)showCalloutView;
- (void)hideCalloutView;
- (void)addTapGestureRecognizer;
- (void)setUseLegacyPinView:(BOOL)value;
- (void)animateToCoordinate:(CLLocationCoordinate2D)newCoordinate duration:(NSTimeInterval)duration;
@end
@interface AIREmptyCalloutBackgroundView : SMCalloutBackgroundView
@end

View File

@@ -0,0 +1,482 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMapMarker.h"
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTImageLoaderProtocol.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
#import <React/RCTImageLoader.h>
#import <React/RCTBridge+Private.h>
NSInteger const AIR_CALLOUT_OPEN_ZINDEX_BASELINE = 999;
@implementation AIREmptyCalloutBackgroundView
@end
@implementation AIRMapMarker {
BOOL _hasSetCalloutOffset;
RCTImageLoaderCancellationBlock _reloadImageCancellationBlock;
MKMarkerAnnotationView *_markerView;
MKPinAnnotationView *_pinView;
BOOL _calloutIsOpen;
NSInteger _zIndexBeforeOpen;
BOOL _useLegacyPinView;
CADisplayLink *_displayLink;
CLLocationCoordinate2D _startCoordinate;
CLLocationCoordinate2D _endCoordinate;
NSTimeInterval _animationStartTime;
NSTimeInterval _animationDuration;
BOOL _isAnimating;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self.layer addObserver:self forKeyPath:@"zPosition" options:NSKeyValueObservingOptionNew context:nil];
}
return self;
}
- (void)reactSetFrame:(CGRect)frame
{
// Make sure we use the image size when available
CGSize size = self.image ? self.image.size : frame.size;
CGRect bounds = {CGPointZero, size};
// The MapView is basically in charge of figuring out the center position of the marker view. If the view changed in
// height though, we need to compensate in such a way that the bottom of the marker stays at the same spot on the
// map.
CGFloat dy = (bounds.size.height - self.bounds.size.height) / 2;
CGPoint center = (CGPoint){ self.center.x, self.center.y - dy };
// Avoid crashes due to nan coords
if (isnan(center.x) || isnan(center.y) ||
isnan(bounds.origin.x) || isnan(bounds.origin.y) ||
isnan(bounds.size.width) || isnan(bounds.size.height)) {
RCTLogError(@"Invalid layout for (%@)%@. position: %@. bounds: %@",
self.reactTag, self, NSStringFromCGPoint(center), NSStringFromCGRect(bounds));
return;
}
self.center = center;
self.bounds = bounds;
}
- (void)insertReactSubview:(id<RCTComponent>)subview atIndex:(NSInteger)atIndex {
if ([subview isKindOfClass:[AIRMapCallout class]]) {
self.calloutView = (AIRMapCallout *)subview;
} else {
[super insertReactSubview:(UIView *)subview atIndex:atIndex];
[self addSubview:subview];
}
}
- (void) setFrame:(CGRect)frame {
// hack to ignore call by react transactions when it shouldn't
if (!CGPointEqualToPoint(self.frame.origin, CGPointZero) &&
CGPointEqualToPoint(frame.origin, CGPointZero)){
return;
}
[super setFrame:frame];
}
- (void)removeReactSubview:(id<RCTComponent>)subview {
if ([subview isKindOfClass:[AIRMapCallout class]] && self.calloutView == subview) {
self.calloutView = nil;
} else {
[super removeReactSubview:(UIView *)subview];
[(UIView *) subview removeFromSuperview];
}
}
- (MKAnnotationView *)getAnnotationView
{
if ([self shouldUsePinView]) {
// In this case, we want to render a platform "default" legacy marker.
if (_pinView == nil && _useLegacyPinView) {
_pinView = [[MKPinAnnotationView alloc] initWithAnnotation:self reuseIdentifier: nil];
[self addGestureRecognizerToView:_pinView];
_pinView.annotation = self;
if ([_pinView respondsToSelector:@selector(setPinTintColor:)]) {
_pinView.pinTintColor = self.pinColor;
}
_pinView.draggable = self.draggable;
_pinView.layer.zPosition = self.zIndex;
_pinView.displayPriority = self.displayPriority;
_pinView.zPriority = self.zIndex;
_pinView.centerOffset = self.centerOffset;
return _pinView;
}
if (_markerView == nil && !_useLegacyPinView) {
_markerView = [[MKMarkerAnnotationView alloc] initWithAnnotation:self reuseIdentifier: nil];
[self addGestureRecognizerToView:_markerView];
_markerView.annotation = self;
_markerView.draggable = self.draggable;
_markerView.layer.zPosition = self.zIndex;
_markerView.markerTintColor = self.pinColor;
_markerView.titleVisibility = self.titleVisibility ?: MKFeatureVisibilityHidden;
_markerView.subtitleVisibility = self.subtitleVisibility ?: MKFeatureVisibilityHidden;
_markerView.displayPriority = self.displayPriority;
_markerView.zPriority = self.zIndex;
_markerView.centerOffset = self.centerOffset;
}
return _markerView ?: _pinView;
} else {
// If it has subviews, it means we are wanting to render a custom marker with arbitrary react views.
// if it has a non-null image, it means we want to render a custom marker with the image.
// In either case, we want to return the AIRMapMarker since it is both an MKAnnotation and an
// MKAnnotationView all at the same time.
self.layer.zPosition = self.zIndex;
self.zPriority = self.zIndex;
return self;
}
}
- (void)fillCalloutView:(SMCalloutView *)calloutView
{
// Set everything necessary on the calloutView before it becomes visible.
// Apply the MKAnnotationView's desired calloutOffset (from the top-middle of the view)
if ([self shouldUsePinView] && !_hasSetCalloutOffset && _useLegacyPinView) {
calloutView.calloutOffset = CGPointMake(-8,0);
} else {
calloutView.calloutOffset = self.calloutOffset;
}
if (self.calloutView) {
calloutView.title = nil;
calloutView.subtitle = nil;
if (self.calloutView.tooltip) {
// if tooltip is true, then the user wants their react view to be the "tooltip" as wwell, so we set
// the background view to something empty/transparent
calloutView.backgroundView = [AIREmptyCalloutBackgroundView new];
} else {
// the default tooltip look is wanted, and the user is just filling the content with their react subviews.
// as a result, we use the default "masked" background view.
calloutView.backgroundView = [SMCalloutMaskedBackgroundView new];
}
// when this is set, the callout's content will be whatever react views the user has put as the callout's
// children.
calloutView.contentView = self.calloutView;
} else {
// if there is no calloutView, it means the user wants to use the default callout behavior with title/subtitle
// pairs.
calloutView.title = self.title;
calloutView.subtitle = self.subtitle;
calloutView.contentView = nil;
calloutView.backgroundView = [SMCalloutMaskedBackgroundView new];
}
}
- (void)showCalloutView
{
_calloutIsOpen = YES;
[self setZIndex:_zIndexBeforeOpen];
MKAnnotationView *annotationView = [self getAnnotationView];
[self setSelected:YES animated:NO];
[self.map selectAnnotation:self animated:NO];
id event = @{
@"action": @"marker-select",
@"id": self.identifier ?: @"unknown",
@"coordinate": @{
@"latitude": @(self.coordinate.latitude),
@"longitude": @(self.coordinate.longitude)
}
};
if (self.map.onMarkerSelect) self.map.onMarkerSelect(event);
if (self.onSelect) self.onSelect(event);
if (![self shouldShowCalloutView]) {
// no callout to show
return;
}
[self fillCalloutView:self.map.calloutView];
// This is where we present our custom callout view... MapKit's built-in callout doesn't have the flexibility
// we need, but a lot of work was done by Nick Farina to make this identical to MapKit's built-in.
[self.map.calloutView presentCalloutFromRect:annotationView.bounds
inView:annotationView
constrainedToView:self.map
animated:YES];
}
#pragma mark - Tap Gesture & Events.
- (void)addTapGestureRecognizer {
[self addGestureRecognizerToView:nil];
}
- (void)addGestureRecognizerToView:(UIView *)view {
if (!view) {
view = self;
}
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_handleTap:)];
// setting this to NO allows the parent MapView to continue receiving marker selection events
tapGestureRecognizer.cancelsTouchesInView = NO;
[view addGestureRecognizer:tapGestureRecognizer];
}
- (void)_handleTap:(UITapGestureRecognizer *)recognizer {
AIRMapMarker *marker = self;
if (!marker) return;
if (marker.selected) {
CGPoint touchPoint = [recognizer locationInView:marker.map.calloutView];
CGRect bubbleFrame = [self.calloutView convertRect:marker.map.calloutView.bounds toView:marker.map];
CGPoint touchPointReal = [recognizer locationInView:self.calloutView];
UIView *calloutView = [marker.map.calloutView hitTest:touchPoint withEvent:nil];
if (calloutView) {
// the callout (or its subview) got clicked, not the marker
UIWindow* win = [[[UIApplication sharedApplication] windows] firstObject];
AIRMapCalloutSubview* calloutSubview = nil;
UIView* tmp = calloutView;
while (tmp && tmp != win && tmp != self.calloutView && tmp != self.map) {
if ([tmp respondsToSelector:@selector(onPress)]) {
calloutSubview = (AIRMapCalloutSubview*) tmp;
break;
}
tmp = tmp.superview;
}
id event = @{
@"action": calloutSubview ? @"callout-inside-press" : @"callout-press",
@"id": marker.identifier ?: @"unknown",
@"point": @{
@"x": @(touchPointReal.x),
@"y": @(touchPointReal.y),
},
@"frame": @{
@"x": @(bubbleFrame.origin.x),
@"y": @(bubbleFrame.origin.y),
@"width": @(bubbleFrame.size.width),
@"height": @(bubbleFrame.size.height),
}
};
if (calloutSubview) calloutSubview.onPress(event);
if (marker.onCalloutPress) marker.onCalloutPress(event);
if (marker.calloutView && marker.calloutView.onPress) marker.calloutView.onPress(event);
if (marker.map.onCalloutPress) marker.map.onCalloutPress(event);
return;
}
}
// the actual marker got clicked
CGPoint touchPointReal = [recognizer locationInView:self.calloutView];
id event = @{
@"action": @"marker-press",
@"id": marker.identifier ?: @"unknown",
@"coordinate": @{
@"latitude": @(marker.coordinate.latitude),
@"longitude": @(marker.coordinate.longitude)
},
@"position": @{
@"x": @(touchPointReal.x),
@"y": @(touchPointReal.y),
}
};
if (marker.onPress) marker.onPress(event);
if (marker.map.onMarkerPress) marker.map.onMarkerPress(event);
[marker.map selectAnnotation:marker animated:NO];
}
- (void)hideCalloutView
{
_calloutIsOpen = NO;
[self setZIndex:_zIndexBeforeOpen];
// hide the callout view
[self.map.calloutView dismissCalloutAnimated:YES];
[self setSelected:NO animated:NO];
[self.map deselectAnnotation:self animated:NO];
id event = @{
@"action": @"marker-deselect",
@"id": self.identifier ?: @"unknown",
@"coordinate": @{
@"latitude": @(self.coordinate.latitude),
@"longitude": @(self.coordinate.longitude)
}
};
if (self.map.onMarkerDeselect) self.map.onMarkerDeselect(event);
if (self.onDeselect) self.onDeselect(event);
}
- (void)setCalloutOffset:(CGPoint)calloutOffset
{
_hasSetCalloutOffset = YES;
[super setCalloutOffset:calloutOffset];
}
- (void) setCenterOffset:(CGPoint)centerOffset
{
[super setCenterOffset:centerOffset];
}
- (BOOL)shouldShowCalloutView
{
return self.calloutView != nil || self.title != nil || self.subtitle != nil;
}
- (BOOL)shouldUsePinView
{
return self.reactSubviews.count == 0 && !self.imageSrc;
}
- (void)setOpacity:(double)opacity
{
[self setAlpha:opacity];
}
- (void)setImageSrc:(NSString *)imageSrc
{
_imageSrc = imageSrc;
if (_reloadImageCancellationBlock) {
_reloadImageCancellationBlock();
_reloadImageCancellationBlock = nil;
}
__weak __typeof(self) weakSelf = self;
_reloadImageCancellationBlock = [[[RCTBridge currentBridge] moduleForName:@"ImageLoader"] loadImageWithURLRequest:[RCTConvert NSURLRequest:_imageSrc]
size:self.bounds.size
scale:RCTScreenScale()
clipped:YES
resizeMode:RCTResizeModeCenter
progressBlock:nil
partialLoadBlock:nil
completionBlock:^(NSError *error, UIImage *image) {
if (error) {
// TODO(lmr): do something with the error?
NSLog(@"failed to load image: %@", error);
}
dispatch_async(dispatch_get_main_queue(), ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
strongSelf.image = image;
});
}];
}
- (void)setPinColor:(UIColor *)pinColor
{
_pinColor = pinColor;
if(_useLegacyPinView && [_pinView respondsToSelector:@selector(setPinTintColor:)]) {
_pinView.pinTintColor = _pinColor;
} else {
_markerView.markerTintColor = _pinColor;
}
}
- (void)setZIndex:(NSInteger)zIndex
{
_zIndexBeforeOpen = zIndex;
_zIndex = _calloutIsOpen ? zIndex + AIR_CALLOUT_OPEN_ZINDEX_BASELINE : zIndex;
self.layer.zPosition = zIndex;
}
- (BOOL)isSelected {
return _isPreselected || [super isSelected];
}
- (void)dealloc {
[self.layer removeObserver:self forKeyPath:@"zPosition"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"zPosition"]) {
self.layer.zPosition = _zIndex;
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
CGRect reactFrame = self.frame;
UIView *firstSubView = self.subviews.firstObject;
if (firstSubView && (CGRectGetWidth(firstSubView.frame) > CGRectGetWidth(reactFrame) ||
CGRectGetHeight(firstSubView.frame) > CGRectGetHeight(reactFrame))) {
reactFrame = firstSubView.frame;
}
[self reactSetFrame:reactFrame];
}
- (void)setUseLegacyPinView:(BOOL)value {
_useLegacyPinView = value;
}
- (void)animateToCoordinate:(CLLocationCoordinate2D)newCoordinate duration:(NSTimeInterval)duration {
if (_isAnimating) {
NSLog(@"Animation already in progress. Rejecting new animation request.");
return;
}
// Mark as animating
_isAnimating = YES;
// Store animation parameters
_startCoordinate = self.coordinate;
_endCoordinate = newCoordinate;
_animationDuration = duration;
_animationStartTime = [NSDate timeIntervalSinceReferenceDate];
// Start a CADisplayLink
if (_displayLink) {
[_displayLink invalidate];
}
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updatePosition)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)updatePosition {
NSTimeInterval elapsed = [NSDate timeIntervalSinceReferenceDate] - _animationStartTime;
CGFloat progress = MIN(elapsed / _animationDuration, 1.0);
// Interpolate coordinates
CLLocationDegrees currentLatitude = _startCoordinate.latitude + progress * (_endCoordinate.latitude - _startCoordinate.latitude);
CLLocationDegrees currentLongitude = _startCoordinate.longitude + progress * (_endCoordinate.longitude - _startCoordinate.longitude);
// Update annotation's coordinate
CLLocationCoordinate2D currentCoordinate = CLLocationCoordinate2DMake(currentLatitude, currentLongitude);
[self setValue:[NSValue valueWithMKCoordinate:currentCoordinate] forKey:@"coordinate"];
// Stop the animation when complete
if (progress == 1.0) {
[_displayLink invalidate];
_displayLink = nil;
_isAnimating = NO; // Reset the animation state
}
}
@end

View File

@@ -0,0 +1,14 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <React/RCTViewManager.h>
@interface AIRMapMarkerManager : RCTViewManager
@end

View File

@@ -0,0 +1,98 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMapMarkerManager.h"
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTUIManager.h>
#import <React/UIView+React.h>
#import "AIRMapMarker.h"
@interface AIRMapMarkerManager () <MKMapViewDelegate>
@end
@implementation AIRMapMarkerManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapMarker *marker = [AIRMapMarker new];
[marker addTapGestureRecognizer];
marker.bridge = self.bridge;
marker.isAccessibilityElement = YES;
marker.accessibilityElementsHidden = NO;
return marker;
}
RCT_EXPORT_VIEW_PROPERTY(identifier, NSString)
RCT_REMAP_VIEW_PROPERTY(testID, accessibilityIdentifier, NSString)
//RCT_EXPORT_VIEW_PROPERTY(reuseIdentifier, NSString)
RCT_EXPORT_VIEW_PROPERTY(title, NSString)
RCT_REMAP_VIEW_PROPERTY(description, subtitle, NSString)
RCT_EXPORT_VIEW_PROPERTY(coordinate, CLLocationCoordinate2D)
RCT_EXPORT_VIEW_PROPERTY(centerOffset, CGPoint)
RCT_EXPORT_VIEW_PROPERTY(calloutOffset, CGPoint)
RCT_REMAP_VIEW_PROPERTY(image, imageSrc, NSString)
RCT_EXPORT_VIEW_PROPERTY(pinColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(draggable, BOOL)
RCT_EXPORT_VIEW_PROPERTY(zIndex, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(opacity, double)
RCT_EXPORT_VIEW_PROPERTY(isPreselected, BOOL)
RCT_EXPORT_VIEW_PROPERTY(titleVisibility, NSString)
RCT_EXPORT_VIEW_PROPERTY(subtitleVisibility, NSString)
RCT_EXPORT_VIEW_PROPERTY(useLegacyPinView, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSelect, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDeselect, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onCalloutPress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDragStart, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDrag, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDragEnd, RCTDirectEventBlock)
RCT_EXPORT_METHOD(showCallout:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRMapMarker class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
} else {
[(AIRMapMarker *) view showCalloutView];
}
}];
}
RCT_EXPORT_METHOD(hideCallout:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRMapMarker class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
} else {
[(AIRMapMarker *) view hideCalloutView];
}
}];
}
RCT_EXPORT_METHOD(redrawCallout:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRMapMarker class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
} else {
//no need to do anything here
}
}];
}
@end

View File

@@ -0,0 +1,36 @@
#import "AIRMapCallout.h"
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import "RCTConvert+AirMap.h"
#import <React/RCTComponent.h>
#import "AIRMap.h"
#import "AIRMapOverlayRenderer.h"
@class RCTBridge;
@interface AIRMapOverlay : UIView <MKOverlay>
@property (nonatomic, strong) AIRMapOverlayRenderer *renderer;
@property (nonatomic, weak) AIRMap *map;
@property (nonatomic, weak) RCTBridge *bridge;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) NSString *imageSrc;
@property (nonatomic, strong, readonly) UIImage *overlayImage;
@property (nonatomic, copy) NSArray *boundsRect;
@property (nonatomic, assign) NSInteger rotation;
@property (nonatomic, assign) CGFloat transparency;
@property (nonatomic, assign) NSInteger zIndex;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
#pragma mark MKOverlay protocol
@property(nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property(nonatomic, readonly) MKMapRect boundingMapRect;
- (BOOL)intersectsMapRect:(MKMapRect)mapRect;
- (BOOL)canReplaceMapContent;
@end

View File

@@ -0,0 +1,103 @@
#import "AIRMapOverlay.h"
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTImageLoaderProtocol.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
@interface AIRMapOverlay()
@property (nonatomic, strong, readwrite) UIImage *overlayImage;
@end
@implementation AIRMapOverlay {
RCTImageLoaderCancellationBlock _reloadImageCancellationBlock;
CLLocationCoordinate2D _southWest;
CLLocationCoordinate2D _northEast;
MKMapRect _mapRect;
}
- (void)setImageSrc:(NSString *)imageSrc
{
NSLog(@">>> SET IMAGESRC: %@", imageSrc);
_imageSrc = imageSrc;
if (_reloadImageCancellationBlock) {
_reloadImageCancellationBlock();
_reloadImageCancellationBlock = nil;
}
__weak typeof(self) weakSelf = self;
_reloadImageCancellationBlock = [[_bridge moduleForName:@"ImageLoader"] loadImageWithURLRequest:[RCTConvert NSURLRequest:_imageSrc]
size:weakSelf.bounds.size
scale:RCTScreenScale()
clipped:YES
resizeMode:RCTResizeModeCenter
progressBlock:nil
partialLoadBlock:nil
completionBlock:^(NSError *error, UIImage *image) {
if (error) {
NSLog(@"%@", error);
}
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@">>> IMAGE: %@", image);
weakSelf.overlayImage = image;
[weakSelf createOverlayRendererIfPossible];
[weakSelf update];
});
}];
}
- (void)setBoundsRect:(NSArray *)boundsRect {
_boundsRect = boundsRect;
_northEast = CLLocationCoordinate2DMake([boundsRect[0][0] doubleValue], [boundsRect[0][1] doubleValue]);
_southWest = CLLocationCoordinate2DMake([boundsRect[1][0] doubleValue], [boundsRect[1][1] doubleValue]);
MKMapPoint southWest = MKMapPointForCoordinate(_southWest);
MKMapPoint northEast = MKMapPointForCoordinate(_northEast);
_mapRect = MKMapRectMake(southWest.x, northEast.y, ABS(northEast.x - southWest.x), ABS(northEast.y - southWest.y));
[self update];
}
- (void)createOverlayRendererIfPossible
{
if (MKMapRectIsEmpty(_mapRect) || !self.overlayImage) return;
__weak typeof(self) weakSelf = self;
self.renderer = [[AIRMapOverlayRenderer alloc] initWithOverlay:weakSelf];
}
- (void)update
{
if (!_renderer) return;
if (_map == nil) return;
[_map removeOverlay:self];
[_map addOverlay:self];
}
#pragma mark MKOverlay implementation
- (CLLocationCoordinate2D)coordinate
{
return MKCoordinateForMapPoint(MKMapPointMake(MKMapRectGetMidX(_mapRect), MKMapRectGetMidY(_mapRect)));
}
- (MKMapRect)boundingMapRect
{
return _mapRect;
}
- (BOOL)intersectsMapRect:(MKMapRect)mapRect
{
return MKMapRectIntersectsRect(_mapRect, mapRect);
}
- (BOOL)canReplaceMapContent
{
return NO;
}
@end

View File

@@ -0,0 +1,5 @@
#import <React/RCTViewManager.h>
@interface AIRMapOverlayManager : RCTViewManager
@end

View File

@@ -0,0 +1,27 @@
#import "AIRMapOverlayManager.h"
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTUIManager.h>
#import <React/UIView+React.h>
#import "AIRMapOverlay.h"
@interface AIRMapOverlayManager () <MKMapViewDelegate>
@end
@implementation AIRMapOverlayManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapOverlay *overlay = [AIRMapOverlay new];
overlay.bridge = self.bridge;
return overlay;
}
RCT_REMAP_VIEW_PROPERTY(bounds, boundsRect, NSArray)
RCT_REMAP_VIEW_PROPERTY(image, imageSrc, NSString)
@end

View File

@@ -0,0 +1,8 @@
#import <MapKit/MapKit.h>
@interface AIRMapOverlayRenderer : MKOverlayRenderer
@property (nonatomic, assign) NSInteger rotation;
@property (nonatomic, assign) CGFloat transparency;
@end

View File

@@ -0,0 +1,30 @@
#import "AIRMapOverlayRenderer.h"
#import "AIRMapOverlay.h"
@implementation AIRMapOverlayRenderer
- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context {
UIImage *image = [(AIRMapOverlay *)self.overlay overlayImage];
CGContextSaveGState(context);
CGImageRef imageReference = image.CGImage;
MKMapRect theMapRect = [self.overlay boundingMapRect];
CGRect theRect = [self rectForMapRect:theMapRect];
CGContextRotateCTM(context, M_PI);
CGContextScaleCTM(context, -1.0, 1.0);
CGContextTranslateCTM(context, 0.0, -theRect.size.height);
CGContextAddRect(context, theRect);
CGContextDrawImage(context, theRect, imageReference);
CGContextRestoreGState(context);
}
- (BOOL)canDrawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale {
return [(AIRMapOverlay *)self.overlay overlayImage] != nil;
}
@end

View File

@@ -0,0 +1,46 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import <React/RCTView.h>
#import "AIRMapCoordinate.h"
#import "AIRMap.h"
#import "RCTConvert+AirMap.h"
@interface AIRMapPolygon: MKAnnotationView <MKOverlay>
@property (nonatomic, weak) AIRMap *map;
@property (nonatomic, strong) MKPolygon *polygon;
@property (nonatomic, strong) MKPolygonRenderer *renderer;
@property (nonatomic, strong) NSArray<MKPolygon *> *interiorPolygons;
@property (nonatomic, strong) NSArray<AIRMapCoordinate *> *coordinates;
@property (nonatomic, strong) NSArray<NSArray<AIRMapCoordinate *> *> *holes;
@property (nonatomic, strong) UIColor *fillColor;
@property (nonatomic, strong) UIColor *strokeColor;
@property (nonatomic, assign) CGFloat strokeWidth;
@property (nonatomic, assign) CGFloat miterLimit;
@property (nonatomic, assign) CGLineCap lineCap;
@property (nonatomic, assign) CGLineJoin lineJoin;
@property (nonatomic, assign) CGFloat lineDashPhase;
@property (nonatomic, strong) NSArray <NSNumber *> *lineDashPattern;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
#pragma mark MKOverlay protocol
@property(nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property(nonatomic, readonly) MKMapRect boundingMapRect;
- (BOOL)intersectsMapRect:(MKMapRect)mapRect;
- (BOOL)canReplaceMapContent;
@end

View File

@@ -0,0 +1,126 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import "AIRMapPolygon.h"
#import <React/UIView+React.h>
@implementation AIRMapPolygon {
}
- (void)setFillColor:(UIColor *)fillColor {
_fillColor = fillColor;
[self update];
}
- (void)setStrokeColor:(UIColor *)strokeColor {
_strokeColor = strokeColor;
[self update];
}
- (void)setStrokeWidth:(CGFloat)strokeWidth {
_strokeWidth = strokeWidth;
[self update];
}
- (void)setLineJoin:(CGLineJoin)lineJoin {
_lineJoin = lineJoin;
[self update];
}
- (void)setLineCap:(CGLineCap)lineCap {
_lineCap = lineCap;
[self update];
}
- (void)setMiterLimit:(CGFloat)miterLimit {
_miterLimit = miterLimit;
[self update];
}
- (void)setLineDashPhase:(CGFloat)lineDashPhase {
_lineDashPhase = lineDashPhase;
[self update];
}
- (void)setLineDashPattern:(NSArray <NSNumber *> *)lineDashPattern {
_lineDashPattern = lineDashPattern;
[self update];
}
- (void)setCoordinates:(NSArray<AIRMapCoordinate *> *)coordinates {
_coordinates = coordinates;
CLLocationCoordinate2D coords[coordinates.count];
for(int i = 0; i < coordinates.count; i++)
{
coords[i] = coordinates[i].coordinate;
}
self.polygon = [MKPolygon polygonWithCoordinates:coords count:coordinates.count interiorPolygons:_interiorPolygons];
// TODO: we could lazy-initialize the polygon, since we don't need it until the
// polygon is in view.
self.renderer = [[MKPolygonRenderer alloc] initWithPolygon:self.polygon];
[self update];
}
- (void)setHoles:(NSArray<NSArray<AIRMapCoordinate *> *> *)holes {
_holes = holes;
if (holes.count)
{
NSMutableArray<MKPolygon *> *polygons = [NSMutableArray array];
for(int h = 0; h < holes.count; h++)
{
CLLocationCoordinate2D coords[holes[h].count];
for(int i = 0; i < holes[h].count; i++)
{
coords[i] = holes[h][i].coordinate;
}
[polygons addObject:[MKPolygon polygonWithCoordinates:coords count:holes[h].count]];
}
_interiorPolygons = polygons;
}
}
- (void) update
{
if (!_renderer) return;
_renderer.fillColor = _fillColor;
_renderer.strokeColor = _strokeColor;
_renderer.lineWidth = _strokeWidth;
_renderer.lineCap = _lineCap;
_renderer.lineJoin = _lineJoin;
_renderer.miterLimit = _miterLimit;
_renderer.lineDashPhase = _lineDashPhase;
_renderer.lineDashPattern = _lineDashPattern;
if (_map == nil) return;
[_map removeOverlay:self];
[_map addOverlay:self];
}
#pragma mark MKOverlay implementation
- (CLLocationCoordinate2D) coordinate
{
return self.polygon.coordinate;
}
- (MKMapRect) boundingMapRect
{
return self.polygon.boundingMapRect;
}
- (BOOL)intersectsMapRect:(MKMapRect)mapRect
{
BOOL answer = [self.polygon intersectsMapRect:mapRect];
return answer;
}
- (BOOL)canReplaceMapContent
{
return NO;
}
@end

View File

@@ -0,0 +1,10 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface AIRMapPolygonManager : RCTViewManager
@end

View File

@@ -0,0 +1,49 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMapPolygonManager.h"
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "RCTConvert+AirMap.h"
#import "AIRMapMarker.h"
#import "AIRMapPolygon.h"
@interface AIRMapPolygonManager()
@end
@implementation AIRMapPolygonManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapPolygon *polygon = [AIRMapPolygon new];
return polygon;
}
RCT_EXPORT_VIEW_PROPERTY(coordinates, AIRMapCoordinateArray)
RCT_EXPORT_VIEW_PROPERTY(holes, AIRMapCoordinateArrayArray)
RCT_EXPORT_VIEW_PROPERTY(fillColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(strokeColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineCap, CGLineCap)
RCT_EXPORT_VIEW_PROPERTY(lineJoin, CGLineJoin)
RCT_EXPORT_VIEW_PROPERTY(miterLimit, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineDashPhase, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineDashPattern, NSArray)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
@end

View File

@@ -0,0 +1,45 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import <React/RCTView.h>
#import "AIRMapCoordinate.h"
#import "AIRMap.h"
#import "RCTConvert+AirMap.h"
@interface AIRMapPolyline: MKAnnotationView <MKOverlay>
@property (nonatomic, weak) AIRMap *map;
@property (nonatomic, strong) MKPolyline *polyline;
@property (nonatomic, strong) MKOverlayPathRenderer *renderer;
@property (nonatomic, strong) NSArray<AIRMapCoordinate *> *coordinates;
@property (nonatomic, strong) UIColor *fillColor;
@property (nonatomic, strong) UIColor *strokeColor;
@property (nonatomic, strong) NSArray<UIColor *> *strokeColors;
@property (nonatomic, assign) CGFloat strokeWidth;
@property (nonatomic, assign) CGFloat miterLimit;
@property (nonatomic, assign) CGLineCap lineCap;
@property (nonatomic, assign) CGLineJoin lineJoin;
@property (nonatomic, assign) CGFloat lineDashPhase;
@property (nonatomic, strong) NSArray <NSNumber *> *lineDashPattern;
@property (nonatomic, assign) BOOL geodesic;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
#pragma mark MKOverlay protocol
@property(nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property(nonatomic, readonly) MKMapRect boundingMapRect;
- (BOOL)intersectsMapRect:(MKMapRect)mapRect;
- (BOOL)canReplaceMapContent;
@end

View File

@@ -0,0 +1,161 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import "AIRMapPolyline.h"
#import "AIRMapPolylineRenderer.h"
#import <React/UIView+React.h>
@implementation AIRMapPolyline {
}
- (void)setFillColor:(UIColor *)fillColor {
_fillColor = fillColor;
[self update];
}
- (void)setStrokeColor:(UIColor *)strokeColor {
_strokeColor = strokeColor;
[self update];
}
- (void)setStrokeColors:(NSArray<UIColor *> *)strokeColors {
_strokeColors = strokeColors;
if ((self.renderer != nil) && ![_renderer isKindOfClass:[AIRMapPolylineRenderer class]]) {
self.renderer = [self createRenderer];
}
[self update];
}
- (void)setStrokeWidth:(CGFloat)strokeWidth {
_strokeWidth = strokeWidth;
[self update];
}
- (void)setLineJoin:(CGLineJoin)lineJoin {
_lineJoin = lineJoin;
[self update];
}
- (void)setLineCap:(CGLineCap)lineCap {
_lineCap = lineCap;
[self update];
}
- (void)setMiterLimit:(CGFloat)miterLimit {
_miterLimit = miterLimit;
[self update];
}
- (void)setLineDashPhase:(CGFloat)lineDashPhase {
_lineDashPhase = lineDashPhase;
[self update];
}
- (void)setLineDashPattern:(NSArray <NSNumber *> *)lineDashPattern {
_lineDashPattern = lineDashPattern;
[self update];
}
-(void)setGeodesic:(BOOL)geodesic
{
_geodesic = geodesic;
if(_coordinates){
[self setCoordinates:_coordinates];
}
}
- (void)setCoordinates:(NSArray<AIRMapCoordinate *> *)coordinates {
_coordinates = coordinates;
CLLocationCoordinate2D *coords = calloc(coordinates.count, sizeof(CLLocationCoordinate2D));
for(int i = 0; i < coordinates.count; i++)
{
coords[i] = coordinates[i].coordinate;
}
if(_geodesic){
self.polyline = [MKGeodesicPolyline polylineWithCoordinates:coords count:coordinates.count];
} else {
self.polyline = [MKPolyline polylineWithCoordinates:coords count:coordinates.count];
}
free(coords);
self.renderer = [self createRenderer];
[self update];
}
- (MKOverlayPathRenderer*)createRenderer {
if (self.polyline == nil) return nil;
if (self.strokeColors == nil) {
// Use the default renderer when no array of stroke-colors is defined.
// This behaviour may be changed in the future if we permanently want to
// use the custom renderer, because it can add funtionality that is not
// supported by the default renderer.
return [[MKPolylineRenderer alloc] initWithPolyline:self.polyline];
}
else {
return [[AIRMapPolylineRenderer alloc] initWithOverlay:self polyline:self.polyline];
}
}
- (void) update
{
if (!_renderer) return;
[self updateRenderer:_renderer];
if (_map == nil) return;
[_map removeOverlay:self];
[_map addOverlay:self];
}
- (void) updateRenderer:(MKOverlayPathRenderer*)renderer {
renderer.fillColor = _fillColor;
renderer.strokeColor = _strokeColor;
renderer.lineWidth = _strokeWidth;
renderer.lineCap = _lineCap;
renderer.lineJoin = _lineJoin;
renderer.miterLimit = _miterLimit;
renderer.lineDashPhase = _lineDashPhase;
renderer.lineDashPattern = _lineDashPattern;
if ([renderer isKindOfClass:[AIRMapPolylineRenderer class]]) {
((AIRMapPolylineRenderer*)renderer).strokeColors = _strokeColors;
}
}
#pragma mark MKOverlay implementation
- (CLLocationCoordinate2D) coordinate
{
return self.polyline.coordinate;
}
- (MKMapRect) boundingMapRect
{
return self.polyline.boundingMapRect;
}
- (BOOL)intersectsMapRect:(MKMapRect)mapRect
{
BOOL answer = [self.polyline intersectsMapRect:mapRect];
return answer;
}
- (BOOL)canReplaceMapContent
{
return NO;
}
#pragma mark AIRMapSnapshot implementation
- (void) drawToSnapshot:(MKMapSnapshot *) snapshot context:(CGContextRef) context
{
AIRMapPolylineRenderer* renderer = [[AIRMapPolylineRenderer alloc] initWithSnapshot:snapshot overlay:self polyline:self.polyline];
[self updateRenderer:renderer];
[renderer drawWithZoomScale:2 inContext:context];
}
@end

View File

@@ -0,0 +1,10 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface AIRMapPolylineManager : RCTViewManager
@end

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMapPolylineManager.h"
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "RCTConvert+AirMap.h"
#import "AIRMapMarker.h"
#import "AIRMapPolyline.h"
@interface AIRMapPolylineManager()
@end
@implementation AIRMapPolylineManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapPolyline *polyline = [AIRMapPolyline new];
return polyline;
}
RCT_EXPORT_VIEW_PROPERTY(coordinates, AIRMapCoordinateArray)
RCT_EXPORT_VIEW_PROPERTY(strokeColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(strokeColors, UIColorArray)
RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineCap, CGLineCap)
RCT_EXPORT_VIEW_PROPERTY(lineJoin, CGLineJoin)
RCT_EXPORT_VIEW_PROPERTY(miterLimit, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineDashPhase, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(lineDashPattern, NSArray)
RCT_EXPORT_VIEW_PROPERTY(geodesic, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
@end

View File

@@ -0,0 +1,19 @@
//
// AIRMapPolylineRenderer.h
// mapDemo
//
// Created by IjzerenHein on 13-11-21.
// Copyright (c) 2017 IjzerenHein. All rights reserved.
//
#import <MapKit/MapKit.h>
@interface AIRMapPolylineRenderer : MKOverlayPathRenderer
-(id)initWithOverlay:(id<MKOverlay>)overlay polyline:(MKPolyline*)polyline;
-(id)initWithSnapshot:(MKMapSnapshot*)snapshot overlay:(id<MKOverlay>)overlay polyline:(MKPolyline*)polyline;
-(void)drawWithZoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context;
@property (nonatomic, strong) NSArray<UIColor *> *strokeColors;
@end

View File

@@ -0,0 +1,240 @@
//
// AIRMapPolylineRenderer.h
// mapDemo
//
// Created by IjzerenHein on 13-11-21.
// Copyright (c) 2017 IjzerenHein. All rights reserved.
//
#import "AIRMapPolylineRenderer.h"
@interface AIRMapPolylineRendererSegment : NSObject
- (id)initWithPoint:(CGPoint)point color:(UIColor*)color;
- (void) addPoint:(CGPoint)point color:(UIColor*)color;
@property CGMutablePathRef path;
@property UIColor *startColor;
@property UIColor *endColor;
@property CGPoint startPoint;
@property CGPoint endPoint;
@end
@implementation AIRMapPolylineRendererSegment
- (id)initWithPoint:(CGPoint)point color:(UIColor*)color
{
self = [super init];
if (self){
self.path = CGPathCreateMutable();
self.startColor = color;
self.startPoint = point;
self.endPoint = point;
CGPathMoveToPoint(self.path, nil, point.x, point.y);
}
return self;
}
- (void) addPoint:(CGPoint)point color:(UIColor*)color
{
self.endPoint = point;
self.endColor = color;
CGPathAddLineToPoint(self.path, nil, point.x, point.y);
}
@end
@implementation AIRMapPolylineRenderer {
MKPolyline* _polyline;
NSArray<UIColor *> *_strokeColors;
MKMapSnapshot* _snapshot;
CLLocationCoordinate2D* _coordinates;
}
@synthesize strokeColors;
- (id)initWithOverlay:(id<MKOverlay>)overlay polyline:(MKPolyline*)polyline
{
self = [super initWithOverlay:overlay];
if (self){
_polyline = polyline;
[self createPath];
}
return self;
}
- (id)initWithSnapshot:(MKMapSnapshot*)snapshot overlay:(id<MKOverlay>)overlay polyline:(MKPolyline*)polyline
{
self = [super initWithOverlay:overlay];
if (self){
_snapshot = snapshot;
_polyline = polyline;
_coordinates = malloc(sizeof(CLLocationCoordinate2D) * [_polyline pointCount]);
[_polyline getCoordinates:_coordinates range:NSMakeRange(0, [_polyline pointCount])];
}
return self;
}
- (void) dealloc
{
if (_coordinates) free(_coordinates);
}
- (CGPoint) pointForIndex:(NSUInteger)index
{
if (_snapshot != nil) {
return [_snapshot pointForCoordinate:_coordinates[index]];
}
else {
return [self pointForMapPoint:_polyline.points[index]];
}
}
- (UIColor*) colorForIndex:(NSUInteger)index
{
if ((_strokeColors == nil) || !_strokeColors.count) return self.strokeColor;
index = MIN(index, _strokeColors.count - 1);
UIColor* color = _strokeColors[index];
CGFloat pc_r,pc_g,pc_b,pc_a;
[color getRed:&pc_r green:&pc_g blue:&pc_b alpha:&pc_a];
return (pc_a == 0) ? nil : color;
}
- (void) createPath
{
CGMutablePathRef path = CGPathCreateMutable();
BOOL first = YES;
for (NSUInteger i = 0, n = _polyline.pointCount; i < n; i++){
CGPoint point = [self pointForIndex:i];
if (first) {
CGPathMoveToPoint(path, nil, point.x, point.y);
first = NO;
} else {
CGPathAddLineToPoint(path, nil, point.x, point.y);
}
}
self.path = path;
}
- (NSArray*) createSegments
{
NSMutableArray* segments = [NSMutableArray new];
if (_polyline.pointCount <= 1) return segments;
AIRMapPolylineRendererSegment* segment = nil;
for (NSUInteger i = 0, n = _polyline.pointCount; i < n; i++){
CGPoint point = [self pointForIndex:i];
UIColor* color = [self colorForIndex:i];
if (segment == nil) {
// Start new segment
segment = [[AIRMapPolylineRendererSegment alloc] initWithPoint:point color:color];
[segments addObject:segment];
}
else if (((color == nil) && (segment.endColor == nil)) ||
((color != nil) && [segment.startColor isEqual:color])) {
// Append point to segment
[segment addPoint:point color: color];
}
else {
// Close the last segment if needed
if (segment.endColor == nil) {
[segment addPoint:point color:color];
}
else {
// Add transition gradient
segment = [[AIRMapPolylineRendererSegment alloc] initWithPoint:segment.endPoint color:segment.endColor];
[segment addPoint:point color:color];
[segments addObject:segment];
}
// Start new segment
if (i < (n - 1)) {
segment = [[AIRMapPolylineRendererSegment alloc] initWithPoint:point color:color];
[segments addObject:segment];
}
}
}
// Remove last segment in case it only contains a single path point
if ((segment != nil) && (segment.endColor == nil)) {
[segments removeLastObject];
}
return segments;
}
- (void) setStrokeColors:(NSArray<UIColor *> *)strokeColors
{
if (_strokeColors != strokeColors) {
_strokeColors = strokeColors;
}
}
- (void) setStrokeColor:(UIColor *)strokeColor
{
if (super.strokeColor != strokeColor) {
super.strokeColor = strokeColor;
}
}
- (void) drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
{
CGRect pointsRect = CGPathGetBoundingBox(self.path);
CGRect mapRectCG = [self rectForMapRect:mapRect];
if (!CGRectIntersectsRect(pointsRect, mapRectCG)) return;
[self drawWithZoomScale:zoomScale inContext:context];
}
- (void) drawWithZoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
{
CGFloat lineWidth = (self.lineWidth / zoomScale) * 2.0;
CGContextSetLineWidth(context, lineWidth);
CGContextSetLineCap(context, self.lineCap);
CGContextSetLineJoin(context, self.lineJoin);
CGContextSetFillColorWithColor(context, self.fillColor.CGColor);
CGContextSetMiterLimit(context, self.miterLimit);
CGFloat dashes[self.lineDashPattern.count];
for (NSUInteger i = 0; i < self.lineDashPattern.count; i++) {
dashes[i] = self.lineDashPattern[i].floatValue;
}
CGContextSetLineDash(context, self.lineDashPhase, dashes, self.lineDashPattern.count);
NSArray* segments = [self createSegments];
for (NSUInteger i = 0, n = segments.count; i < n; i++) {
AIRMapPolylineRendererSegment* segment = segments[i];
CGContextBeginPath(context);
CGContextAddPath(context, segment.path);
// When segment has two colors, draw it as a gradient
if (![segment.startColor isEqual:segment.endColor]) {
CGFloat pc_r,pc_g,pc_b,pc_a,
cc_r,cc_g,cc_b,cc_a;
[segment.startColor getRed:&pc_r green:&pc_g blue:&pc_b alpha:&pc_a];
[segment.endColor getRed:&cc_r green:&cc_g blue:&cc_b alpha:&cc_a];
CGFloat gradientColors[8] = {pc_r,pc_g,pc_b,pc_a,
cc_r,cc_g,cc_b,cc_a};
CGFloat gradientLocation[2] = {0,1};
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, gradientColors, gradientLocation, 2);
CGColorSpaceRelease(colorSpace);
CGContextReplacePathWithStrokedPath(context);
CGContextClip(context);
CGContextDrawLinearGradient(
context,
gradient,
segment.startPoint,
segment.endPoint,
kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation
);
CGGradientRelease(gradient);
CGContextResetClip(context);
}
else {
CGContextSetStrokeColorWithColor(context, segment.startColor.CGColor);
CGContextStrokePath(context);
}
}
}
@end

View File

@@ -0,0 +1,17 @@
//
// AIRMapSnapshot.h
// AirMaps
//
// Created by Hein Rutjes on 26/09/16.
// Copyright © 2016 Christopher. All rights reserved.
//
#ifndef AIRMapSnapshot_h
#define AIRMapSnapshot_h
@protocol AIRMapSnapshot <NSObject>
@optional
- (void) drawToSnapshot:(MKMapSnapshot *) snapshot context:(CGContextRef) context;
@end
#endif /* AIRMapSnapshot_h */

View File

@@ -0,0 +1,57 @@
//
// AIRUrlTileOverlay.h
// AirMaps
//
// Created by cascadian on 3/19/16.
// Copyright © 2016. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import <React/RCTView.h>
#import "AIRMapCoordinate.h"
#import "AIRMap.h"
#import "RCTConvert+AirMap.h"
#import "AIRMapUrlTileCachedOverlay.h"
@interface AIRMapUrlTile : MKAnnotationView <MKOverlay> {
BOOL _urlTemplateSet;
BOOL _tileSizeSet;
BOOL _flipYSet;
BOOL _tileCachePathSet;
BOOL _tileCacheMaxAgeSet;
BOOL _maximumNativeZSet;
BOOL _cachedOverlayCreated;
BOOL _opacitySet;
}
@property (nonatomic, weak) AIRMap *map;
@property (nonatomic, strong) AIRMapUrlTileCachedOverlay *tileOverlay;
@property (nonatomic, strong) MKTileOverlayRenderer *renderer;
@property (nonatomic, copy) NSString *urlTemplate;
@property NSInteger maximumZ;
@property NSInteger maximumNativeZ;
@property NSInteger minimumZ;
@property BOOL flipY;
@property BOOL shouldReplaceMapContent;
@property NSInteger tileSize;
@property (nonatomic, copy) NSString *tileCachePath;
@property NSInteger tileCacheMaxAge;
@property BOOL offlineMode;
@property CGFloat opacity;
- (void)updateProperties;
- (void)update;
#pragma mark MKOverlay protocol
@property(nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property(nonatomic, readonly) MKMapRect boundingMapRect;
//- (BOOL)intersectsMapRect:(MKMapRect)mapRect;
- (BOOL)canReplaceMapContent;
@end

View File

@@ -0,0 +1,211 @@
//
// AIRUrlTileOverlay.m
// AirMaps
//
// Created by cascadian on 3/19/16.
// Copyright © 2016. All rights reserved.
//
#import "AIRMapUrlTile.h"
#import <React/UIView+React.h>
#import "AIRMapUrlTileCachedOverlay.h"
@implementation AIRMapUrlTile
- (void)setShouldReplaceMapContent:(BOOL)shouldReplaceMapContent
{
_shouldReplaceMapContent = shouldReplaceMapContent;
if (self.tileOverlay) {
self.tileOverlay.canReplaceMapContent = _shouldReplaceMapContent;
}
[self update];
}
- (void)setMaximumZ:(NSInteger)maximumZ
{
_maximumZ = maximumZ;
if (self.tileOverlay) {
self.tileOverlay.maximumZ = _maximumZ;
}
[self update];
}
- (void)setMaximumNativeZ:(NSInteger)maximumNativeZ
{
_maximumNativeZ = maximumNativeZ;
_maximumNativeZSet = YES;
if (_cachedOverlayCreated) {
self.tileOverlay.maximumNativeZ = _maximumNativeZ;
} else {
[self createTileOverlayAndRendererIfPossible];
}
[self update];
}
- (void)setMinimumZ:(NSInteger)minimumZ
{
_minimumZ = minimumZ;
if (self.tileOverlay) {
self.tileOverlay.minimumZ = _minimumZ;
}
[self update];
}
- (void)setFlipY:(BOOL)flipY
{
_flipY = flipY;
_flipYSet = YES;
if (self.tileOverlay) {
self.tileOverlay.geometryFlipped = _flipY;
}
[self update];
}
- (void)setUrlTemplate:(NSString *)urlTemplate
{
_urlTemplate = urlTemplate;
_urlTemplateSet = YES;
[self createTileOverlayAndRendererIfPossible];
[self update];
}
- (void)setTileSize:(NSInteger)tileSize
{
_tileSize = tileSize;
_tileSizeSet = YES;
[self createTileOverlayAndRendererIfPossible];
[self update];
}
- (void)setTileCachePath:(NSString *)tileCachePath{
if (!tileCachePath) return;
_tileCachePath = tileCachePath;
_tileCachePathSet = YES;
[self createTileOverlayAndRendererIfPossible];
[self update];
}
- (void)setTileCacheMaxAge:(NSInteger)tileCacheMaxAge{
_tileCacheMaxAge = tileCacheMaxAge;
_tileCacheMaxAgeSet = YES;
if (_cachedOverlayCreated) {
self.tileOverlay.tileCacheMaxAge = _tileCacheMaxAge;
} else {
[self createTileOverlayAndRendererIfPossible];
}
[self update];
}
- (void)setOfflineMode:(BOOL)offlineMode
{
_offlineMode = offlineMode;
if (_cachedOverlayCreated) {
self.tileOverlay.offlineMode = _offlineMode;
}
if (self.renderer) [self.renderer reloadData];
}
- (void)setOpacity:(CGFloat)opacity
{
_opacity = opacity;
_opacitySet = YES;
if (self.renderer) {
self.renderer.alpha = opacity;
} else {
[self createTileOverlayAndRendererIfPossible];
}
[self update];
}
- (void)createTileOverlayAndRendererIfPossible
{
if (!_urlTemplateSet) return;
if (_tileCachePathSet || _maximumNativeZSet) {
NSLog(@"tileCache dir %@", _tileCachePath);
self.tileOverlay = [[AIRMapUrlTileCachedOverlay alloc] initWithURLTemplate:self.urlTemplate];
_cachedOverlayCreated = YES;
if (_tileCachePathSet) {
NSURL *urlPath = [NSURL URLWithString:[self.tileCachePath stringByAppendingString:@"/"]];
if (urlPath.fileURL) {
self.tileOverlay.tileCachePath = urlPath;
} else {
NSURL *filePath = [NSURL fileURLWithPath:self.tileCachePath isDirectory:YES];
self.tileOverlay.tileCachePath = filePath;
}
if (_tileCacheMaxAgeSet) {
self.tileOverlay.tileCacheMaxAge = self.tileCacheMaxAge;
}
}
} else {
NSLog(@"tileCache normal overlay");
self.tileOverlay = [[MKTileOverlay alloc] initWithURLTemplate:self.urlTemplate];
_cachedOverlayCreated = NO;
}
[self updateProperties];
self.renderer = [[MKTileOverlayRenderer alloc] initWithTileOverlay:self.tileOverlay];
if (_opacitySet) {
self.renderer.alpha = self.opacity;
}
}
- (void)updateProperties
{
self.tileOverlay.canReplaceMapContent = self.shouldReplaceMapContent;
if(self.minimumZ) {
self.tileOverlay.minimumZ = self.minimumZ;
}
if (self.maximumZ) {
self.tileOverlay.maximumZ = self.maximumZ;
}
if (_cachedOverlayCreated && self.maximumNativeZ) {
self.tileOverlay.maximumNativeZ = self.maximumNativeZ;
}
if (_flipYSet) {
self.tileOverlay.geometryFlipped = self.flipY;
}
if (_tileSizeSet) {
self.tileOverlay.tileSize = CGSizeMake(self.tileSize, self.tileSize);
}
if (_cachedOverlayCreated && self.offlineMode) {
self.tileOverlay.offlineMode = self.offlineMode;
}
}
- (void)update
{
if (!_renderer) return;
if (_map == nil) return;
[_map removeOverlay:self];
[_map addOverlay:self level:MKOverlayLevelAboveLabels];
for (id<MKOverlay> overlay in _map.overlays) {
if ([overlay isKindOfClass:[AIRMapUrlTile class]]) {
continue;
}
[_map removeOverlay:overlay];
[_map addOverlay:overlay];
}
}
#pragma mark MKOverlay implementation
- (CLLocationCoordinate2D)coordinate
{
return self.tileOverlay.coordinate;
}
- (MKMapRect)boundingMapRect
{
return self.tileOverlay.boundingMapRect;
}
- (BOOL)canReplaceMapContent
{
return self.tileOverlay.canReplaceMapContent;
}
@end

View File

@@ -0,0 +1,17 @@
//
// AIRMapUrlTileCachedOverlay.h
// Airmaps
//
// Created by Markus Suomi on 10/04/2021.
//
#import <MapKit/MapKit.h>
@interface AIRMapUrlTileCachedOverlay : MKTileOverlay
@property NSInteger maximumNativeZ;
@property (nonatomic, copy) NSURL *tileCachePath;
@property NSInteger tileCacheMaxAge;
@property BOOL offlineMode;
@end

View File

@@ -0,0 +1,235 @@
//
// AIRMapUrlTileCachedOverlay.m
// Airmaps
//
// Created by Markus Suomi on 10/04/2021.
//
#import "AIRMapUrlTileCachedOverlay.h"
@interface AIRMapUrlTileCachedOverlay ()
@end
@implementation AIRMapUrlTileCachedOverlay {
CIContext *_ciContext;
CGColorSpaceRef _colorspace;
NSURLSession *_urlSession;
}
- (void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result
{
if (!result) return;
NSInteger maximumZ = self.maximumNativeZ ? self.maximumNativeZ : path.z;
[self scaleIfNeededLowerZoomTile:path maximumZ:maximumZ result:^(NSData *image, NSError *error) {
if (!image && self.offlineMode && self.tileCachePath) {
NSInteger zoomLevelToStart = (path.z > maximumZ) ? maximumZ - 1 : path.z - 1;
NSInteger minimumZoomToSearch = self.minimumZ >= zoomLevelToStart - 3 ? self.minimumZ : zoomLevelToStart - 3;
[self findLowerZoomTileAndScale:path tryZ:zoomLevelToStart minZ:minimumZoomToSearch result:result];
} else {
result(image, error);
}
}];
}
- (void)scaleIfNeededLowerZoomTile:(MKTileOverlayPath)path maximumZ:(NSInteger)maximumZ result:(void (^)(NSData *, NSError *))result
{
NSInteger overZoomLevel = path.z - maximumZ;
if (overZoomLevel <= 0) {
[self getTileImage:path result:result];
return;
}
NSInteger zoomFactor = 1 << overZoomLevel;
MKTileOverlayPath parentTile;
parentTile.x = path.x >> overZoomLevel;
parentTile.y = path.y >> overZoomLevel;
parentTile.z = path.z - overZoomLevel;
parentTile.contentScaleFactor = path.contentScaleFactor;
NSInteger xOffset = path.x % zoomFactor;
NSInteger yOffset = path.y % zoomFactor;
NSInteger subTileSize = self.tileSize.width / zoomFactor;
if (!_ciContext) _ciContext = [CIContext context];
if (!_colorspace) _colorspace = CGColorSpaceCreateDeviceRGB();
[self getTileImage:parentTile result:^(NSData *image, NSError *error) {
if (!image) {
result(nil, nil);
return;
}
CIImage* originalCIImage = [CIImage imageWithData:image];
CGRect rect;
rect.origin.x = xOffset * subTileSize;
rect.origin.y = self.tileSize.width - (yOffset + 1) * subTileSize;
rect.size.width = subTileSize;
rect.size.height = subTileSize;
CIVector *inputRect = [CIVector vectorWithCGRect:rect];
CIFilter* cropFilter = [CIFilter filterWithName:@"CICrop"];
[cropFilter setValue:originalCIImage forKey:@"inputImage"];
[cropFilter setValue:inputRect forKey:@"inputRectangle"];
CGAffineTransform trans = CGAffineTransformMakeScale(zoomFactor, zoomFactor);
CIImage* scaledCIImage = [cropFilter.outputImage imageByApplyingTransform:trans];
NSData *finalImage = [_ciContext PNGRepresentationOfImage:scaledCIImage format:kCIFormatABGR8 colorSpace:_colorspace options:nil];
result(finalImage, nil);
}];
}
- (void)findLowerZoomTileAndScale:(MKTileOverlayPath)path tryZ:(NSInteger)tryZ minZ:(NSInteger)minZ result:(void (^)(NSData *, NSError *))result
{
[self scaleIfNeededLowerZoomTile:path maximumZ:tryZ result:^(NSData *image, NSError *error) {
if (image) {
result(image, error);
} else if (tryZ >= minZ) {
[self findLowerZoomTileAndScale:path tryZ:tryZ - 1 minZ:minZ result:result];
} else {
result(nil, nil);
}
}];
}
- (void)getTileImage:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result
{
NSData *image;
NSURL *tileCacheFileDirectory = [NSURL URLWithString:[NSString stringWithFormat:@"%d/%d/", (int)path.z, (int)path.x] relativeToURL:self.tileCachePath];
NSURL *tileCacheFilePath = [NSURL URLWithString:[NSString stringWithFormat:@"%d", (int)path.y] relativeToURL:tileCacheFileDirectory];
if (self.tileCachePath) {
image = [self readTileImage:path fromFilePath:tileCacheFilePath];
if (image) {
result(image, nil);
if (!self.offlineMode && self.tileCacheMaxAge) {
[self checkForRefresh:path fromFilePath:tileCacheFilePath];
}
}
}
if (!image) {
if (!self.offlineMode) {
[self fetchTile:path result:^(NSData *image, NSError *error) {
result(image, error);
if (image && self.tileCachePath) {
[self writeTileImage:tileCacheFileDirectory withTileCacheFilePath:tileCacheFilePath withTileData:image];
}
}];
} else {
result(nil, nil);
}
}
}
- (NSData *)readTileImage:(MKTileOverlayPath)path fromFilePath:(NSURL *)tileCacheFilePath
{
NSError *error;
if ([[NSFileManager defaultManager] fileExistsAtPath:[tileCacheFilePath path]]) {
if (!self.tileCacheMaxAge) {
[[NSFileManager defaultManager] setAttributes:@{NSFileModificationDate:[NSDate date]}
ofItemAtPath:[tileCacheFilePath path]
error:&error];
}
NSData *tile = [NSData dataWithContentsOfFile:[tileCacheFilePath path]];
NSLog(@"tileCache HIT for %d_%d_%d", (int)path.z, (int)path.x, (int)path.y);
NSLog(@"tileCache HIT, with max age set at %d", self.tileCacheMaxAge);
return tile;
} else {
NSLog(@"tileCache MISS for %d_%d_%d", (int)path.z, (int)path.x, (int)path.y);
return nil;
}
}
- (void)fetchTile:(MKTileOverlayPath)path result:(void (^)(NSData *, NSError *))result
{
if (!_urlSession) [self createURLSession];
[[_urlSession dataTaskWithURL:[self URLForTilePath:path]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
result(data, error);
}] resume];
}
- (void)writeTileImage:(NSURL *)tileCacheFileDirectory withTileCacheFilePath:(NSURL *)tileCacheFilePath withTileData:(NSData *)data
{
NSError *error;
if (![[NSFileManager defaultManager] fileExistsAtPath:[tileCacheFileDirectory path]]) {
[[NSFileManager defaultManager] createDirectoryAtPath:[tileCacheFileDirectory path] withIntermediateDirectories:YES attributes:nil error:&error];
if (error) {
NSLog(@"Error: %@", error);
return;
}
}
[[NSFileManager defaultManager] createFileAtPath:[tileCacheFilePath path] contents:data attributes:nil];
NSLog(@"tileCache SAVED tile %@", [tileCacheFilePath path]);
}
- (void)createTileCacheDirectory
{
NSError *error;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *tileCacheBaseDirectory = [NSString stringWithFormat:@"%@/tileCache", documentsDirectory];
self.tileCachePath = [NSURL fileURLWithPath:tileCacheBaseDirectory isDirectory:YES];
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.tileCachePath path]])
[[NSFileManager defaultManager] createDirectoryAtPath:[self.tileCachePath path] withIntermediateDirectories:NO attributes:nil error:&error];
}
- (void)createURLSession
{
if (!_urlSession) {
_urlSession = [NSURLSession sharedSession];
}
}
- (void)checkForRefresh:(MKTileOverlayPath)path fromFilePath:(NSURL *)tileCacheFilePath
{
if ([self doesFileNeedRefresh:path fromFilePath:tileCacheFilePath withMaxAge:self.tileCacheMaxAge]) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^ {
// This code runs asynchronously!
if ([self doesFileNeedRefresh:path fromFilePath:tileCacheFilePath withMaxAge:self.tileCacheMaxAge]) {
if (!_urlSession) [self createURLSession];
[[_urlSession dataTaskWithURL:[self URLForTilePath:path]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
if (!error) {
[[NSFileManager defaultManager] createFileAtPath:[tileCacheFilePath path] contents:data attributes:nil];
NSLog(@"tileCache File refreshed at %@", [tileCacheFilePath path]);
}
}] resume];
}
});
}
}
- (BOOL)doesFileNeedRefresh:(MKTileOverlayPath)path fromFilePath:(NSURL *)tileCacheFilePath withMaxAge:(NSInteger)tileCacheMaxAge
{
NSError *error;
NSDictionary<NSFileAttributeKey, id> *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[tileCacheFilePath path] error:&error];
if (fileAttributes) {
NSDate *modificationDate = fileAttributes[@"NSFileModificationDate"];
if (modificationDate) {
if (-1 * (int)modificationDate.timeIntervalSinceNow > tileCacheMaxAge) {
return YES;
}
}
}
return NO;
}
@end

View File

@@ -0,0 +1,14 @@
//
// AIRMapUrlTileManager.h
// AirMaps
//
// Created by cascadian on 3/19/16.
// Copyright © 2016. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface AIRMapUrlTileManager : RCTViewManager
@end

View File

@@ -0,0 +1,47 @@
//
// AIRMapUrlTileManager.m
// AirMaps
//
// Created by cascadian on 3/19/16.
// Copyright © 2016. All rights reserved.
//
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "AIRMapMarker.h"
#import "AIRMapUrlTile.h"
#import "AIRMapUrlTileManager.h"
@interface AIRMapUrlTileManager()
@end
@implementation AIRMapUrlTileManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapUrlTile *tile = [AIRMapUrlTile new];
return tile;
}
RCT_EXPORT_VIEW_PROPERTY(urlTemplate, NSString)
RCT_EXPORT_VIEW_PROPERTY(maximumZ, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(maximumNativeZ, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(minimumZ, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(flipY, BOOL)
RCT_EXPORT_VIEW_PROPERTY(shouldReplaceMapContent, BOOL)
RCT_EXPORT_VIEW_PROPERTY(tileSize, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(tileCachePath, NSString)
RCT_EXPORT_VIEW_PROPERTY(tileCacheMaxAge, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(offlineMode, BOOL)
RCT_EXPORT_VIEW_PROPERTY(opacity, CGFloat)
@end

View File

@@ -0,0 +1,35 @@
//
// AIRMapWMSTile.h
// AirMaps
//
// Created by nizam on 10/28/18.
// Copyright © 2018. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import <React/RCTView.h>
#import "AIRMapCoordinate.h"
#import "AIRMap.h"
#import "RCTConvert+AirMap.h"
#import "AIRMapUrlTile.h"
#import "AIRMapUrlTileCachedOverlay.h"
@interface AIRMapWMSTile : AIRMapUrlTile <MKOverlay>
@end
@interface AIRMapWMSTileOverlay : MKTileOverlay
@end
@interface AIRMapWMSTileCachedOverlay : AIRMapUrlTileCachedOverlay
@end
@interface AIRMapWMSTileHelper : NSObject
+ (NSURL *)URLForTilePath:(MKTileOverlayPath)path withURLTemplate:(NSString *)URLTemplate withTileSize:(NSInteger)tileSize;
@end

View File

@@ -0,0 +1,116 @@
//
// AIRMapWMSTile.m
// AirMaps
//
// Created by nizam on 10/28/18.
// Copyright © 2018. All rights reserved.
//
#import "AIRMapWMSTile.h"
#import <React/UIView+React.h>
@implementation AIRMapWMSTile
- (void)createTileOverlayAndRendererIfPossible
{
if (!_urlTemplateSet) return;
if (_tileCachePathSet || _maximumNativeZSet) {
NSLog(@"tileCache new overlay dir %@", self.tileCachePath);
NSLog(@"tileCache %d", _tileCachePathSet);
NSLog(@"tileCache %d", _maximumNativeZSet);
self.tileOverlay = [[AIRMapWMSTileCachedOverlay alloc] initWithURLTemplate:self.urlTemplate];
_cachedOverlayCreated = YES;
if (_tileCachePathSet) {
NSURL *urlPath = [NSURL URLWithString:[self.tileCachePath stringByAppendingString:@"/"]];
if (urlPath.fileURL) {
self.tileOverlay.tileCachePath = urlPath;
} else {
NSURL *filePath = [NSURL fileURLWithPath:self.tileCachePath isDirectory:YES];
self.tileOverlay.tileCachePath = filePath;
}
if (_tileCacheMaxAgeSet) {
self.tileOverlay.tileCacheMaxAge = self.tileCacheMaxAge;
}
}
} else {
NSLog(@"tileCache normal overlay");
self.tileOverlay = [[AIRMapWMSTileOverlay alloc] initWithURLTemplate:self.urlTemplate];
_cachedOverlayCreated = NO;
}
[self updateProperties];
self.renderer = [[MKTileOverlayRenderer alloc] initWithTileOverlay:self.tileOverlay];
if (_opacitySet) {
self.renderer.alpha = self.opacity;
}
}
@end
@implementation AIRMapWMSTileOverlay
- (id)initWithURLTemplate:(NSString *)URLTemplate
{
self = [super initWithURLTemplate:URLTemplate];
return self;
}
- (NSURL *)URLForTilePath:(MKTileOverlayPath)path
{
return [AIRMapWMSTileHelper URLForTilePath:path withURLTemplate:self.URLTemplate withTileSize:self.tileSize.width];
}
@end
@implementation AIRMapWMSTileCachedOverlay
- (id)initWithURLTemplate:(NSString *)URLTemplate
{
self = [super initWithURLTemplate:URLTemplate];
return self;
}
- (NSURL *)URLForTilePath:(MKTileOverlayPath)path
{
return [AIRMapWMSTileHelper URLForTilePath:path withURLTemplate:self.URLTemplate withTileSize:self.tileSize.width];
}
@end
@implementation AIRMapWMSTileHelper
+ (NSURL *)URLForTilePath:(MKTileOverlayPath)path withURLTemplate:(NSString *)URLTemplate withTileSize:(NSInteger)tileSize
{
NSArray *bb = [self getBoundBox:path.x yAxis:path.y zoom:path.z];
NSMutableString *url = [URLTemplate mutableCopy];
[url replaceOccurrencesOfString: @"{minX}" withString:[NSString stringWithFormat:@"%@", bb[0]] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{minY}" withString:[NSString stringWithFormat:@"%@", bb[1]] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{maxX}" withString:[NSString stringWithFormat:@"%@", bb[2]] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{maxY}" withString:[NSString stringWithFormat:@"%@", bb[3]] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{width}" withString:[NSString stringWithFormat:@"%d", (int)tileSize] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{height}" withString:[NSString stringWithFormat:@"%d", (int)tileSize] options:0 range:NSMakeRange(0, url.length)];
return [NSURL URLWithString:url];
}
+ (NSArray *)getBoundBox:(NSInteger)x yAxis:(NSInteger)y zoom:(NSInteger)zoom
{
double MapX = -20037508.34789244;
double MapY = 20037508.34789244;
double FULL = 20037508.34789244 * 2;
double tile = FULL / pow(2.0, (double)zoom);
NSArray *result =[[NSArray alloc] initWithObjects:
[NSNumber numberWithDouble:MapX + (double)x * tile],
[NSNumber numberWithDouble:MapY - (double)(y + 1) * tile],
[NSNumber numberWithDouble:MapX + (double)(x + 1) * tile],
[NSNumber numberWithDouble:MapY - (double)y * tile],
nil];
return result;
}
@end

View File

@@ -0,0 +1,14 @@
//
// AIRMapWMSTileManager.h
// AirMaps
//
// Created by nizam on 10/28/18.
// Copyright © 2018. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface AIRMapWMSTileManager : RCTViewManager
@end

View File

@@ -0,0 +1,47 @@
//
// AIRMapWMSTileManager.m
// AirMaps
//
// Created by nizam on 10/28/18.
// Copyright © 2018. All rights reserved.
//
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "AIRMapMarker.h"
#import "AIRMapWMSTile.h"
#import "AIRMapWMSTileManager.h"
@interface AIRMapWMSTileManager()
@end
@implementation AIRMapWMSTileManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRMapWMSTile *tile = [AIRMapWMSTile new];
return tile;
}
RCT_EXPORT_VIEW_PROPERTY(urlTemplate, NSString)
RCT_EXPORT_VIEW_PROPERTY(maximumZ, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(maximumNativeZ, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(minimumZ, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(shouldReplaceMapContent, BOOL)
RCT_EXPORT_VIEW_PROPERTY(tileSize, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(tileCachePath, NSString)
RCT_EXPORT_VIEW_PROPERTY(tileCacheMaxAge, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(offlineMode, BOOL)
RCT_EXPORT_VIEW_PROPERTY(opacity, CGFloat)
@end

View File

@@ -0,0 +1,24 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Foundation/Foundation.h>
#import "AIRMap.h"
NS_ASSUME_NONNULL_BEGIN
@interface AIRWeakMapReference : NSObject
@property (nonatomic, weak) AIRMap *mapView;
- (instancetype)initWithMapView:(AIRMap *)mapView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,23 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRWeakMapReference.h"
@implementation AIRWeakMapReference
- (instancetype)initWithMapView:(AIRMap *)mapView {
self = [super init];
if (self) {
_mapView = mapView;
}
return self;
}
@end

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AIRWeakTimerReference : NSObject
- (instancetype)initWithTarget:(id)target andSelector:(SEL)selector;
- (void)timerDidFire:(NSTimer *)timer;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRWeakTimerReference.h"
@implementation AIRWeakTimerReference
{
__weak NSObject *_target;
SEL _selector;
}
- (instancetype)initWithTarget:(id)target andSelector:(SEL)selector {
self = [super init];
if (self) {
_target = target;
_selector = selector;
}
return self;
}
- (void)timerDidFire:(NSTimer *)timer
{
if(_target)
{
[_target performSelector:_selector withObject:timer];
}
else
{
[timer invalidate];
}
}
@end

View File

@@ -0,0 +1,204 @@
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
/*
SMCalloutView
-------------
Created by Nick Farina (nfarina@gmail.com)
Version 2.1.2
*/
/// options for which directions the callout is allowed to "point" in.
typedef NS_OPTIONS(NSUInteger, SMCalloutArrowDirection) {
SMCalloutArrowDirectionUp = 1 << 0,
SMCalloutArrowDirectionDown = 1 << 1,
SMCalloutArrowDirectionAny = SMCalloutArrowDirectionUp | SMCalloutArrowDirectionDown
};
/// options for the callout present/dismiss animation
typedef NS_ENUM(NSInteger, SMCalloutAnimation) {
/// the "bounce" animation we all know and love from @c UIAlertView
SMCalloutAnimationBounce,
/// a simple fade in or out
SMCalloutAnimationFade,
/// grow or shrink linearly, like in the iPad Calendar app
SMCalloutAnimationStretch
};
NS_ASSUME_NONNULL_BEGIN
/// when delaying our popup in order to scroll content into view, you can use this amount to match the
/// animation duration of UIScrollView when using @c -setContentOffset:animated.
extern NSTimeInterval const kSMCalloutViewRepositionDelayForUIScrollView;
@protocol SMCalloutViewDelegate;
@class SMCalloutBackgroundView;
//
// Callout view.
//
#if __IPHONE_OS_VERSION_MAX_ALLOWED < 100000
@interface SMCalloutView : UIView
#else
@interface SMCalloutView : UIView <CAAnimationDelegate>
#endif
@property (nonatomic, weak, nullable) id<SMCalloutViewDelegate> delegate;
/// title/titleView relationship mimics UINavigationBar.
@property (nonatomic, copy, nullable) NSString *title;
@property (nonatomic, copy, nullable) NSString *subtitle;
/// Left accessory view for the call out
@property (nonatomic, strong, nullable) UIView *leftAccessoryView;
/// Right accessoty view for the call out
@property (nonatomic, strong, nullable) UIView *rightAccessoryView;
/// Default @c SMCalloutArrowDirectionDown
@property (nonatomic, assign) SMCalloutArrowDirection permittedArrowDirection;
/// The current arrow direction
@property (nonatomic, readonly) SMCalloutArrowDirection currentArrowDirection;
/// if the @c UIView you're constraining to has portions that are overlapped by nav bar, tab bar, etc. you'll need to tell us those insets.
@property (nonatomic, assign) UIEdgeInsets constrainedInsets;
/// default is @c SMCalloutMaskedBackgroundView, or @c SMCalloutDrawnBackgroundView when using @c SMClassicCalloutView
@property (nonatomic, strong) SMCalloutBackgroundView *backgroundView;
/**
@brief Custom title view.
@disucssion Keep in mind that @c SMCalloutView calls @c -sizeThatFits on titleView/subtitleView if defined, so your view
may be resized as a result of that (especially if you're using @c UILabel/UITextField). You may want to subclass and override @c -sizeThatFits, or just wrap your view in a "generic" @c UIView if you do not want it to be auto-sized.
@warning If this is set, the respective @c title property will be ignored.
*/
@property (nonatomic, strong, nullable) UIView *titleView;
/**
@brief Custom subtitle view.
@discussion Keep in mind that @c SMCalloutView calls @c -sizeThatFits on subtitleView if defined, so your view
may be resized as a result of that (especially if you're using @c UILabel/UITextField). You may want to subclass and override @c -sizeThatFits, or just wrap your view in a "generic" @c UIView if you do not want it to be auto-sized.
@warning If this is set, the respective @c subtitle property will be ignored.
*/
@property (nonatomic, strong, nullable) UIView *subtitleView;
/// Custom "content" view that can be any width/height. If this is set, title/subtitle/titleView/subtitleView are all ignored.
@property (nonatomic, retain, nullable) UIView *contentView;
/// Custom content view margin
@property (nonatomic, assign) UIEdgeInsets contentViewInset;
/// calloutOffset is the offset in screen points from the top-middle of the target view, where the anchor of the callout should be shown.
@property (nonatomic, assign) CGPoint calloutOffset;
/// default SMCalloutAnimationBounce, SMCalloutAnimationFade respectively
@property (nonatomic, assign) SMCalloutAnimation presentAnimation, dismissAnimation;
/// Returns a new instance of SMCalloutView if running on iOS 7 or better, otherwise a new instance of SMClassicCalloutView if available.
+ (SMCalloutView *)platformCalloutView;
/**
@brief Presents a callout view by adding it to "inView" and pointing at the given rect of inView's bounds.
@discussion Constrains the callout to the bounds of the given view. Optionally scrolls the given rect into view (plus margins)
if @c -delegate is set and responds to @c -delayForRepositionWithSize.
@param rect @c CGRect to present the view from
@param view view to 'constrain' the @c constrainedView to
@param constrainedView @c UIView to be constrainted in @c view
@param animated @c BOOL if presentation should be animated
*/
- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated;
/**
@brief Present a callout layer in the `layer` and pointing at the given rect of the `layer` bounds
@discussion Same as the view-based presentation, but inserts the callout into a CALayer hierarchy instead.
@note Be aware that you'll have to direct your own touches to any accessory views, since CALayer doesn't relay touch events.
@param rect @c CGRect to present the view from
@param layer layer to 'constrain' the @c constrainedLayer to
@param constrainedLayer @c CALayer to be constrained in @c layer
@param animated @c BOOL if presentation should be animated
*/
- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated;
/**
Dismiss the callout view
@param animated @c BOOL if dismissal should be animated
*/
- (void)dismissCalloutAnimated:(BOOL)animated;
/// For subclassers. You can override this method to provide your own custom animation for presenting/dismissing the callout.
- (CAAnimation *)animationWithType:(SMCalloutAnimation)type presenting:(BOOL)presenting;
@end
//
// Background view - default draws the iOS 7 system background style (translucent white with rounded arrow).
//
/// Abstract base class
@interface SMCalloutBackgroundView : UIView
/// indicates where the tip of the arrow should be drawn, as a pixel offset
@property (nonatomic, assign) CGPoint arrowPoint;
/// will be set by the callout when the callout is in a highlighted state
@property (nonatomic, assign) BOOL highlighted;
/// returns an optional layer whose contents should mask the callout view's contents (not honored by @c SMClassicCalloutView )
@property (nonatomic, assign) CALayer *contentMask;
/// height of the callout "arrow"
@property (nonatomic, assign) CGFloat anchorHeight;
/// the smallest possible distance from the edge of our control to the "tip" of the anchor, from either left or right
@property (nonatomic, assign) CGFloat anchorMargin;
@end
/// Default for iOS 7, this reproduces the "masked" behavior of the iOS 7-style callout view.
/// Accessories are masked by the shape of the callout (including the arrow itself).
@interface SMCalloutMaskedBackgroundView : SMCalloutBackgroundView
@end
//
// Delegate methods
//
@protocol SMCalloutViewDelegate <NSObject>
@optional
/// Controls whether the callout "highlights" when pressed. default YES. You must also respond to @c -calloutViewClicked below.
/// Not honored by @c SMClassicCalloutView.
- (BOOL)calloutViewShouldHighlight:(SMCalloutView *)calloutView;
/// Called when the callout view is clicked. Not honored by @c SMClassicCalloutView.
- (void)calloutViewClicked:(SMCalloutView *)calloutView;
/**
Called when the callout view detects that it will be outside the constrained view when it appears,
or if the target rect was already outside the constrained view. You can implement this selector
to respond to this situation by repositioning your content first in order to make everything visible.
The @c CGSize passed is the calculated offset necessary to make everything visible (plus a nice margin).
It expects you to return the amount of time you need to reposition things so the popup can be delayed.
Typically you would return @c kSMCalloutViewRepositionDelayForUIScrollView if you're repositioning by calling @c [UIScrollView @c setContentOffset:animated:].
@param calloutView the @c SMCalloutView to reposition
@param offset calculated offset necessary to make everything visible
@returns @c NSTimeInterval to delay the repositioning
*/
- (NSTimeInterval)calloutView:(SMCalloutView *)calloutView delayForRepositionWithSize:(CGSize)offset;
/// Called before the callout view appears on screen, or before the appearance animation will start.
- (void)calloutViewWillAppear:(SMCalloutView *)calloutView;
/// Called after the callout view appears on screen, or after the appearance animation is complete.
- (void)calloutViewDidAppear:(SMCalloutView *)calloutView;
/// Called before the callout view is removed from the screen, or before the disappearance animation is complete.
- (void)calloutViewWillDisappear:(SMCalloutView *)calloutView;
/// Called after the callout view is removed from the screen, or after the disappearance animation is complete.
- (void)calloutViewDidDisappear:(SMCalloutView *)calloutView;
NS_ASSUME_NONNULL_END
@end

View File

@@ -0,0 +1,858 @@
#import "SMCalloutView.h"
//
// UIView frame helpers - we do a lot of UIView frame fiddling in this class; these functions help keep things readable.
//
@interface UIView (SMFrameAdditions)
@property (nonatomic, assign) CGPoint frameOrigin;
@property (nonatomic, assign) CGSize frameSize;
@property (nonatomic, assign) CGFloat frameX, frameY, frameWidth, frameHeight; // normal rect properties
@property (nonatomic, assign) CGFloat frameLeft, frameTop, frameRight, frameBottom; // these will stretch/shrink the rect
@end
//
// Callout View.
//
#define CALLOUT_DEFAULT_CONTAINER_HEIGHT 44 // height of just the main portion without arrow
#define CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT 52 // height of just the main portion without arrow (when subtitle is present)
#define CALLOUT_MIN_WIDTH 61 // minimum width of system callout
#define TITLE_HMARGIN 12 // the title/subtitle view's normal horizontal margin from the edges of our callout view or from the accessories
#define TITLE_TOP 11 // the top of the title view when no subtitle is present
#define TITLE_SUB_TOP 4 // the top of the title view when a subtitle IS present
#define TITLE_HEIGHT 21 // title height, fixed
#define SUBTITLE_TOP 28 // the top of the subtitle, when present
#define SUBTITLE_HEIGHT 15 // subtitle height, fixed
#define BETWEEN_ACCESSORIES_MARGIN 7 // margin between accessories when no title/subtitle is present
#define TOP_ANCHOR_MARGIN 13 // all the above measurements assume a bottom anchor! if we're pointing "up" we'll need to add this top margin to everything.
#define COMFORTABLE_MARGIN 10 // when we try to reposition content to be visible, we'll consider this margin around your target rect
NSTimeInterval const kSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0;
@interface SMCalloutView ()
@property (nonatomic, strong) UIButton *containerView; // for masking and interaction
@property (nonatomic, strong) UILabel *titleLabel, *subtitleLabel;
@property (nonatomic, assign) SMCalloutArrowDirection currentArrowDirection;
@property (nonatomic, assign) BOOL popupCancelled;
@end
@implementation SMCalloutView
+ (SMCalloutView *)platformCalloutView {
// if you haven't compiled SMClassicCalloutView into your app, then we can't possibly create an instance of it!
if (!NSClassFromString(@"SMClassicCalloutView"))
return [SMCalloutView new];
// ok we have both - so choose the best one based on current platform
if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1)
return [SMCalloutView new]; // iOS 7+
else
return [NSClassFromString(@"SMClassicCalloutView") new];
}
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.permittedArrowDirection = SMCalloutArrowDirectionDown;
self.presentAnimation = SMCalloutAnimationBounce;
self.dismissAnimation = SMCalloutAnimationFade;
self.backgroundColor = [UIColor clearColor];
self.containerView = [UIButton new];
self.containerView.isAccessibilityElement = NO;
self.isAccessibilityElement = NO;
self.contentViewInset = UIEdgeInsetsMake(12, 12, 12, 12);
[self.containerView addTarget:self action:@selector(highlightIfNecessary) forControlEvents:UIControlEventTouchDown | UIControlEventTouchDragInside];
[self.containerView addTarget:self action:@selector(unhighlightIfNecessary) forControlEvents:UIControlEventTouchDragOutside | UIControlEventTouchCancel | UIControlEventTouchUpOutside | UIControlEventTouchUpInside];
[self.containerView addTarget:self action:@selector(calloutClicked) forControlEvents:UIControlEventTouchUpInside];
}
return self;
}
- (BOOL)supportsHighlighting {
if (![self.delegate respondsToSelector:@selector(calloutViewClicked:)])
return NO;
if ([self.delegate respondsToSelector:@selector(calloutViewShouldHighlight:)])
return [self.delegate calloutViewShouldHighlight:self];
return YES;
}
- (void)highlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = YES; }
- (void)unhighlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = NO; }
- (void)calloutClicked {
if ([self.delegate respondsToSelector:@selector(calloutViewClicked:)])
[self.delegate calloutViewClicked:self];
}
- (UIView *)titleViewOrDefault {
if (self.titleView)
// if you have a custom title view defined, return that.
return self.titleView;
else {
if (!self.titleLabel) {
// create a default titleView
self.titleLabel = [UILabel new];
self.titleLabel.frameHeight = TITLE_HEIGHT;
self.titleLabel.opaque = NO;
self.titleLabel.backgroundColor = [UIColor clearColor];
self.titleLabel.font = [UIFont systemFontOfSize:17];
self.titleLabel.textColor = [UIColor blackColor];
}
return self.titleLabel;
}
}
- (UIView *)subtitleViewOrDefault {
if (self.subtitleView)
// if you have a custom subtitle view defined, return that.
return self.subtitleView;
else {
if (!self.subtitleLabel) {
// create a default subtitleView
self.subtitleLabel = [UILabel new];
self.subtitleLabel.frameHeight = SUBTITLE_HEIGHT;
self.subtitleLabel.opaque = NO;
self.subtitleLabel.backgroundColor = [UIColor clearColor];
self.subtitleLabel.font = [UIFont systemFontOfSize:12];
self.subtitleLabel.textColor = [UIColor blackColor];
}
return self.subtitleLabel;
}
}
- (SMCalloutBackgroundView *)backgroundView {
// create our default background on first access only if it's nil, since you might have set your own background anyway.
return _backgroundView ? _backgroundView : (_backgroundView = [self defaultBackgroundView]);
}
- (SMCalloutBackgroundView *)defaultBackgroundView {
return [SMCalloutMaskedBackgroundView new];
}
- (void)rebuildSubviews {
// remove and re-add our appropriate subviews in the appropriate order
[self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self.containerView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self setNeedsDisplay];
[self addSubview:self.backgroundView];
[self addSubview:self.containerView];
if (self.contentView) {
[self.containerView addSubview:self.contentView];
}
else {
if (self.titleViewOrDefault) [self.containerView addSubview:self.titleViewOrDefault];
if (self.subtitleViewOrDefault) [self.containerView addSubview:self.subtitleViewOrDefault];
}
if (self.leftAccessoryView) [self.containerView addSubview:self.leftAccessoryView];
if (self.rightAccessoryView) [self.containerView addSubview:self.rightAccessoryView];
}
// Accessory margins. Accessories are centered vertically when shorter
// than the callout, otherwise they grow from the upper corner.
- (CGFloat)leftAccessoryVerticalMargin {
if (self.leftAccessoryView.frameHeight < self.calloutContainerHeight)
return roundf((self.calloutContainerHeight - self.leftAccessoryView.frameHeight) / 2);
else
return 0;
}
- (CGFloat)leftAccessoryHorizontalMargin {
return fminf(self.leftAccessoryVerticalMargin, TITLE_HMARGIN);
}
- (CGFloat)rightAccessoryVerticalMargin {
if (self.rightAccessoryView.frameHeight < self.calloutContainerHeight)
return roundf((self.calloutContainerHeight - self.rightAccessoryView.frameHeight) / 2);
else
return 0;
}
- (CGFloat)rightAccessoryHorizontalMargin {
return fminf(self.rightAccessoryVerticalMargin, TITLE_HMARGIN);
}
- (CGFloat)innerContentMarginLeft {
if (self.leftAccessoryView)
return self.leftAccessoryHorizontalMargin + self.leftAccessoryView.frameWidth + TITLE_HMARGIN;
else
return self.contentViewInset.left;
}
- (CGFloat)innerContentMarginRight {
if (self.rightAccessoryView)
return self.rightAccessoryHorizontalMargin + self.rightAccessoryView.frameWidth + TITLE_HMARGIN;
else
return self.contentViewInset.right;
}
- (CGFloat)calloutHeight {
return self.calloutContainerHeight + self.backgroundView.anchorHeight;
}
- (CGFloat)calloutContainerHeight {
if (self.contentView)
return self.contentView.frameHeight + self.contentViewInset.bottom + self.contentViewInset.top;
else if (self.subtitleView || self.subtitle.length > 0)
return CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT;
else
return CALLOUT_DEFAULT_CONTAINER_HEIGHT;
}
- (CGSize)sizeThatFits:(CGSize)size {
// calculate how much non-negotiable space we need to reserve for margin and accessories
CGFloat margin = self.innerContentMarginLeft + self.innerContentMarginRight;
// how much room is left for text?
CGFloat availableWidthForText = size.width - margin - 1;
// no room for text? then we'll have to squeeze into the given size somehow.
if (availableWidthForText < 0)
availableWidthForText = 0;
CGSize preferredTitleSize = [self.titleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, TITLE_HEIGHT)];
CGSize preferredSubtitleSize = [self.subtitleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, SUBTITLE_HEIGHT)];
// total width we'd like
CGFloat preferredWidth;
if (self.contentView) {
// if we have a content view, then take our preferred size directly from that
preferredWidth = self.contentView.frameWidth + margin;
}
else if (preferredTitleSize.width >= 0.000001 || preferredSubtitleSize.width >= 0.000001) {
// if we have a title or subtitle, then our assumed margins are valid, and we can apply them
preferredWidth = fmaxf(preferredTitleSize.width, preferredSubtitleSize.width) + margin;
}
else {
// ok we have no title or subtitle to speak of. In this case, the system callout would actually not display
// at all! But we can handle it.
preferredWidth = self.leftAccessoryView.frameWidth + self.rightAccessoryView.frameWidth + self.leftAccessoryHorizontalMargin + self.rightAccessoryHorizontalMargin;
if (self.leftAccessoryView && self.rightAccessoryView)
preferredWidth += BETWEEN_ACCESSORIES_MARGIN;
}
// ensure we're big enough to fit our graphics!
preferredWidth = fmaxf(preferredWidth, CALLOUT_MIN_WIDTH);
// ask to be smaller if we have space, otherwise we'll fit into what we have by truncating the title/subtitle.
return CGSizeMake(fminf(preferredWidth, size.width), self.calloutHeight);
}
- (CGSize)offsetToContainRect:(CGRect)innerRect inRect:(CGRect)outerRect {
CGFloat nudgeRight = fmaxf(0, CGRectGetMinX(outerRect) - CGRectGetMinX(innerRect));
CGFloat nudgeLeft = fminf(0, CGRectGetMaxX(outerRect) - CGRectGetMaxX(innerRect));
CGFloat nudgeTop = fmaxf(0, CGRectGetMinY(outerRect) - CGRectGetMinY(innerRect));
CGFloat nudgeBottom = fminf(0, CGRectGetMaxY(outerRect) - CGRectGetMaxY(innerRect));
return CGSizeMake(nudgeLeft ? nudgeLeft : nudgeRight, nudgeTop ? nudgeTop : nudgeBottom);
}
- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated {
[self presentCalloutFromRect:rect inLayer:view.layer ofView:view constrainedToLayer:constrainedView.layer animated:animated];
}
- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated {
[self presentCalloutFromRect:rect inLayer:layer ofView:nil constrainedToLayer:constrainedLayer animated:animated];
}
// this private method handles both CALayer and UIView parents depending on what's passed.
- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer ofView:(UIView *)view constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated {
// Sanity check: dismiss this callout immediately if it's displayed somewhere
if (self.layer.superlayer) [self dismissCalloutAnimated:NO];
// cancel any presenting animation that may be in progress
[self.layer removeAnimationForKey:@"present"];
// figure out the constrained view's rect in our popup view's coordinate system
CGRect constrainedRect = [constrainedLayer convertRect:constrainedLayer.bounds toLayer:layer];
// apply our edge constraints
constrainedRect = UIEdgeInsetsInsetRect(constrainedRect, self.constrainedInsets);
constrainedRect = CGRectInset(constrainedRect, COMFORTABLE_MARGIN, COMFORTABLE_MARGIN);
// form our subviews based on our content set so far
[self rebuildSubviews];
// apply title/subtitle (if present
self.titleLabel.text = self.title;
self.subtitleLabel.text = self.subtitle;
// size the callout to fit the width constraint as best as possible
self.frameSize = [self sizeThatFits:CGSizeMake(constrainedRect.size.width, self.calloutHeight)];
// how much room do we have in the constraint box, both above and below our target rect?
CGFloat topSpace = CGRectGetMinY(rect) - CGRectGetMinY(constrainedRect);
CGFloat bottomSpace = CGRectGetMaxY(constrainedRect) - CGRectGetMaxY(rect);
// we prefer to point our arrow down.
SMCalloutArrowDirection bestDirection = SMCalloutArrowDirectionDown;
// we'll point it up though if that's the only option you gave us.
if (self.permittedArrowDirection == SMCalloutArrowDirectionUp)
bestDirection = SMCalloutArrowDirectionUp;
// or, if we don't have enough space on the top and have more space on the bottom, and you
// gave us a choice, then pointing up is the better option.
if (self.permittedArrowDirection == SMCalloutArrowDirectionAny && topSpace < self.calloutHeight && bottomSpace > topSpace)
bestDirection = SMCalloutArrowDirectionUp;
self.currentArrowDirection = bestDirection;
// we want to point directly at the horizontal center of the given rect. calculate our "anchor point" in terms of our
// target view's coordinate system. make sure to offset the anchor point as requested if necessary.
CGFloat anchorX = self.calloutOffset.x + CGRectGetMidX(rect);
CGFloat anchorY = self.calloutOffset.y + (bestDirection == SMCalloutArrowDirectionDown ? CGRectGetMinY(rect) : CGRectGetMaxY(rect));
// we prefer to sit centered directly above our anchor
CGFloat calloutX = roundf(anchorX - self.frameWidth / 2);
// but not if it's going to get too close to the edge of our constraints
if (calloutX < constrainedRect.origin.x)
calloutX = constrainedRect.origin.x;
if (calloutX > constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth)
calloutX = constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth;
// what's the farthest to the left and right that we could point to, given our background image constraints?
CGFloat minPointX = calloutX + self.backgroundView.anchorMargin;
CGFloat maxPointX = calloutX + self.frameWidth - self.backgroundView.anchorMargin;
// we may need to scoot over to the left or right to point at the correct spot
CGFloat adjustX = 0;
if (anchorX < minPointX) adjustX = anchorX - minPointX;
if (anchorX > maxPointX) adjustX = anchorX - maxPointX;
// add the callout to the given layer (or view if possible, to receive touch events)
if (view)
[view addSubview:self];
else
[layer addSublayer:self.layer];
CGPoint calloutOrigin = {
.x = calloutX + adjustX,
.y = bestDirection == SMCalloutArrowDirectionDown ? (anchorY - self.calloutHeight) : anchorY
};
self.frameOrigin = calloutOrigin;
// now set the *actual* anchor point for our layer so that our "popup" animation starts from this point.
CGPoint anchorPoint = [layer convertPoint:CGPointMake(anchorX, anchorY) toLayer:self.layer];
// pass on the anchor point to our background view so it knows where to draw the arrow
self.backgroundView.arrowPoint = anchorPoint;
// adjust it to unit coordinates for the actual layer.anchorPoint property
anchorPoint.x /= self.frameWidth;
anchorPoint.y /= self.frameHeight;
self.layer.anchorPoint = anchorPoint;
// setting the anchor point moves the view a bit, so we need to reset
self.frameOrigin = calloutOrigin;
// make sure our frame is not on half-pixels or else we may be blurry!
CGFloat scale = [UIScreen mainScreen].scale;
self.frameX = floorf(self.frameX*scale)/scale;
self.frameY = floorf(self.frameY*scale)/scale;
// layout now so we can immediately start animating to the final position if needed
[self setNeedsLayout];
[self layoutIfNeeded];
// if we're outside the bounds of our constraint rect, we'll give our delegate an opportunity to shift us into position.
// consider both our size and the size of our target rect (which we'll assume to be the size of the content you want to scroll into view.
CGRect contentRect = CGRectUnion(self.frame, rect);
CGSize offset = [self offsetToContainRect:contentRect inRect:constrainedRect];
NSTimeInterval delay = 0;
self.popupCancelled = NO; // reset this before calling our delegate below
if ([self.delegate respondsToSelector:@selector(calloutView:delayForRepositionWithSize:)] && !CGSizeEqualToSize(offset, CGSizeZero))
delay = [self.delegate calloutView:(id)self delayForRepositionWithSize:offset];
// there's a chance that user code in the delegate method may have called -dismissCalloutAnimated to cancel things; if that
// happened then we need to bail!
if (self.popupCancelled) return;
// now we want to mask our contents to our background view (if requested) to match the iOS 7 style
self.containerView.layer.mask = self.backgroundView.contentMask;
// if we need to delay, we don't want to be visible while we're delaying, so hide us in preparation for our popup
self.hidden = YES;
// create the appropriate animation, even if we're not animated
CAAnimation *animation = [self animationWithType:self.presentAnimation presenting:YES];
// nuke the duration if no animation requested - we'll still need to "run" the animation to get delays and callbacks
if (!animated)
animation.duration = 0.0000001; // can't be zero or the animation won't "run"
animation.beginTime = CACurrentMediaTime() + delay;
animation.delegate = self;
[self.layer addAnimation:animation forKey:@"present"];
}
- (void)animationDidStart:(CAAnimation *)anim {
BOOL presenting = [[anim valueForKey:@"presenting"] boolValue];
if (presenting) {
if ([_delegate respondsToSelector:@selector(calloutViewWillAppear:)])
[_delegate calloutViewWillAppear:(id)self];
// ok, animation is on, let's make ourselves visible!
self.hidden = NO;
}
else if (!presenting) {
if ([_delegate respondsToSelector:@selector(calloutViewWillDisappear:)])
[_delegate calloutViewWillDisappear:(id)self];
}
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)finished {
BOOL presenting = [[anim valueForKey:@"presenting"] boolValue];
if (presenting && finished) {
if ([_delegate respondsToSelector:@selector(calloutViewDidAppear:)])
[_delegate calloutViewDidAppear:(id)self];
}
else if (!presenting && finished) {
[self removeFromParent];
[self.layer removeAnimationForKey:@"dismiss"];
if ([_delegate respondsToSelector:@selector(calloutViewDidDisappear:)])
[_delegate calloutViewDidDisappear:(id)self];
}
}
- (void)dismissCalloutAnimated:(BOOL)animated {
// cancel all animations that may be in progress
[self.layer removeAnimationForKey:@"present"];
[self.layer removeAnimationForKey:@"dismiss"];
self.popupCancelled = YES;
if (animated) {
CAAnimation *animation = [self animationWithType:self.dismissAnimation presenting:NO];
animation.delegate = self;
[self.layer addAnimation:animation forKey:@"dismiss"];
}
else {
[self removeFromParent];
}
}
- (void)removeFromParent {
if (self.superview)
[self removeFromSuperview];
else {
// removing a layer from a superlayer causes an implicit fade-out animation that we wish to disable.
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self.layer removeFromSuperlayer];
[CATransaction commit];
}
}
- (CAAnimation *)animationWithType:(SMCalloutAnimation)type presenting:(BOOL)presenting {
CAAnimation *animation = nil;
if (type == SMCalloutAnimationBounce) {
CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"];
fade.duration = 0.23;
fade.fromValue = presenting ? @0.0 : @1.0;
fade.toValue = presenting ? @1.0 : @0.0;
fade.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
CABasicAnimation *bounce = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
bounce.duration = 0.23;
bounce.fromValue = presenting ? @0.7 : @1.0;
bounce.toValue = presenting ? @1.0 : @0.7;
bounce.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.59367:0.12066:0.18878:1.5814];
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = @[fade, bounce];
group.duration = 0.23;
animation = group;
}
else if (type == SMCalloutAnimationFade) {
CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"];
fade.duration = 1.0/3.0;
fade.fromValue = presenting ? @0.0 : @1.0;
fade.toValue = presenting ? @1.0 : @0.0;
animation = fade;
}
else if (type == SMCalloutAnimationStretch) {
CABasicAnimation *stretch = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
stretch.duration = 0.1;
stretch.fromValue = presenting ? @0.0 : @1.0;
stretch.toValue = presenting ? @1.0 : @0.0;
animation = stretch;
}
// CAAnimation is KVC compliant, so we can store whether we're presenting for lookup in our delegate methods
[animation setValue:@(presenting) forKey:@"presenting"];
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
return animation;
}
- (void)layoutSubviews {
self.containerView.frame = self.bounds;
self.backgroundView.frame = self.bounds;
// if we're pointing up, we'll need to push almost everything down a bit
CGFloat dy = self.currentArrowDirection == SMCalloutArrowDirectionUp ? TOP_ANCHOR_MARGIN : 0;
self.titleViewOrDefault.frameX = self.innerContentMarginLeft;
self.titleViewOrDefault.frameY = (self.subtitleView || self.subtitle.length ? TITLE_SUB_TOP : TITLE_TOP) + dy;
self.titleViewOrDefault.frameWidth = self.frameWidth - self.innerContentMarginLeft - self.innerContentMarginRight;
self.subtitleViewOrDefault.frameX = self.titleViewOrDefault.frameX;
self.subtitleViewOrDefault.frameY = SUBTITLE_TOP + dy;
self.subtitleViewOrDefault.frameWidth = self.titleViewOrDefault.frameWidth;
self.leftAccessoryView.frameX = self.leftAccessoryHorizontalMargin;
self.leftAccessoryView.frameY = self.leftAccessoryVerticalMargin + dy;
self.rightAccessoryView.frameX = self.frameWidth - self.rightAccessoryHorizontalMargin - self.rightAccessoryView.frameWidth;
self.rightAccessoryView.frameY = self.rightAccessoryVerticalMargin + dy;
if (self.contentView) {
self.contentView.frameX = self.innerContentMarginLeft;
self.contentView.frameY = self.contentViewInset.top + dy;
}
}
#pragma mark - Accessibility
- (NSInteger)accessibilityElementCount {
return (!!self.leftAccessoryView + !!self.titleViewOrDefault +
!!self.subtitleViewOrDefault + !!self.rightAccessoryView);
}
- (id)accessibilityElementAtIndex:(NSInteger)index {
if (index == 0) {
return self.leftAccessoryView ? self.leftAccessoryView : self.titleViewOrDefault;
}
if (index == 1) {
return self.leftAccessoryView ? self.titleViewOrDefault : self.subtitleViewOrDefault;
}
if (index == 2) {
return self.leftAccessoryView ? self.subtitleViewOrDefault : self.rightAccessoryView;
}
if (index == 3) {
return self.leftAccessoryView ? self.rightAccessoryView : nil;
}
return nil;
}
- (NSInteger)indexOfAccessibilityElement:(id)element {
if (element == nil) return NSNotFound;
if (element == self.leftAccessoryView) return 0;
if (element == self.titleViewOrDefault) {
return self.leftAccessoryView ? 1 : 0;
}
if (element == self.subtitleViewOrDefault) {
return self.leftAccessoryView ? 2 : 1;
}
if (element == self.rightAccessoryView) {
return self.leftAccessoryView ? 3 : 2;
}
return NSNotFound;
}
@end
// import this known "private API" from SMCalloutBackgroundView
@interface SMCalloutBackgroundView (EmbeddedImages)
+ (UIImage *)embeddedImageNamed:(NSString *)name;
@end
//
// Callout Background View.
//
@interface SMCalloutMaskedBackgroundView ()
@property (nonatomic, strong) UIView *containerView, *containerBorderView, *arrowView;
@property (nonatomic, strong) UIImageView *arrowImageView, *arrowHighlightedImageView, *arrowBorderView;
@end
static UIImage *blackArrowImage = nil, *whiteArrowImage = nil, *grayArrowImage = nil;
@implementation SMCalloutMaskedBackgroundView
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// Here we're mimicking the very particular (and odd) structure of the system callout view.
// The hierarchy and view/layer values were discovered by inspecting map kit using Reveal.app
self.containerView = [UIView new];
self.containerView.backgroundColor = [UIColor whiteColor];
self.containerView.alpha = 0.96;
self.containerView.layer.cornerRadius = 8;
self.containerView.layer.shadowRadius = 30;
self.containerView.layer.shadowOpacity = 0.1;
self.containerBorderView = [UIView new];
self.containerBorderView.layer.borderColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor;
self.containerBorderView.layer.borderWidth = 0.5;
self.containerBorderView.layer.cornerRadius = 8.5;
if (!blackArrowImage) {
blackArrowImage = [SMCalloutBackgroundView embeddedImageNamed:@"CalloutArrow"];
whiteArrowImage = [self image:blackArrowImage withColor:[UIColor whiteColor]];
grayArrowImage = [self image:blackArrowImage withColor:[UIColor colorWithWhite:0.85 alpha:1]];
}
self.anchorHeight = 13;
self.anchorMargin = 27;
self.arrowView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, blackArrowImage.size.width, blackArrowImage.size.height)];
self.arrowView.alpha = 0.96;
self.arrowImageView = [[UIImageView alloc] initWithImage:whiteArrowImage];
self.arrowHighlightedImageView = [[UIImageView alloc] initWithImage:grayArrowImage];
self.arrowHighlightedImageView.hidden = YES;
self.arrowBorderView = [[UIImageView alloc] initWithImage:blackArrowImage];
self.arrowBorderView.alpha = 0.1;
self.arrowBorderView.frameY = 0.5;
[self addSubview:self.containerView];
[self.containerView addSubview:self.containerBorderView];
[self addSubview:self.arrowView];
[self.arrowView addSubview:self.arrowBorderView];
[self.arrowView addSubview:self.arrowImageView];
[self.arrowView addSubview:self.arrowHighlightedImageView];
}
return self;
}
// Make sure we relayout our images when our arrow point changes!
- (void)setArrowPoint:(CGPoint)arrowPoint {
[super setArrowPoint:arrowPoint];
[self setNeedsLayout];
}
- (void)setHighlighted:(BOOL)highlighted {
[super setHighlighted:highlighted];
self.containerView.backgroundColor = highlighted ? [UIColor colorWithWhite:0.85 alpha:1] : [UIColor whiteColor];
self.arrowImageView.hidden = highlighted;
self.arrowHighlightedImageView.hidden = !highlighted;
}
- (UIImage *)image:(UIImage *)image withColor:(UIColor *)color {
UIGraphicsBeginImageContextWithOptions(image.size, NO, 0);
CGRect imageRect = (CGRect){.size=image.size};
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(c, 0, image.size.height);
CGContextScaleCTM(c, 1, -1);
CGContextClipToMask(c, imageRect, image.CGImage);
[color setFill];
CGContextFillRect(c, imageRect);
UIImage *whiteImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return whiteImage;
}
- (void)layoutSubviews {
BOOL pointingUp = self.arrowPoint.y < self.frameHeight/2;
// if we're pointing up, we'll need to push almost everything down a bit
CGFloat dy = pointingUp ? TOP_ANCHOR_MARGIN : 0;
self.containerView.frame = CGRectMake(0, dy, self.frameWidth, self.frameHeight - self.arrowView.frameHeight + 0.5);
self.containerBorderView.frame = CGRectInset(self.containerView.bounds, -0.5, -0.5);
self.arrowView.frameX = roundf(self.arrowPoint.x - self.arrowView.frameWidth / 2);
if (pointingUp) {
self.arrowView.frameY = 1;
self.arrowView.transform = CGAffineTransformMakeRotation(M_PI);
}
else {
self.arrowView.frameY = self.containerView.frameHeight - 0.5;
self.arrowView.transform = CGAffineTransformIdentity;
}
}
- (CALayer *)contentMask {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *maskImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CALayer *layer = [CALayer layer];
layer.frame = self.bounds;
layer.contents = (id)maskImage.CGImage;
return layer;
}
@end
@implementation SMCalloutBackgroundView
+ (NSData *)dataWithBase64EncodedString:(NSString *)string {
//
// NSData+Base64.m
//
// Version 1.0.2
//
// Created by Nick Lockwood on 12/01/2012.
// Copyright (C) 2012 Charcoal Design
//
// Distributed under the permissive zlib License
// Get the latest version from here:
//
// https://github.com/nicklockwood/Base64
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//
const char lookup[] = {
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 62, 99, 99, 99, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 99, 99, 99, 99, 99, 99,
99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 99, 99, 99, 99, 99,
99, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 99, 99, 99, 99, 99
};
NSData *inputData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
long long inputLength = [inputData length];
const unsigned char *inputBytes = [inputData bytes];
long long maxOutputLength = (inputLength / 4 + 1) * 3;
NSMutableData *outputData = [NSMutableData dataWithLength:(NSUInteger)maxOutputLength];
unsigned char *outputBytes = (unsigned char *)[outputData mutableBytes];
int accumulator = 0;
long long outputLength = 0;
unsigned char accumulated[] = {0, 0, 0, 0};
for (long long i = 0; i < inputLength; i++) {
unsigned char decoded = lookup[inputBytes[i] & 0x7F];
if (decoded != 99) {
accumulated[accumulator] = decoded;
if (accumulator == 3) {
outputBytes[outputLength++] = (accumulated[0] << 2) | (accumulated[1] >> 4);
outputBytes[outputLength++] = (accumulated[1] << 4) | (accumulated[2] >> 2);
outputBytes[outputLength++] = (accumulated[2] << 6) | accumulated[3];
}
accumulator = (accumulator + 1) % 4;
}
}
//handle left-over data
if (accumulator > 0) outputBytes[outputLength] = (accumulated[0] << 2) | (accumulated[1] >> 4);
if (accumulator > 1) outputBytes[++outputLength] = (accumulated[1] << 4) | (accumulated[2] >> 2);
if (accumulator > 2) outputLength++;
//truncate data to match actual output length
outputData.length = (NSUInteger)outputLength;
return outputLength? outputData: nil;
}
+ (UIImage *)embeddedImageNamed:(NSString *)name {
CGFloat screenScale = [UIScreen mainScreen].scale;
if (screenScale > 1.0) {
name = [name stringByAppendingString:@"_2x"];
screenScale = 2.0;
}
SEL selector = NSSelectorFromString(name);
if (![(id)self respondsToSelector:selector]) {
NSLog(@"Could not find an embedded image. Ensure that you've added a class-level method named +%@", name);
return nil;
}
// We need to hush the compiler here - but we know what we're doing!
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
NSString *base64String = [(id)self performSelector:selector];
#pragma clang diagnostic pop
UIImage *rawImage = [UIImage imageWithData:[self dataWithBase64EncodedString:base64String]];
return [UIImage imageWithCGImage:rawImage.CGImage scale:screenScale orientation:UIImageOrientationUp];
}
+ (NSString *)CalloutArrow { return @"iVBORw0KGgoAAAANSUhEUgAAACcAAAANCAYAAAAqlHdlAAAAHGlET1QAAAACAAAAAAAAAAcAAAAoAAAABwAAAAYAAADJEgYpIwAAAJVJREFUOBFiYIAAdn5+fkFOTkE5Dg5eW05O3lJOTr6zQPyfDhhoD28pxF5BOZA7gE5ih7oLN8XJyR8MdNwrGjkQaC5/MG7biZDh4OBXBDruLpUdeBdkLhHWE1bCzs6nAnTcUyo58DnIPMK2kqAC6DALIP5JoQNB+i1IsJZ4pcBEm0iJ40D6ibeNDJVAx00k04ETSbUOAAAA//+SwicfAAAAe0lEQVRjYCAdMHNy8u7l5OT7Tzzm3Qu0hpl0q8jQwcPDIwp02B0iHXeHl5dXhAxryNfCzc2tC3TcJwIO/ARSR74tFOjk4uL1BzruHw4H/gPJU2A85Vq5uPjTgY77g+bAPyBxyk2nggkcHPxOnJz8B4AOfAGiQXwqGMsAACGK1kPPMHNBAAAAAElFTkSuQmCC"; }
+ (NSString *)CalloutArrow_2x { return @"iVBORw0KGgoAAAANSUhEUgAAAE4AAAAaCAYAAAAZtWr8AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAHGlET1QAAAACAAAAAAAAAA0AAAAoAAAADQAAAA0AAAFMRh0LGwAAARhJREFUWAnclbENwjAQRZ0mih2fDYgsQEVDxQZMgKjpWYAJkBANI8AGDIEoM0WkzBDRAf8klB44g0OkU1zE3/+9RIpS7VVY730/y/woTWlsjJ9iPcN9pbXfY85auyvm/qcDNmb0e2Z+sk/ZBTthN0oVttX12mJIWeaWEFf+kbySmZQa0msu3nzaGJprTXV3BVLNDG/if7bNOTeAvFP35NGJu39GL7Abb27bFXncVQBZLgJf3jp+ebSWIxZMgrxdvPJoJ4gqHpXgV36ITR46HUGaiNMKB6YQd4lI3gV8qTBjmDhrbQFxVQTyKu4ShjJQap7nE4hrfiiv4Q6B8MLGat1bQNztB/JwZm8Rli5wujFu821xfGZgLPUAAAD//4wvm4gAAAD7SURBVOWXMQ6CMBiFgaFpi6VyBEedXJy4hMQTeBSvRDgJEySegI3EQWOivkZnqUB/k0LyL7R9L++D9G+DwP0TCZGUqCdRlYgUuY9F4JCmqQa0hgBcY7wIItFZMLZYS5l0ruAZbXhs6BIROgmhcoB7OIAHTZUTRqG3wp9xmhqc0aRPQu8YAlwxIbwCEUL6GH9wfDcLXY2HpyvvmkHf9+BcrwCuHQGvNRp9Pl6OY0PPAO42AB7WqMxLKLahpFR7gLv/AA9zPe+gtvAMCIC7WMC7CqEPtrqzmBfHyy3A1V/g1Th27GYBY0BIxrk6Ap65254/VZp30GID9JwteQEZrVMWXqGn8gAAAABJRU5ErkJggg=="; }
@end
//
// Our UIView frame helpers implementation
//
@implementation UIView (SMFrameAdditions)
- (CGPoint)frameOrigin { return self.frame.origin; }
- (void)setFrameOrigin:(CGPoint)origin { self.frame = (CGRect){ .origin=origin, .size=self.frame.size }; }
- (CGFloat)frameX { return self.frame.origin.x; }
- (void)setFrameX:(CGFloat)x { self.frame = (CGRect){ .origin.x=x, .origin.y=self.frame.origin.y, .size=self.frame.size }; }
- (CGFloat)frameY { return self.frame.origin.y; }
- (void)setFrameY:(CGFloat)y { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=y, .size=self.frame.size }; }
- (CGSize)frameSize { return self.frame.size; }
- (void)setFrameSize:(CGSize)size { self.frame = (CGRect){ .origin=self.frame.origin, .size=size }; }
- (CGFloat)frameWidth { return self.frame.size.width; }
- (void)setFrameWidth:(CGFloat)width { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=width, .size.height=self.frame.size.height }; }
- (CGFloat)frameHeight { return self.frame.size.height; }
- (void)setFrameHeight:(CGFloat)height { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=height }; }
- (CGFloat)frameLeft { return self.frame.origin.x; }
- (void)setFrameLeft:(CGFloat)left { self.frame = (CGRect){ .origin.x=left, .origin.y=self.frame.origin.y, .size.width=fmaxf(self.frame.origin.x+self.frame.size.width-left,0), .size.height=self.frame.size.height }; }
- (CGFloat)frameTop { return self.frame.origin.y; }
- (void)setFrameTop:(CGFloat)top { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=top, .size.width=self.frame.size.width, .size.height=fmaxf(self.frame.origin.y+self.frame.size.height-top,0) }; }
- (CGFloat)frameRight { return self.frame.origin.x + self.frame.size.width; }
- (void)setFrameRight:(CGFloat)right { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=fmaxf(right-self.frame.origin.x,0), .size.height=self.frame.size.height }; }
- (CGFloat)frameBottom { return self.frame.origin.y + self.frame.size.height; }
- (void)setFrameBottom:(CGFloat)bottom { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=fmaxf(bottom-self.frame.origin.y,0) }; }
@end

View File

@@ -0,0 +1,25 @@
//
// RNMapsGoogleMapView.h
//
//
// Created by Salah Ghanim on 23.11.24.
// Copyright © 2024 react-native-maps. All rights reserved.
//
#ifdef RCT_NEW_ARCH_ENABLED
#include "RNMapsDefines.h"
#if HAVE_GOOGLE_MAPS == 0
#import <React/RCTViewComponentView.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNMapsGoogleMapView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END
#endif
#endif

View File

@@ -0,0 +1,77 @@
//
// RNMapsGoogleMapView.mm
//
//
// Created by Salah Ghanim on 23.11.24.
// Copyright © 2024 react-native-maps. All rights reserved.
//
#include "RNMapsDefines.h"
#if HAVE_GOOGLE_MAPS == 0
#if __has_include(<ReactNativeMaps/generated/RNMapsAirModuleDelegate.h>)
#import <ReactNativeMaps/generated/RNMapsHostViewDelegate.h>
#import <ReactNativeMaps/generated/ComponentDescriptors.h>
#import <ReactNativeMaps/generated/EventEmitters.h>
#import <ReactNativeMaps/generated/Props.h>
#import <ReactNativeMaps/generated/RCTComponentViewHelpers.h>
#else
#import "../../generated/RNMapsSpecs/ComponentDescriptors.h"
#import "../../generated/RNMapsSpecs/EventEmitters.h"
#import "../../generated/RNMapsSpecs/Props.h"
#import "../../generated/RNMapsSpecs/RCTComponentViewHelpers.h"
#import "../../generated/RNMapsHostViewDelegate.h"
#endif
#import "RCTFabricComponentsPlugins.h"
#import <React/RCTConversions.h>
#import "PlaceHolderGoogleMapView.h"
using namespace facebook::react;
@implementation RNMapsGoogleMapView
#pragma mark - JS Commands
- (void)animateToRegion:(NSString *)regionJSON duration:(NSInteger)duration{
}
- (void)setCamera:(NSString *)cameraJSON{
}
- (void)animateCamera:(NSString *)cameraJSON duration:(NSInteger)duration{
}
- (void)fitToElements:(NSString *)edgePaddingJSON animated:(BOOL)animated {
}
- (void)fitToSuppliedMarkers:(NSString *)markersJSON edgePaddingJSON:(NSString *)edgePaddingJSON animated:(BOOL)animated {
}
- (void)fitToCoordinates:(NSString *)coordinatesJSON edgePaddingJSON:(NSString *)edgePaddingJSON animated:(BOOL)animated {
}
- (void) setIndoorActiveLevelIndex:(NSInteger)activeLevelIndex
{
}
#pragma mark - Native commands
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
}
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RNMapsGoogleMapViewComponentDescriptor>();
}
@end
Class<RCTComponentViewProtocol> RNMapsGoogleMapViewCls(void)
{
return RNMapsGoogleMapView.class;
}
#endif

View File

@@ -0,0 +1,25 @@
//
// RNMapsGoogleMapView.h
//
//
// Created by Salah Ghanim on 23.11.24.
// Copyright © 2024 react-native-maps. All rights reserved.
//
#ifdef RCT_NEW_ARCH_ENABLED
#include "RNMapsDefines.h"
#if HAVE_GOOGLE_MAPS == 0
#import <React/RCTViewComponentView.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNMapsGooglePolygonView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END
#endif
#endif

View File

@@ -0,0 +1,52 @@
//
// RNMapsGoogleMapView.mm
//
//
// Created by Salah Ghanim on 23.11.24.
// Copyright © 2024 react-native-maps. All rights reserved.
//
#include "RNMapsDefines.h"
#if HAVE_GOOGLE_MAPS == 0
#if __has_include(<ReactNativeMaps/generated/RNMapsAirModuleDelegate.h>)
#import <ReactNativeMaps/generated/RNMapsHostViewDelegate.h>
#import <ReactNativeMaps/generated/ComponentDescriptors.h>
#import <ReactNativeMaps/generated/EventEmitters.h>
#import <ReactNativeMaps/generated/Props.h>
#import <ReactNativeMaps/generated/RCTComponentViewHelpers.h>
#else
#import "../../generated/RNMapsSpecs/ComponentDescriptors.h"
#import "../../generated/RNMapsSpecs/EventEmitters.h"
#import "../../generated/RNMapsSpecs/Props.h"
#import "../../generated/RNMapsSpecs/RCTComponentViewHelpers.h"
#endif
#import "RCTFabricComponentsPlugins.h"
#import <React/RCTConversions.h>
#import "PlaceHolderPolygonView.h"
using namespace facebook::react;
@interface RNMapsGooglePolygonView () <RCTRNMapsGooglePolygonViewProtocol>
@end
@implementation RNMapsGooglePolygonView
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RNMapsGooglePolygonComponentDescriptor>();
}
@end
Class<RCTComponentViewProtocol> RNMapsGooglePolygonCls(void)
{
return RNMapsGooglePolygonView.class;
}
#endif

View File

@@ -0,0 +1,13 @@
//
// RCTComponentData+Maps.h
// AirMaps
//
// Created by Salah Ghanim on 24.12.23.
// Copyright © 2023 Christopher. All rights reserved.
//
#import <React/RCTComponentData.h>
@interface RCTComponentData (Maps)
@end

View File

@@ -0,0 +1,55 @@
//
// RCTComponentData+Maps.m
// AirMaps
//
// Created by Salah Ghanim on 24.12.23.
// Copyright © 2023 Christopher. All rights reserved.
//
#import "RCTComponentData+Maps.h"
#import <objc/runtime.h>
#import <Foundation/NSObjCRuntime.h>
@implementation RCTComponentData (Maps)
- (void) myCustom_setProps:(NSDictionary<NSString *, id> *)props forShadowView:(RCTShadowView *)shadowView{
// Pass initialProps to any manager that supports initialProps
id manager = [self manager];
if ([manager respondsToSelector:@selector(setInitialProps:)]) {
[manager performSelector:@selector(setInitialProps:) withObject:props];
}
// Call the original method
[self myCustom_setProps:props forShadowView:shadowView];
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [RCTComponentData class]; // Or the class where the method is defined
SEL originalSelector = @selector(setProps:forShadowView:);
SEL swizzledSelector = @selector(myCustom_setProps:forShadowView:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
@end

View File

@@ -0,0 +1,23 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <CoreLocation/CoreLocation.h>
#import <MapKit/MapKit.h>
#import <React/RCTConvert.h>
@class AIRMapCoordinate;
@interface RCTConvert (AirMap)
+ (MKCoordinateSpan)MKCoordinateSpan:(id)json;
+ (MKCoordinateRegion)MKCoordinateRegion:(id)json;
+ (MKMapCamera*)MKMapCamera:(id)json;
+ (MKMapCamera*)MKMapCameraWithDefaults:(id)json existingCamera:(MKMapCamera*)camera;
+ (MKMapType)MKMapType:(id)json;
+ (NSDictionary*) dictonaryFromString:(NSString *) str;
+ (NSArray*) arrayFromString:(NSString *) str;
+ (NSArray<NSArray<AIRMapCoordinate *> *> *)AIRMapCoordinateArrayArray:(id)json;
+ (AIRMapCoordinate*) AIRMapCoordinate:(id)json;
@end

View File

@@ -0,0 +1,106 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import "RCTConvert+AirMap.h"
#import <React/RCTConvert+CoreLocation.h>
#import "AIRMapCoordinate.h"
@implementation RCTConvert (AirMap)
+ (MKCoordinateSpan)MKCoordinateSpan:(id)json
{
json = [self NSDictionary:json];
return (MKCoordinateSpan){
[self CLLocationDegrees:json[@"latitudeDelta"]],
[self CLLocationDegrees:json[@"longitudeDelta"]]
};
}
+ (MKCoordinateRegion)MKCoordinateRegion:(id)json
{
return (MKCoordinateRegion){
[self CLLocationCoordinate2D:json],
[self MKCoordinateSpan:json]
};
}
+ (MKMapCamera*)MKMapCamera:(id)json
{
json = [self NSDictionary:json];
return [RCTConvert MKMapCameraWithDefaults:json existingCamera:nil];
}
+ (NSDictionary*) dictonaryFromString:(NSString *) str {
NSError *jsonError;
NSData *objectData = [str dataUsingEncoding:NSUTF8StringEncoding];
return [NSJSONSerialization JSONObjectWithData:objectData
options:NSJSONReadingMutableContainers
error:&jsonError];
}
+ (NSArray*) arrayFromString:(NSString *) str {
NSError *jsonError;
NSData *objectData = [str dataUsingEncoding:NSUTF8StringEncoding];
return [NSJSONSerialization JSONObjectWithData:objectData
options:NSJSONReadingMutableContainers
error:&jsonError];
}
+ (MKMapCamera*)MKMapCameraWithDefaults:(id)json existingCamera:(MKMapCamera*)camera
{
json = [self NSDictionary:json];
if (camera == nil) {
camera = [[MKMapCamera alloc] init];
} else {
camera = [camera copy];
}
if (json[@"center"]) {
camera.centerCoordinate = [self CLLocationCoordinate2D:json[@"center"]];
}
if (json[@"pitch"]) {
camera.pitch = [self double:json[@"pitch"]];
}
if (json[@"altitude"]) {
camera.altitude = [self double:json[@"altitude"]];
}
if (json[@"heading"]) {
camera.heading = [self double:json[@"heading"]];
}
return camera;
}
RCT_ENUM_CONVERTER(MKMapType, (@{
@"standard": @(MKMapTypeStandard),
@"satellite": @(MKMapTypeSatellite),
@"hybrid": @(MKMapTypeHybrid),
@"satelliteFlyover": @(MKMapTypeSatelliteFlyover),
@"hybridFlyover": @(MKMapTypeHybridFlyover),
@"mutedStandard": @(MKMapTypeMutedStandard)
}), MKMapTypeStandard, integerValue)
// NOTE(lmr):
// This is a bit of a hack, but I'm using this class to simply wrap
// around a `CLLocationCoordinate2D`, since I was unable to figure out
// how to handle an array of structs like CLLocationCoordinate2D. Would love
// to get rid of this if someone can show me how...
+ (AIRMapCoordinate *)AIRMapCoordinate:(id)json
{
AIRMapCoordinate *coord = [AIRMapCoordinate new];
coord.coordinate = [self CLLocationCoordinate2D:json];
return coord;
}
RCT_ARRAY_CONVERTER(AIRMapCoordinate)
+ (NSArray<NSArray<AIRMapCoordinate *> *> *)AIRMapCoordinateArrayArray:(id)json
{
return RCTConvertArrayValue(@selector(AIRMapCoordinateArray:), json);
}
@end

View File

@@ -0,0 +1,15 @@
// TurboModuleExample
#ifdef RCT_NEW_ARCH_ENABLED
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNMapsAirModule : NSObject
@end
NS_ASSUME_NONNULL_END
#endif

View File

@@ -0,0 +1,164 @@
//
// RNMapsMarkerManager.m
// AirMaps
//
// Created by Salah Ghanim on 23.11.24.
// Copyright © 2024 react-native-maps. All rights reserved.
//
#ifdef RCT_NEW_ARCH_ENABLED
#import "RNMapsAirModule.h"
#import <React/RCTUIManager.h>
#import <React/RCTViewManager.h>
#import "RNMapsMapView.h"
#import "AIRMap.h"
#import "AIRMapSnapshot.h"
#import <MapKit/MapKit.h>
#import "RCTConvert+AirMap.h"
#if __has_include(<ReactNativeMaps/generated/RNMapsAirModuleDelegate.h>)
#import <ReactNativeMaps/generated/RNMapsSpecs.h>
#import <ReactNativeMaps/generated/RNMapsHostViewDelegate.h>
#else
#import "RNMapsSpecs.h"
#import "RNMapsHostViewDelegate.h"
#endif
@interface RNMapsAirModule()<NativeAirMapsModuleSpec>
@end
@implementation RNMapsAirModule
@synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED;
- (void)executeWithMapView:(double)tag
success:(void (^)(id<RNMapsAirModuleDelegate> mapView))success
reject:(RCTPromiseRejectBlock)reject {
dispatch_async(dispatch_get_main_queue(), ^{
id view = [_viewRegistry_DEPRECATED viewForReactTag:[NSNumber numberWithDouble:tag]];
if ([view conformsToProtocol:@protocol(RNMapsHostViewDelegate)]) {
id<RNMapsAirModuleDelegate> mapViewDelegate = [view mapView];
success(mapViewDelegate);
} else {
reject(@"Invalid argument", [NSString stringWithFormat:@"Invalid view returned from registry, expecting RNMapsMapView, got: %@", view], NULL);
}
});
}
- (void)getCamera:(double)tag
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
[self executeWithMapView:tag success:^(id<RNMapsAirModuleDelegate> mapView) {
resolve([mapView getCameraDic]);
} reject:reject];
}
- (void)getMarkersFrames:(double)tag
onlyVisible:(BOOL)onlyVisible
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
[self executeWithMapView:tag success:^(id<RNMapsAirModuleDelegate> mapView) {
resolve([mapView getMarkersFramesWithOnlyVisible:onlyVisible]);
} reject:reject];
}
- (void)getMapBoundaries:(double)tag
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
[self executeWithMapView:tag success:^(id<RNMapsAirModuleDelegate> mapView) {
resolve([mapView getMapBoundaries]);
} reject:reject];
}
- (void)takeSnapshot:(double)tag
config:(NSString *)configStr
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
NSDictionary* config = [RCTConvert dictonaryFromString:configStr];
[self executeWithMapView:tag success:^(id<RNMapsAirModuleDelegate> mapView) {
[mapView takeSnapshotWithConfig:config success:resolve error:reject];
} reject:reject];
}
- (void)getAddressFromCoordinates:(double)tag
coordinate:(JS::NativeAirMapsModule::LatLng &)coordinate
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
double latitude = coordinate.latitude();
double longitude = coordinate.longitude();
CLLocation *location = [[CLLocation alloc] initWithLatitude:latitude
longitude:longitude];
CLGeocoder *geoCoder = [[CLGeocoder alloc] init];
[geoCoder reverseGeocodeLocation:location
completionHandler:^(NSArray *placemarks, NSError *error) {
if (error == nil && [placemarks count] > 0){
CLPlacemark *placemark = placemarks[0];
resolve(@{
@"name" : [NSString stringWithFormat:@"%@", placemark.name],
@"thoroughfare" : [NSString stringWithFormat:@"%@", placemark.thoroughfare],
@"subThoroughfare" : [NSString stringWithFormat:@"%@", placemark.subThoroughfare],
@"locality" : [NSString stringWithFormat:@"%@", placemark.locality],
@"subLocality" : [NSString stringWithFormat:@"%@", placemark.subLocality],
@"administrativeArea" : [NSString stringWithFormat:@"%@", placemark.administrativeArea],
@"subAdministrativeArea" : [NSString stringWithFormat:@"%@", placemark.subAdministrativeArea],
@"postalCode" : [NSString stringWithFormat:@"%@", placemark.postalCode],
@"countryCode" : [NSString stringWithFormat:@"%@", placemark.ISOcountryCode],
@"country" : [NSString stringWithFormat:@"%@", placemark.country],
});
} else {
reject(@"Invalid argument", [NSString stringWithFormat:@"Can not get address location"], NULL);
}
}];
}
- (void)getPointForCoordinate:(double)tag
coordinate:(JS::NativeAirMapsModule::LatLng &)coordinate
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
double latitude = coordinate.latitude();
double longitude = coordinate.longitude();
CLLocationCoordinate2D location = CLLocationCoordinate2DMake(latitude,longitude);
[self executeWithMapView:tag success:^(id<RNMapsAirModuleDelegate> mapView) {
resolve([mapView getPointForCoordinates:location]);
} reject:reject];
}
- (void)getCoordinateForPoint:(double)tag
point:(JS::NativeAirMapsModule::Point &)point
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
double x = point.x();
double y = point.y();
CGPoint pt = CGPointMake(x,y);
[self executeWithMapView:tag success:^(id<RNMapsAirModuleDelegate> mapView) {
resolve([mapView getCoordinatesForPoint:pt]);
} reject:reject];
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeAirMapsModuleSpecJSI>(params);
}
+ (NSString *)moduleName {
return nil;
}
@end
#endif

View File

@@ -0,0 +1 @@
#define HAVE_GOOGLE_MAPS 1

View File

@@ -0,0 +1,27 @@
//
// RNMapsMarker.h
// AirMaps
//
// Created by Salah Ghanim on 23.11.24.
// Copyright © 2024 react-native-maps. All rights reserved.
//
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
#import <UIKit/UIKit.h>
#if __has_include(<ReactNativeMaps/generated/RNMapsHostViewDelegate.h>)
#import <ReactNativeMaps/generated/RNMapsHostViewDelegate.h>
#else
#import "RNMapsHostViewDelegate.h"
#endif
@class AIRMap;
NS_ASSUME_NONNULL_BEGIN
@interface RNMapsMapView : RCTViewComponentView<RNMapsHostViewDelegate>
@end
NS_ASSUME_NONNULL_END
#endif

View File

@@ -0,0 +1,639 @@
//
// RNMapsMarker.m
// AirMaps
//
// Created by Salah Ghanim on 23.11.24.
// Copyright © 2024 react-native-maps. All rights reserved.
//
#import "RNMapsMapView.h"
#import "AIRMap.h"
#import "AIRMapMarker.h"
#import "AIRMapManager.h"
#import "RNMapsMarkerView.h"
#if __has_include(<ReactNativeMaps/generated/RNMapsAirModuleDelegate.h>)
#import <ReactNativeMaps/generated/RNMapsAirModuleDelegate.h>
#import <ReactNativeMaps/generated/RNMapsSpecs.h>
#import <ReactNativeMaps/generated/RNMapsHostViewDelegate.h>
#import <ReactNativeMaps/generated/ComponentDescriptors.h>
#import <ReactNativeMaps/generated/EventEmitters.h>
#import <ReactNativeMaps/generated/Props.h>
#import <ReactNativeMaps/generated/RCTComponentViewHelpers.h>
#else
#import "../generated/RNMapsAirModuleDelegate.h"
#import "../generated/RNMapsSpecs/RNMapsSpecs.h"
#import "../generated/RNMapsSpecs/ComponentDescriptors.h"
#import "../generated/RNMapsSpecs/EventEmitters.h"
#import "../generated/RNMapsSpecs/Props.h"
#import "../generated/RNMapsSpecs/RCTComponentViewHelpers.h"
#endif
#import "RCTFabricComponentsPlugins.h"
#import <React/RCTConversions.h>
#import "UIView+AirMap.h"
using namespace facebook::react;
@interface RNMapsMapView () <RCTRNMapsMapViewViewProtocol>
@end
@implementation RNMapsMapView {
AIRMap *_view;
AIRMapManager* _legacyMapManager;
}
- (id<RNMapsAirModuleDelegate>) mapView {
return (id<RNMapsAirModuleDelegate>)_view;
}
- (void) prepareForRecycle
{
[super prepareForRecycle];
[_view removeFromSuperview];
_view = nil;
_legacyMapManager = nil;
self.contentView = nil;
}
#pragma mark - JS Commands
- (void)animateToRegion:(NSString *)regionJSON duration:(NSInteger)duration{
NSDictionary* regionDic = [RCTConvert dictonaryFromString:regionJSON];
MKCoordinateRegion region = [RCTConvert MKCoordinateRegion:regionDic];
Boolean animated = duration == 0 ? NO :YES;
_view.ignoreRegionChanges = animated;
[_view setRegion:region animated:animated];
}
- (void)setCamera:(NSString *)cameraJSON{
NSDictionary* cameraDic = [RCTConvert dictonaryFromString:cameraJSON];
MKMapCamera *camera = [RCTConvert MKMapCameraWithDefaults:cameraDic existingCamera:[_view camera]];
[_view setCamera:camera];
}
- (void)animateCamera:(NSString *)cameraJSON duration:(NSInteger)duration{
NSDictionary* cameraDic = [RCTConvert dictonaryFromString:cameraJSON];
MKMapCamera *camera = [RCTConvert MKMapCameraWithDefaults:cameraDic existingCamera:[_view camera]];
// don't emit region change events when we are setting the camera
_view.ignoreRegionChanges = YES;
[_view setCamera:camera animated:YES];
}
- (void)fitToElements:(NSString *)edgePaddingJSON animated:(BOOL)animated {
[_view showAnnotations:_view.annotations animated:animated];
}
- (void)fitToSuppliedMarkers:(NSString *)markersJSON edgePaddingJSON:(NSString *)edgePaddingJSON animated:(BOOL)animated {
NSArray* markers = [RCTConvert arrayFromString:markersJSON];
NSPredicate *filterMarkers = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
AIRMapMarker *marker = (AIRMapMarker *)evaluatedObject;
return [marker isKindOfClass:[AIRMapMarker class]] && [markers containsObject:marker.identifier];
}];
NSArray *filteredMarkers = [_view.annotations filteredArrayUsingPredicate:filterMarkers];
[_view showAnnotations:filteredMarkers animated:animated];
}
- (void)fitToCoordinates:(NSString *)coordinatesJSON edgePaddingJSON:(NSString *)edgePaddingJSON animated:(BOOL)animated {
NSArray* coordinatesArr = [RCTConvert arrayFromString:coordinatesJSON];
NSMutableArray<AIRMapCoordinate*>* mutableArray = [NSMutableArray new];
for (id json : coordinatesArr){
[mutableArray addObject:[RCTConvert AIRMapCoordinate:json]];
}
NSDictionary* edgePadding = [RCTConvert dictonaryFromString:edgePaddingJSON];
UIEdgeInsets edgeInsets = [RCTConvert UIEdgeInsets:edgePadding];
[_view fitToCoordinates:mutableArray edgePadding:edgeInsets animated:animated];
}
- (void) setIndoorActiveLevelIndex:(NSInteger) activeLevelIndex
{
// do nothing (google only)
}
#pragma mark - Native commands
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTRNMapsMapViewHandleCommand(self, commandName, args);
}
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RNMapsMapViewComponentDescriptor>();
}
- (void) prepareMapView
{
if (_legacyMapManager && _view) return;
static const auto defaultProps = std::make_shared<const RNMapsMapViewProps>();
_props = defaultProps;
_legacyMapManager = [[AIRMapManager alloc] init];
_view = (AIRMap *)[_legacyMapManager view];
self.contentView = _view;
_view.onLongPress = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
// Extract values from the NSDictionary
NSDictionary* coordinateDict = dictionary[@"coordinate"];
NSDictionary* positionDict = dictionary[@"position"];
// Populate the OnMapPressCoordinate struct
facebook::react::RNMapsMapViewEventEmitter::OnLongPressCoordinate coordinate = {
.latitude = [coordinateDict[@"latitude"] doubleValue],
.longitude = [coordinateDict[@"longitude"] doubleValue],
};
// Populate the OnMapPressPosition struct
facebook::react::RNMapsMapViewEventEmitter::OnLongPressPosition position = {
.x = [positionDict[@"x"] doubleValue],
.y = [positionDict[@"y"] doubleValue],
};
auto mapViewEventEmitter = std::static_pointer_cast<RNMapsMapViewEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMapViewEventEmitter::OnLongPress data = {
.action = std::string([@"long-press" UTF8String]),
.position = position,
.coordinate = coordinate
};
mapViewEventEmitter->onLongPress(data);
}
};
_view.onPress = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
// Extract values from the NSDictionary
NSDictionary* coordinateDict = dictionary[@"coordinate"];
NSDictionary* positionDict = dictionary[@"position"];
// Populate the OnMapPressCoordinate struct
facebook::react::RNMapsMapViewEventEmitter::OnPressCoordinate coordinate = {
.latitude = [coordinateDict[@"latitude"] doubleValue],
.longitude = [coordinateDict[@"longitude"] doubleValue],
};
// Populate the OnMapPressPosition struct
facebook::react::RNMapsMapViewEventEmitter::OnPressPosition position = {
.x = [positionDict[@"x"] doubleValue],
.y = [positionDict[@"y"] doubleValue],
};
auto mapViewEventEmitter = std::static_pointer_cast<RNMapsMapViewEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMapViewEventEmitter::OnPress data = {
.action = std::string([@"press" UTF8String]),
.position = position,
.coordinate = coordinate
};
mapViewEventEmitter->onPress(data);
}
};
_view.onMapReady = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
auto mapViewEventEmitter = std::static_pointer_cast<RNMapsMapViewEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMapViewEventEmitter::OnMapReady data = {};
mapViewEventEmitter->onMapReady(data);
}
};
_view.onRegionChange = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
NSDictionary* regionDict = dictionary[@"region"];
auto mapViewEventEmitter = std::static_pointer_cast<RNMapsMapViewEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMapViewEventEmitter::OnRegionChange data = {
.region.latitude = [regionDict[@"latitude"] doubleValue],
.region.longitude = [regionDict[@"longitude"] doubleValue],
.region.latitudeDelta = [regionDict[@"latitudeDelta"] doubleValue],
.region.longitudeDelta = [regionDict[@"longitudeDelta"] doubleValue],
.isGesture = [dictionary[@"isGesture"] boolValue],
};
mapViewEventEmitter->onRegionChange(data);
}
};
_view.onDoublePress = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
NSDictionary* coordinateDict = dictionary[@"coordinate"];
NSDictionary* positionDict = dictionary[@"position"];
// Populate the OnMapPressCoordinate struct
facebook::react::RNMapsMapViewEventEmitter::OnDoublePressCoordinate coordinate = {
.latitude = [coordinateDict[@"latitude"] doubleValue],
.longitude = [coordinateDict[@"longitude"] doubleValue],
};
// Populate the OnMapDouplePressPosition struct
facebook::react::RNMapsMapViewEventEmitter::OnDoublePressPosition position = {
.x = [positionDict[@"x"] doubleValue],
.y = [positionDict[@"y"] doubleValue],
};
auto mapViewEventEmitter = std::static_pointer_cast<RNMapsMapViewEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMapViewEventEmitter::OnDoublePress data = {
.position = position,
.coordinate = coordinate
};
mapViewEventEmitter->onDoublePress(data);
}
};
_view.onPanDrag = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
NSDictionary* coordinateDict = dictionary[@"coordinate"];
NSDictionary* positionDict = dictionary[@"position"];
// Populate the OnMapPressCoordinate struct
facebook::react::RNMapsMapViewEventEmitter::OnPanDragCoordinate coordinate = {
.latitude = [coordinateDict[@"latitude"] doubleValue],
.longitude = [coordinateDict[@"longitude"] doubleValue],
};
// Populate the OnMapDouplePressPosition struct
facebook::react::RNMapsMapViewEventEmitter::OnPanDragPosition position = {
.x = [positionDict[@"x"] doubleValue],
.y = [positionDict[@"y"] doubleValue],
};
auto mapViewEventEmitter = std::static_pointer_cast<RNMapsMapViewEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMapViewEventEmitter::OnPanDrag data = {
.position = position,
.coordinate = coordinate,
};
mapViewEventEmitter->onPanDrag(data);
}
};
_view.onUserLocationChange = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
NSDictionary* coordinateDict = dictionary[@"coordinate"];
NSDictionary* errorDict = dictionary[@"error"];
// Populate the OnMapPressCoordinate struct
facebook::react::RNMapsMapViewEventEmitter::OnUserLocationChangeCoordinate coordinate = {
.latitude = [coordinateDict[@"latitude"] doubleValue],
.longitude = [coordinateDict[@"longitude"] doubleValue],
.altitude =[coordinateDict[@"altitude"] doubleValue],
.timestamp =[coordinateDict[@"timestamp"] doubleValue],
.accuracy =[coordinateDict[@"accuracy"] floatValue],
.speed =[coordinateDict[@"speed"] floatValue],
.heading =[coordinateDict[@"heading"] floatValue],
.altitudeAccuracy =[coordinateDict[@"altitudeAccuracy"] floatValue],
};
NSString* str = @"";
if (errorDict){
str = errorDict[@"message"];
}
facebook::react::RNMapsMapViewEventEmitter::OnUserLocationChangeError error = {
.message = std::string([str UTF8String]),
};
auto mapViewEventEmitter = std::static_pointer_cast<RNMapsMapViewEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMapViewEventEmitter::OnUserLocationChange data = {
.coordinate = coordinate,
.error = error,
};
mapViewEventEmitter->onUserLocationChange(data);
}
};
_view.onCalloutPress = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
NSDictionary* coordinateDict = dictionary[@"coordinate"];
NSDictionary* positionDict = dictionary[@"position"];
// Populate the OnCalloutPressPoint struct
facebook::react::RNMapsMapViewEventEmitter::OnCalloutPressPoint point = {
.x = [coordinateDict[@"x"] doubleValue],
.y = [coordinateDict[@"y"] doubleValue],
};
facebook::react::RNMapsMapViewEventEmitter::OnCalloutPressFrame frame = {
.x = [positionDict[@"x"] doubleValue],
.y = [positionDict[@"y"] doubleValue],
.width = [positionDict[@"width"] doubleValue],
.height = [positionDict[@"height"] doubleValue],
};
auto mapViewEventEmitter = std::static_pointer_cast<RNMapsMapViewEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMapViewEventEmitter::OnCalloutPress data = {
.action = std::string([dictionary[@"action"] UTF8String]),
.id = std::string([dictionary[@"id"] UTF8String]),
.point = point,
.frame = frame,
};
mapViewEventEmitter->onCalloutPress(data);
}
};
_view.onRegionChangeStart = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
NSDictionary* regionDict = dictionary[@"region"];
auto mapViewEventEmitter = std::static_pointer_cast<RNMapsMapViewEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMapViewEventEmitter::OnRegionChangeStart data = {
.region.latitude = [regionDict[@"latitude"] doubleValue],
.region.longitude = [regionDict[@"longitude"] doubleValue],
.region.latitudeDelta = [regionDict[@"latitudeDelta"] doubleValue],
.region.longitudeDelta = [regionDict[@"longitudeDelta"] doubleValue],
.isGesture = [dictionary[@"isGesture"] boolValue],
};
mapViewEventEmitter->onRegionChangeStart(data);
}
};
_view.onRegionChangeComplete = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
NSDictionary* regionDict = dictionary[@"region"];
auto mapViewEventEmitter = std::static_pointer_cast<RNMapsMapViewEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMapViewEventEmitter::OnRegionChangeComplete data = {
.region.latitude = [regionDict[@"latitude"] doubleValue],
.region.longitude = [regionDict[@"longitude"] doubleValue],
.region.latitudeDelta = [regionDict[@"latitudeDelta"] doubleValue],
.region.longitudeDelta = [regionDict[@"longitudeDelta"] doubleValue],
.isGesture = [dictionary[@"isGesture"] boolValue],
};
mapViewEventEmitter->onRegionChangeComplete(data);
}
};
#define HANDLE_MARKER_DRAG_EVENT(eventName, emitterFunction) \
if (_eventEmitter) { \
NSDictionary* coordinateDict = dictionary[@"coordinate"]; \
auto mapViewEventEmitter = \
std::static_pointer_cast<RNMapsMapViewEventEmitter const>(_eventEmitter); \
facebook::react::RNMapsMapViewEventEmitter::eventName data = { \
.coordinate.latitude = [coordinateDict[@"latitude"] doubleValue], \
.coordinate.longitude = [coordinateDict[@"longitude"] doubleValue], \
.id = std::string([[dictionary valueForKey:@"id"] UTF8String]), \
}; \
mapViewEventEmitter->emitterFunction(data); \
}
#define HANDLE_MARKER_EVENT(eventName, emitterFunction, actionName) \
if (_eventEmitter) { \
NSDictionary* coordinateDict = dictionary[@"coordinate"]; \
facebook::react::RNMapsMapViewEventEmitter::eventName##Coordinate coordinate = { \
.latitude = [coordinateDict[@"latitude"] doubleValue], \
.longitude = [coordinateDict[@"longitude"] doubleValue], \
}; \
\
auto mapViewEventEmitter = \
std::static_pointer_cast<RNMapsMapViewEventEmitter const>(_eventEmitter); \
facebook::react::RNMapsMapViewEventEmitter::eventName data = { \
.action = std::string([@actionName UTF8String]), \
.id = std::string([[dictionary valueForKey:@"id"] UTF8String]), \
.coordinate = coordinate \
}; \
mapViewEventEmitter->emitterFunction(data); \
}
_view.onMarkerDrag = [self](NSDictionary* dictionary) {
HANDLE_MARKER_DRAG_EVENT(OnMarkerDrag, onMarkerDrag);
};
_view.onMarkerDragStart = [self](NSDictionary* dictionary) {
HANDLE_MARKER_DRAG_EVENT(OnMarkerDragStart, onMarkerDragStart);
};
_view.onMarkerDragEnd = [self](NSDictionary* dictionary) {
HANDLE_MARKER_DRAG_EVENT(OnMarkerDragEnd, onMarkerDragEnd);
};
_view.onMarkerSelect = [self](NSDictionary* dictionary) {
HANDLE_MARKER_EVENT(OnMarkerSelect, onMarkerSelect, "marker-select");
};
_view.onMarkerDeselect = [self](NSDictionary* dictionary) {
HANDLE_MARKER_EVENT(OnMarkerDeselect, onMarkerDeselect, "marker-deselect");
};
_view.onMarkerPress = [self](NSDictionary* dictionary) {
HANDLE_MARKER_EVENT(OnMarkerPress, onMarkerPress, "marker-press");
};
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self prepareMapView];
}
return self;
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index{
id<RCTComponent> paperView = [childComponentView getPaperViewFromChildComponentView];
if (paperView){
[_view insertReactSubview:paperView atIndex:index];
} else if ([childComponentView isKindOfClass:[RNMapsMarkerView class]]){
RNMapsMarkerView* fabricMarker = (RNMapsMarkerView *) childComponentView;
[_view insertReactSubview:[fabricMarker marker] atIndex:index];
}
else {
[_view insertReactSubview:childComponentView atIndex:index];
}
}
- (void) unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
id<RCTComponent> paperView = [childComponentView getPaperViewFromChildComponentView];
if (paperView){
[_view removeReactSubview:paperView];
}
else if ([childComponentView isKindOfClass:[RNMapsMarkerView class]]){
RNMapsMarkerView* fabricMarker = (RNMapsMarkerView *) childComponentView;
[_view removeReactSubview:[fabricMarker marker]];
} else {
[_view removeReactSubview:childComponentView];
}
}
MKMapType mapRNTypeToMKMapType(RNMapsMapViewMapType rnMapType) {
switch (rnMapType) {
case RNMapsMapViewMapType::Standard: return MKMapTypeStandard;
case RNMapsMapViewMapType::Satellite: return MKMapTypeSatellite;
case RNMapsMapViewMapType::Hybrid: return MKMapTypeHybrid;
case RNMapsMapViewMapType::MutedStandard: return MKMapTypeMutedStandard;
case RNMapsMapViewMapType::SatelliteFlyover: return MKMapTypeSatelliteFlyover;
case RNMapsMapViewMapType::HybridFlyover: return MKMapTypeHybridFlyover;
default: return MKMapTypeStandard; // Default case
}
}
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
[self prepareMapView];
const auto &oldViewProps = *std::static_pointer_cast<RNMapsMapViewProps const>(_props);
const auto &newViewProps = *std::static_pointer_cast<RNMapsMapViewProps const>(props);
#define REMAP_MAPVIEW_PROP(name) \
if (oldViewProps.name != newViewProps.name) { \
_view.name = newViewProps.name; \
}
#define REMAP_MAPVIEW_STRING_PROP(name) \
if (oldViewProps.name != newViewProps.name) { \
_view.name = RCTNSStringFromString(newViewProps.name); \
}
#define REMAP_MAPVIEW_COLOR_PROP(name) \
if (oldViewProps.name != newViewProps.name) { \
_view.name = RCTUIColorFromSharedColor(newViewProps.name); \
}
#define REMAP_MAPVIEW_POINT_PROP(name) \
if (newViewProps.name.x != oldViewProps.name.x || \
newViewProps.name.y != oldViewProps.name.y) { \
_view.name = CGPointMake(newViewProps.name.x, newViewProps.name.y); \
}
#define REMAP_MAPVIEW_REGION_PROP(name) \
if (!(newViewProps.name.latitude == 0 && \
newViewProps.name.longitude == 0 && \
newViewProps.name.latitudeDelta == 0 && \
newViewProps.name.longitudeDelta == 0)) { \
if (newViewProps.name.latitude != oldViewProps.name.latitude || \
newViewProps.name.longitude != oldViewProps.name.longitude || \
newViewProps.name.latitudeDelta != oldViewProps.name.latitudeDelta ||\
newViewProps.name.longitudeDelta != oldViewProps.name.longitudeDelta) { \
MKCoordinateSpan span = MKCoordinateSpanMake(newViewProps.name.latitudeDelta, \
newViewProps.name.longitudeDelta); \
MKCoordinateRegion region = MKCoordinateRegionMake(CLLocationCoordinate2DMake( \
newViewProps.name.latitude, \
newViewProps.name.longitude), span); \
_view.name = region; \
} \
}
#define REMAP_MAPVIEW_CAMERA_PROP(name) \
if (newViewProps.name.center.latitude != oldViewProps.name.center.latitude || \
newViewProps.name.center.longitude != oldViewProps.name.center.longitude || \
newViewProps.name.altitude != oldViewProps.name.altitude || \
newViewProps.name.heading != oldViewProps.name.heading || \
newViewProps.name.pitch != oldViewProps.name.pitch) { \
CLLocationCoordinate2D center = CLLocationCoordinate2DMake( \
newViewProps.name.center.latitude, \
newViewProps.name.center.longitude); \
MKMapCamera* camera = [[MKMapCamera alloc] init]; \
camera.centerCoordinate = center; \
camera.heading = newViewProps.name.heading; \
camera.pitch = newViewProps.name.pitch; \
camera.centerCoordinateDistance = newViewProps.name.altitude; \
_view.name = camera; \
}
#define REMAP_MAPVIEW_EDGEINSETS_PROP(name) \
if (newViewProps.name.top != oldViewProps.name.top || \
newViewProps.name.right != oldViewProps.name.right || \
newViewProps.name.bottom != oldViewProps.name.bottom || \
newViewProps.name.left != oldViewProps.name.left) { \
_view.name = UIEdgeInsetsMake(newViewProps.name.top, \
newViewProps.name.left, \
newViewProps.name.bottom, \
newViewProps.name.right); \
}
#define REMAP_MAPVIEW_MAPTYPE(rnMapType) MKMapType##rnMapType
REMAP_MAPVIEW_PROP(cacheEnabled)
REMAP_MAPVIEW_PROP(followsUserLocation)
REMAP_MAPVIEW_PROP(loadingEnabled)
REMAP_MAPVIEW_PROP(scrollEnabled)
REMAP_MAPVIEW_PROP(handlePanDrag)
REMAP_MAPVIEW_PROP(maxDelta)
REMAP_MAPVIEW_PROP(maxZoom)
REMAP_MAPVIEW_PROP(minDelta)
REMAP_MAPVIEW_PROP(minZoom)
REMAP_MAPVIEW_PROP(showsCompass)
REMAP_MAPVIEW_PROP(showsScale)
REMAP_MAPVIEW_PROP(showsUserLocation)
REMAP_MAPVIEW_PROP(userLocationCalloutEnabled)
REMAP_MAPVIEW_PROP(zoomEnabled)
REMAP_MAPVIEW_PROP(loadingEnabled)
REMAP_MAPVIEW_PROP(showsTraffic)
REMAP_MAPVIEW_PROP(pitchEnabled)
REMAP_MAPVIEW_PROP(showsBuildings)
REMAP_MAPVIEW_PROP(rotateEnabled)
REMAP_MAPVIEW_PROP(showsPointsOfInterests)
REMAP_MAPVIEW_POINT_PROP(compassOffset)
if (![_view ignoreRegionChanges]){
_view.ignoreRegionChanges = YES;
REMAP_MAPVIEW_REGION_PROP(initialRegion)
REMAP_MAPVIEW_REGION_PROP(region)
_view.ignoreRegionChanges = NO;
}
REMAP_MAPVIEW_CAMERA_PROP(initialCamera)
REMAP_MAPVIEW_CAMERA_PROP(camera)
REMAP_MAPVIEW_EDGEINSETS_PROP(legalLabelInsets)
REMAP_MAPVIEW_EDGEINSETS_PROP(appleLogoInsets)
REMAP_MAPVIEW_EDGEINSETS_PROP(mapPadding)
REMAP_MAPVIEW_COLOR_PROP(loadingIndicatorColor)
REMAP_MAPVIEW_COLOR_PROP(loadingIndicatorColor)
REMAP_MAPVIEW_COLOR_PROP(tintColor)
REMAP_MAPVIEW_STRING_PROP(userLocationAnnotationTitle)
if (oldViewProps.mapType != newViewProps.mapType){
_view.mapType = mapRNTypeToMKMapType(newViewProps.mapType);
}
if (oldViewProps.userInterfaceStyle != newViewProps.userInterfaceStyle){
switch (newViewProps.userInterfaceStyle) {
case RNMapsMapViewUserInterfaceStyle::Light:
_view.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
break;
case RNMapsMapViewUserInterfaceStyle::Dark:
_view.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
break;
case RNMapsMapViewUserInterfaceStyle::System:
_view.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified;
break;
}
}
if (oldViewProps.cameraZoomRange.minCenterCoordinateDistance != newViewProps.cameraZoomRange.minCenterCoordinateDistance ||
oldViewProps.cameraZoomRange.maxCenterCoordinateDistance != newViewProps.cameraZoomRange.maxCenterCoordinateDistance ||
oldViewProps.cameraZoomRange.animated != newViewProps.cameraZoomRange.animated) {
MKMapCameraZoomRange* zoomRange = [[MKMapCameraZoomRange alloc] initWithMinCenterCoordinateDistance:newViewProps.cameraZoomRange.minCenterCoordinateDistance maxCenterCoordinateDistance:newViewProps.cameraZoomRange.maxCenterCoordinateDistance];
[_view setCameraZoomRange:zoomRange animated:newViewProps.cameraZoomRange.animated];
}
if (oldViewProps.pointsOfInterestFilter != newViewProps.pointsOfInterestFilter) {
NSMutableArray<NSString *> *filterArray = [NSMutableArray new];
for (const auto& str : newViewProps.pointsOfInterestFilter) {
[filterArray addObject:RCTNSStringFromString(str)];
}
_view.pointsOfInterestFilter = filterArray;
}
[super updateProps:props oldProps:oldProps];
}
@end
Class<RCTComponentViewProtocol> RNMapsMapViewCls(void)
{
return RNMapsMapView.class;
}

View File

@@ -0,0 +1,42 @@
//
// RNMapsMarkerManager.m
// AirMaps
//
// Created by Salah Ghanim on 23.11.24.
// Copyright © 2024 react-native-maps. All rights reserved.
//
#import <React/RCTLog.h>
#import <React/RCTUIManager.h>
#import <React/RCTViewManager.h>
@interface RNMapsMapViewManager : RCTViewManager
@end
@implementation RNMapsMapViewManager
RCT_EXPORT_MODULE(RNMapsMapViewManager)
RCT_EXPORT_VIEW_PROPERTY(onMapReady, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onLongPress, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onCalloutPress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDoublePress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onPanDrag, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMarkerPress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMarkerSelect, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMarkerDeselect, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMarkerDragStart, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMarkerDrag, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMarkerDragEnd, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onRegionChangeStart, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onRegionChangeComplete, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onUserLocationChange, RCTDirectEventBlock)
@end

View File

@@ -0,0 +1,24 @@
//
// RNMapsMarker.h
// AirMaps
//
// Created by Salah Ghanim on 23.11.24.
// Copyright © 2024 react-native-maps. All rights reserved.
//
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^LoadCompletionHandler)(UIView *view);
@class AIRMapMarker;
@interface RNMapsMarkerView : RCTViewComponentView
- (AIRMapMarker*) marker;
@end
NS_ASSUME_NONNULL_END
#endif

View File

@@ -0,0 +1,440 @@
//
// RNMapsMarker.m
// AirMaps
//
// Created by Salah Ghanim on 23.11.24.
// Copyright © 2024 react-native-maps. All rights reserved.
//
#import "RNMapsMarkerView.h"
#import "AIRMap.h"
#import "AIRMapMarker.h"
#import "AIRMapMarkerManager.h"
#if __has_include(<ReactNativeMaps/generated/RNMapsAirModuleDelegate.h>)
#import <ReactNativeMaps/generated/RNMapsHostViewDelegate.h>
#import <ReactNativeMaps/generated/ComponentDescriptors.h>
#import <ReactNativeMaps/generated/EventEmitters.h>
#import <ReactNativeMaps/generated/Props.h>
#import <ReactNativeMaps/generated/RCTComponentViewHelpers.h>
#else
#import "../generated/RNMapsHostViewDelegate.h"
#import "../generated/RNMapsSpecs/ComponentDescriptors.h"
#import "../generated/RNMapsSpecs/EventEmitters.h"
#import "../generated/RNMapsSpecs/Props.h"
#import "../generated/RNMapsSpecs/RCTComponentViewHelpers.h"
#endif
#import "RCTFabricComponentsPlugins.h"
#import <React/RCTConversions.h>
#import "UIView+AirMap.h"
using namespace facebook::react;
@interface RNMapsMarkerView () <RCTRNMapsMarkerViewProtocol>
@end
@implementation RNMapsMarkerView {
AIRMapMarker *_view;
AIRMapMarkerManager* _legacyMapManager;
}
- (AIRMapMarker *) marker {
return _view;
}
- (void) prepareForRecycle
{
[super prepareForRecycle];
[_view removeFromSuperview];
_view = nil;
_legacyMapManager = nil;
}
#pragma mark - JS Commands
#pragma mark - Native commands
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTRNMapsMarkerHandleCommand(self, commandName, args);
}
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RNMapsMarkerComponentDescriptor>();
}
- (void) animateToCoordinates:(double)latitude longitude:(double)longitude duration:(NSInteger)duration
{
[_view animateToCoordinate:CLLocationCoordinate2DMake(latitude, longitude) duration:duration/1000];
}
- (void) showCallout
{
[_view.map selectAnnotation:_view animated:YES];
}
- (void) hideCallout
{
[_view.map deselectAnnotation:_view animated:YES];
}
- (void) redraw
{
// do nothing
}
- (void) redrawCallout
{
// do nothing
}
- (void) setCoordinates:(double)latitude longitude:(double)longitude
{
[_view setCoordinate:CLLocationCoordinate2DMake(latitude, longitude)];
}
- (void) prepareMarkerView
{
if (_legacyMapManager && _view) return;
static const auto defaultProps = std::make_shared<const RNMapsMarkerProps>();
_props = defaultProps;
_legacyMapManager = [[AIRMapMarkerManager alloc] init];
_view = (AIRMapMarker *)[_legacyMapManager view];
_view.onPress = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
// Extract values from the NSDictionary
NSDictionary* coordinateDict = dictionary[@"coordinate"];
NSDictionary* positionDict = dictionary[@"position"];
// Populate the OnCalloutPressPoint struct
facebook::react::RNMapsMarkerEventEmitter::OnPressPosition point = {
.x = [coordinateDict[@"x"] doubleValue],
.y = [coordinateDict[@"y"] doubleValue],
};
facebook::react::RNMapsMarkerEventEmitter::OnPressCoordinate coordinate = {
.latitude = [coordinateDict[@"latitude"] doubleValue],
.longitude = [coordinateDict[@"longitude"] doubleValue],
};
auto eventEmitter = std::static_pointer_cast<RNMapsMarkerEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMarkerEventEmitter::OnPress data = {
.action = std::string([dictionary[@"action"] UTF8String]),
.id = std::string([dictionary[@"id"] UTF8String]),
.position = point,
.coordinate = coordinate,
};
eventEmitter->onPress(data);
}
};
_view.onDeselect = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
// Extract values from the NSDictionary
NSDictionary* coordinateDict = dictionary[@"coordinate"];
NSDictionary* positionDict = dictionary[@"position"];
// Populate the OnCalloutPressPoint struct
facebook::react::RNMapsMarkerEventEmitter::OnDeselectPosition point = {
.x = [coordinateDict[@"x"] doubleValue],
.y = [coordinateDict[@"y"] doubleValue],
};
facebook::react::RNMapsMarkerEventEmitter::OnDeselectCoordinate coordinate = {
.latitude = [coordinateDict[@"latitude"] doubleValue],
.longitude = [coordinateDict[@"longitude"] doubleValue],
};
auto eventEmitter = std::static_pointer_cast<RNMapsMarkerEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMarkerEventEmitter::OnDeselect data = {
.action = std::string([dictionary[@"action"] UTF8String]),
.id = std::string([dictionary[@"id"] UTF8String]),
.position = point,
.coordinate = coordinate,
};
eventEmitter->onDeselect(data);
}
};
_view.onDrag = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
// Extract values from the NSDictionary
NSDictionary* coordinateDict = dictionary[@"coordinate"];
facebook::react::RNMapsMarkerEventEmitter::OnDragCoordinate coordinate = {
.latitude = [coordinateDict[@"latitude"] doubleValue],
.longitude = [coordinateDict[@"longitude"] doubleValue],
};
auto eventEmitter = std::static_pointer_cast<RNMapsMarkerEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMarkerEventEmitter::OnDrag data = {
.id = std::string([dictionary[@"id"] UTF8String]),
.coordinate = coordinate,
};
eventEmitter->onDrag(data);
}
};
_view.onDragStart = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
// Extract values from the NSDictionary
NSDictionary* coordinateDict = dictionary[@"coordinate"];
facebook::react::RNMapsMarkerEventEmitter::OnDragStartCoordinate coordinate = {
.latitude = [coordinateDict[@"latitude"] doubleValue],
.longitude = [coordinateDict[@"longitude"] doubleValue],
};
auto eventEmitter = std::static_pointer_cast<RNMapsMarkerEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMarkerEventEmitter::OnDragStart data = {
.id = std::string([dictionary[@"id"] UTF8String]),
.coordinate = coordinate,
};
eventEmitter->onDragStart(data);
}
};
_view.onDragEnd = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
// Extract values from the NSDictionary
NSDictionary* coordinateDict = dictionary[@"coordinate"];
facebook::react::RNMapsMarkerEventEmitter::OnDragEndCoordinate coordinate = {
.latitude = [coordinateDict[@"latitude"] doubleValue],
.longitude = [coordinateDict[@"longitude"] doubleValue],
};
auto eventEmitter = std::static_pointer_cast<RNMapsMarkerEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMarkerEventEmitter::OnDragEnd data = {
.id = std::string([dictionary[@"id"] UTF8String]),
.coordinate = coordinate,
};
eventEmitter->onDragEnd(data);
}
};
_view.onSelect = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
// Extract values from the NSDictionary
NSDictionary* coordinateDict = dictionary[@"coordinate"];
NSDictionary* positionDict = dictionary[@"position"];
// Populate the OnCalloutPressPoint struct
facebook::react::RNMapsMarkerEventEmitter::OnSelectPosition point = {
.x = [coordinateDict[@"x"] doubleValue],
.y = [coordinateDict[@"y"] doubleValue],
};
facebook::react::RNMapsMarkerEventEmitter::OnSelectCoordinate coordinate = {
.latitude = [coordinateDict[@"latitude"] doubleValue],
.longitude = [coordinateDict[@"longitude"] doubleValue],
};
auto eventEmitter = std::static_pointer_cast<RNMapsMarkerEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMarkerEventEmitter::OnSelect data = {
.action = std::string([dictionary[@"action"] UTF8String]),
.id = std::string([dictionary[@"id"] UTF8String]),
.position = point,
.coordinate = coordinate,
};
eventEmitter->onSelect(data);
}
};
_view.onCalloutPress = [self](NSDictionary* dictionary) {
if (_eventEmitter) {
// Extract values from the NSDictionary
NSDictionary* pointDict = dictionary[@"point"];
NSDictionary* frameDict = dictionary[@"frame"];
// Populate the OnCalloutPressPoint struct
facebook::react::RNMapsMarkerEventEmitter::OnCalloutPressPoint point = {
.x = [pointDict[@"x"] doubleValue],
.y = [pointDict[@"y"] doubleValue],
};
facebook::react::RNMapsMarkerEventEmitter::OnCalloutPressFrame frame = {
.x = [frameDict[@"x"] doubleValue],
.y = [frameDict[@"x"] doubleValue],
.width = [frameDict[@"width"] doubleValue],
.height = [frameDict[@"height"] doubleValue],
};
auto eventEmitter = std::static_pointer_cast<RNMapsMarkerEventEmitter const>(_eventEmitter);
facebook::react::RNMapsMarkerEventEmitter::OnCalloutPress data = {
.action = std::string([dictionary[@"action"] UTF8String]),
.id = std::string([dictionary[@"id"] UTF8String]),
.frame = frame,
.point = point,
};
eventEmitter->onCalloutPress(data);
}
};
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index{
if (!_view){
[self prepareMarkerView];
}
id<RCTComponent> paperView = [childComponentView getPaperViewFromChildComponentView];
if (paperView){
[_view insertReactSubview:paperView atIndex:index];
} else {
[_view insertReactSubview:childComponentView atIndex:index];
}
}
- (void) unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
id<RCTComponent> paperView = [childComponentView getPaperViewFromChildComponentView];
if (paperView){
[_view removeReactSubview:paperView];
} else {
[_view removeReactSubview:childComponentView];
}
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self prepareMarkerView];
}
return self;
}
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
[self prepareMarkerView];
const auto &oldViewProps = *std::static_pointer_cast<RNMapsMarkerProps const>(_props);
const auto &newViewProps = *std::static_pointer_cast<RNMapsMarkerProps const>(props);
BOOL completionHandlerCalled = false;
#define REMAP_MAPVIEW_PROP(name) \
if (oldViewProps.name != newViewProps.name) { \
_view.name = newViewProps.name; \
}
#define REMAP_MAPVIEW_STRING_PROP(name) \
if (oldViewProps.name != newViewProps.name) { \
_view.name = RCTNSStringFromString(newViewProps.name); \
}
#define REMAP_MAPVIEW_COLOR_PROP(name) \
if (oldViewProps.name != newViewProps.name) { \
_view.name = RCTUIColorFromSharedColor(newViewProps.name); \
}
#define REMAP_MAPVIEW_POINT_PROP(name) \
if (newViewProps.name.x != oldViewProps.name.x || \
newViewProps.name.y != oldViewProps.name.y) { \
_view.name = CGPointMake(newViewProps.name.x, newViewProps.name.y); \
}
if (newViewProps.zIndex != oldViewProps.zIndex){
if (newViewProps.zIndex.has_value()){
_view.zIndex = newViewProps.zIndex.value();
} else {
// default
_view.zIndex = 0;
}
}
if (newViewProps.image != oldViewProps.image){
if (newViewProps.image.uri.size()){
[_view setImageSrc:RCTNSStringFromString(newViewProps.image.uri)];
completionHandlerCalled = true;
} else {
[_view setImageSrc:nil];
}
}
REMAP_MAPVIEW_PROP(useLegacyPinView)
REMAP_MAPVIEW_PROP(opacity)
REMAP_MAPVIEW_PROP(draggable)
REMAP_MAPVIEW_PROP(isPreselected)
REMAP_MAPVIEW_COLOR_PROP(pinColor)
REMAP_MAPVIEW_POINT_PROP(calloutOffset)
REMAP_MAPVIEW_POINT_PROP(centerOffset)
if (newViewProps.coordinate.latitude != oldViewProps.coordinate.latitude ||
newViewProps.coordinate.longitude != oldViewProps.coordinate.longitude) {
CLLocationCoordinate2D coordinates = CLLocationCoordinate2DMake(
newViewProps.coordinate.latitude,
newViewProps.coordinate.longitude);
_view.coordinate = coordinates;
}
if (newViewProps.description != oldViewProps.description){
_view.subtitle = RCTNSStringFromString(newViewProps.description);
}
REMAP_MAPVIEW_STRING_PROP(title)
REMAP_MAPVIEW_STRING_PROP(identifier)
if (newViewProps.titleVisibility != oldViewProps.titleVisibility){
switch (newViewProps.titleVisibility) {
case RNMapsMarkerTitleVisibility::Visible:
[_view setTitleVisibility:MKFeatureVisibilityVisible];
break;
case RNMapsMarkerTitleVisibility::Adaptive:
[_view setTitleVisibility:MKFeatureVisibilityAdaptive];
break;
case RNMapsMarkerTitleVisibility::Hidden:
[_view setTitleVisibility:MKFeatureVisibilityHidden];
break;
}
}
if (newViewProps.subtitleVisibility != oldViewProps.subtitleVisibility){
switch (newViewProps.subtitleVisibility) {
case RNMapsMarkerSubtitleVisibility::Visible:
[_view setSubtitleVisibility:MKFeatureVisibilityVisible];
break;
case RNMapsMarkerSubtitleVisibility::Adaptive:
[_view setSubtitleVisibility:MKFeatureVisibilityAdaptive];
break;
case RNMapsMarkerSubtitleVisibility::Hidden:
[_view setSubtitleVisibility:MKFeatureVisibilityHidden];
break;
}
}
if (newViewProps.displayPriority != oldViewProps.displayPriority){
switch (newViewProps.displayPriority) {
case RNMapsMarkerDisplayPriority::Required:
[_view setDisplayPriority:MKFeatureDisplayPriorityRequired];
break;
case RNMapsMarkerDisplayPriority::High:
[_view setDisplayPriority:MKFeatureDisplayPriorityDefaultHigh];
break;
case RNMapsMarkerDisplayPriority::Low:
[_view setDisplayPriority:MKFeatureDisplayPriorityDefaultLow];
break;
}
}
[super updateProps:props oldProps:oldProps];
}
@end
Class<RCTComponentViewProtocol> RNMapsMarkerCls(void)
{
return RNMapsMarkerView.class;
}

View File

@@ -0,0 +1,28 @@
//
// RNMapsMarkerManager.m
// AirMaps
//
// Created by Salah Ghanim on 23.11.24.
// Copyright © 2024 react-native-maps. All rights reserved.
//
#import <React/RCTLog.h>
#import <React/RCTUIManager.h>
#import <React/RCTViewManager.h>
@interface RNMapsMarkerViewManager : RCTViewManager
@end
@implementation RNMapsMarkerViewManager
RCT_EXPORT_MODULE(RNMapsMarkerViewManager)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSelect, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDeselect, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onCalloutPress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDragStart, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDrag, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDragEnd, RCTDirectEventBlock)
@end

View File

@@ -0,0 +1,11 @@
//
// Created by Salah Ghanim on 29/12/2024.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface UIView (AirMap)
- (UIView *) getPaperViewFromChildComponentView;
@end

View File

@@ -0,0 +1,20 @@
//
// Created by Leland Richardson on 12/27/15.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import "UIView+AirMap.h"
@implementation UIView (AirMap)
- (UIView *) getPaperViewFromChildComponentView {
// Check if the childComponentView responds to the "adapter" selector
if ([self respondsToSelector:@selector(paperView)]) {
// Safely return the paperView
return [self valueForKey:@"paperView"];
}
// Return nil if paperView is not accessible
return nil;
}
@end

View File

@@ -0,0 +1,6 @@
framework module ReactNativeMaps {
header "UIView+AirMap.h"
header "RCTConvert+AirMap.h"
export *
}