Skip to content

Commit

Permalink
Add gradient/multi-color polyline support for iOS (MapKit) (#1911)
Browse files Browse the repository at this point in the history
* Add multi/gradient polyline support for iOS (MapKit)

* Fixed MapExplorer not building with RN51

* Renamed color/color2 to startColor & endColor for new Polyline renderer

* Fixed gradient line sometimes not visible on certain zoom resolutions
  • Loading branch information
IjzerenHein authored and rborn committed Dec 23, 2017
1 parent 8b28c64 commit fef05cd
Show file tree
Hide file tree
Showing 12 changed files with 442 additions and 46 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,14 @@ So far, `<Circle />`, `<Polygon />`, and `<Polyline />` are available to pass in



### Gradient Polylines (iOS MapKit only)

Gradient polylines can be created using the `strokeColors` prop of the `<Polyline>` component.

![](https://i.imgur.com/P7UeqAm.png?1)



### Default Markers

Default markers will be rendered unless a custom marker is specified. One can optionally adjust the
Expand Down
32 changes: 32 additions & 0 deletions docs/polyline.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| `coordinates` | `Array<LatLng>` | (Required) | An array of coordinates to describe the polyline
| `strokeWidth` | `Number` | `1` | The stroke width to use for the path.
| `strokeColor` | `String` | `#000` | The stroke color to use for the path.
| `strokeColors` | `Array<String>` | `null` | The stroke colors to use for the path (iOS only). Must be the same length as `coordinates`.
| `lineCap` | `String` | `round` | The line cap style to apply to the open ends of the path. Possible values are `butt`, `round` or `square`.
| `lineJoin` | `String` | `round` | The line join style to apply to corners of the path. Possible values are `miter`, `round` or `bevel`.
| `miterLimit` | `Number` | | The limiting value that helps avoid spikes at junctions between connected line segments. The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If the ratio of the miter length—that is, the diagonal length of the miter join—to the line thickness exceeds the miter limit, the joint is converted to a bevel join. The default miter limit is 10, which results in the conversion of miters whose angle at the joint is less than 11 degrees.
Expand All @@ -28,3 +29,34 @@ type LatLng {
longitude: Number,
}
```

## Gradient Polylines (iOS MapKit only)

Gradient polylines can be created by using the `strokeColors` prop. `strokeColors` must be an array with the same number of elements as `coordinates`.

Example:

```js
<MapView>
<MapView.Polyline
coordinates={[
{ latitude: 37.8025259, longitude: -122.4351431 },
{ latitude: 37.7896386, longitude: -122.421646 },
{ latitude: 37.7665248, longitude: -122.4161628 },
{ latitude: 37.7734153, longitude: -122.4577787 },
{ latitude: 37.7948605, longitude: -122.4596065 },
{ latitude: 37.8025259, longitude: -122.4351431 }
])
strokeColor="#000" // fallback for when `strokeColors` is not supported by the map-provider
strokeColors={[
'#7F0000',
'#00000000', // no color, creates a "long" gradient between the previous and next coordinate
'#B24112',
'#E5845C',
'#238C23',
'#7F0000'
]}
strokeWidth={6}
/>
</MapView>
```
2 changes: 2 additions & 0 deletions example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import MarkerTypes from './examples/MarkerTypes';
import DraggableMarkers from './examples/DraggableMarkers';
import PolygonCreator from './examples/PolygonCreator';
import PolylineCreator from './examples/PolylineCreator';
import GradientPolylines from './examples/GradientPolylines';
import AnimatedViews from './examples/AnimatedViews';
import AnimatedMarkers from './examples/AnimatedMarkers';
import Callouts from './examples/Callouts';
Expand Down Expand Up @@ -131,6 +132,7 @@ class App extends React.Component {
[DraggableMarkers, 'Draggable Markers', true],
[PolygonCreator, 'Polygon Creator', true],
[PolylineCreator, 'Polyline Creator', true],
[GradientPolylines, 'Gradient Polylines', true],
[AnimatedViews, 'Animating with MapViews'],
[AnimatedMarkers, 'Animated Marker Position'],
[Callouts, 'Custom Callouts', true],
Expand Down
77 changes: 77 additions & 0 deletions example/examples/GradientPolylines.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import {
StyleSheet,
Dimensions,
} from 'react-native';

import MapView from 'react-native-maps';

const { width, height } = Dimensions.get('window');

const ASPECT_RATIO = width / height;
const LATITUDE = 37.78825;
const LONGITUDE = -122.4324;
const LATITUDE_DELTA = 0.0922;
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;

const COORDINATES = [
{ latitude: 37.8025259, longitude: -122.4351431 },
{ latitude: 37.7896386, longitude: -122.421646 },
{ latitude: 37.7665248, longitude: -122.4161628 },
{ latitude: 37.7734153, longitude: -122.4577787 },
{ latitude: 37.7948605, longitude: -122.4596065 },
{ latitude: 37.8025259, longitude: -122.4351431 },
];

const COLORS = [
'#7F0000',
'#00000000', // no color, creates a "long" gradient between the previous and next coordinate
'#B24112',
'#E5845C',
'#238C23',
'#7F0000',
];

class GradientPolylines extends React.Component {
constructor(props) {
super(props);

this.state = {
region: {
latitude: LATITUDE,
longitude: LONGITUDE,
latitudeDelta: LATITUDE_DELTA,
longitudeDelta: LONGITUDE_DELTA,
},
};
}

render() {
return (
<MapView
provider={this.props.provider}
style={styles.container}
initialRegion={this.state.region}
>
<MapView.Polyline
coordinates={COORDINATES}
strokeColor="#000"
strokeColors={COLORS}
strokeWidth={6}
/>
</MapView>
);
}
}

GradientPolylines.propTypes = {
provider: MapView.ProviderPropType,
};

const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
},
});

module.exports = GradientPolylines;
2 changes: 1 addition & 1 deletion example/ios/AirMapsExplorer.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "../../node_modules/react-native/packager/react-native-xcode.sh";
shellScript = "../../node_modules/react-native/scripts/react-native-xcode.sh";
};
17E7BB4CEC38ABB7449E144E /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
Expand Down
5 changes: 5 additions & 0 deletions lib/components/MapPolyline.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ const propTypes = {
*/
strokeColor: PropTypes.string,

/**
* The stroke colors to use for the path.
*/
strokeColors: PropTypes.arrayOf(PropTypes.string),

/**
* The order in which this tile overlay is drawn with respect to other overlays. An overlay
* with a larger z-index is drawn over overlays with smaller z-indices. The order of overlays
Expand Down
6 changes: 6 additions & 0 deletions lib/ios/AirMaps.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
1125B2E61C4AD3DA007D0023 /* AIRMapPolylineManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1125B2D61C4AD3DA007D0023 /* AIRMapPolylineManager.m */; };
1125B2F21C4AD445007D0023 /* SMCalloutView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1125B2F11C4AD445007D0023 /* SMCalloutView.m */; };
19DABC7F1E7C9D3C00F41150 /* RCTConvert+AirMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 19DABC7E1E7C9D3C00F41150 /* RCTConvert+AirMap.m */; };
2163AA501FEAEDD100BBEC95 /* AIRMapPolylineRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = 2163AA4F1FEAEDD100BBEC95 /* AIRMapPolylineRenderer.m */; };
628F81201FD16DF80058313A /* AIRMapLocalTile.m in Sources */ = {isa = PBXBuildFile; fileRef = 628F811F1FD16DF80058313A /* AIRMapLocalTile.m */; };
628F81231FD16EFA0058313A /* AIRMapLocalTileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 628F81221FD16EFA0058313A /* AIRMapLocalTileManager.m */; };
62AEC4D41FD5A0AA003225E0 /* AIRMapLocalTileOverlay.m in Sources */ = {isa = PBXBuildFile; fileRef = 62AEC4D31FD5A0AA003225E0 /* AIRMapLocalTileOverlay.m */; };
Expand Down Expand Up @@ -73,6 +74,8 @@
11FA5C511C4A1296003AC2EE /* libAirMaps.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libAirMaps.a; sourceTree = BUILT_PRODUCTS_DIR; };
19DABC7D1E7C9D3C00F41150 /* RCTConvert+AirMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+AirMap.h"; sourceTree = "<group>"; };
19DABC7E1E7C9D3C00F41150 /* RCTConvert+AirMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+AirMap.m"; sourceTree = "<group>"; };
2163AA4E1FEAEDD100BBEC95 /* AIRMapPolylineRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapPolylineRenderer.h; sourceTree = "<group>"; };
2163AA4F1FEAEDD100BBEC95 /* AIRMapPolylineRenderer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMapPolylineRenderer.m; sourceTree = "<group>"; };
628F811E1FD16D780058313A /* AIRMapLocalTile.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AIRMapLocalTile.h; sourceTree = "<group>"; };
628F811F1FD16DF80058313A /* AIRMapLocalTile.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AIRMapLocalTile.m; sourceTree = "<group>"; };
628F81211FD16EAB0058313A /* AIRMapLocalTileManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AIRMapLocalTileManager.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -141,6 +144,8 @@
1125B2D31C4AD3DA007D0023 /* AIRMapPolyline.h */,
1125B2D51C4AD3DA007D0023 /* AIRMapPolylineManager.h */,
1125B2D61C4AD3DA007D0023 /* AIRMapPolylineManager.m */,
2163AA4E1FEAEDD100BBEC95 /* AIRMapPolylineRenderer.h */,
2163AA4F1FEAEDD100BBEC95 /* AIRMapPolylineRenderer.m */,
1125B2F01C4AD445007D0023 /* SMCalloutView.h */,
1125B2F11C4AD445007D0023 /* SMCalloutView.m */,
19DABC7D1E7C9D3C00F41150 /* RCTConvert+AirMap.h */,
Expand Down Expand Up @@ -227,6 +232,7 @@
1125B2DA1C4AD3DA007D0023 /* AIRMap.m in Sources */,
1125B2DF1C4AD3DA007D0023 /* AIRMapCoordinate.m in Sources */,
1125B2F21C4AD445007D0023 /* SMCalloutView.m in Sources */,
2163AA501FEAEDD100BBEC95 /* AIRMapPolylineRenderer.m in Sources */,
1125B2E11C4AD3DA007D0023 /* AIRMapMarker.m in Sources */,
1125B2E21C4AD3DA007D0023 /* AIRMapMarkerManager.m in Sources */,
DA6C26381C9E2AFE0035349F /* AIRMapUrlTile.m in Sources */,
Expand Down
3 changes: 2 additions & 1 deletion lib/ios/AirMaps/AIRMapPolyline.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
@property (nonatomic, weak) AIRMap *map;

@property (nonatomic, strong) MKPolyline *polyline;
@property (nonatomic, strong) MKPolylineRenderer *renderer;
@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;
Expand Down
89 changes: 45 additions & 44 deletions lib/ios/AirMaps/AIRMapPolyline.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
//

#import "AIRMapPolyline.h"
#import "AIRMapPolylineRenderer.h"
#import <React/UIView+React.h>


@implementation AIRMapPolyline {

}

- (void)setFillColor:(UIColor *)fillColor {
Expand All @@ -21,6 +22,14 @@ - (void)setStrokeColor:(UIColor *)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];
Expand Down Expand Up @@ -59,27 +68,48 @@ - (void)setCoordinates:(NSArray<AIRMapCoordinate *> *)coordinates {
coords[i] = coordinates[i].coordinate;
}
self.polyline = [MKPolyline polylineWithCoordinates:coords count:coordinates.count];
self.renderer = [[MKPolylineRenderer alloc] initWithPolyline:self.polyline];
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;
_renderer.fillColor = _fillColor;
_renderer.strokeColor = _strokeColor;
_renderer.lineWidth = _strokeWidth;
_renderer.lineCap = _lineCap;
_renderer.lineJoin = _lineJoin;
_renderer.miterLimit = _miterLimit;
_renderer.lineDashPhase = _lineDashPhase;
_renderer.lineDashPattern = _lineDashPattern;

[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

Expand Down Expand Up @@ -109,38 +139,9 @@ - (BOOL)canReplaceMapContent

- (void) drawToSnapshot:(MKMapSnapshot *) snapshot context:(CGContextRef) context
{
// Prepare context
CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor);
CGContextSetLineWidth(context, self.strokeWidth);
CGContextSetLineCap(context, self.lineCap);
CGContextSetLineJoin(context, self.lineJoin);
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);

// Begin path
CGContextBeginPath(context);

// Get coordinates
CLLocationCoordinate2D coordinates[[self.polyline pointCount]];
[self.polyline getCoordinates:coordinates range:NSMakeRange(0, [self.polyline pointCount])];

// Draw line segments
for(int i = 0; i < [self.polyline pointCount]; i++) {
CGPoint point = [snapshot pointForCoordinate:coordinates[i]];
if (i == 0) {
CGContextMoveToPoint(context,point.x, point.y);
}
else{
CGContextAddLineToPoint(context,point.x, point.y);
}
}

// Finish path
CGContextStrokePath(context);
AIRMapPolylineRenderer* renderer = [[AIRMapPolylineRenderer alloc] initWithSnapshot:snapshot overlay:self polyline:self.polyline];
[self updateRenderer:renderer];
[renderer drawWithZoomScale:2 inContext:context];
}

@end
@end
1 change: 1 addition & 0 deletions lib/ios/AirMaps/AIRMapPolylineManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ - (UIView *)view

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)
Expand Down
19 changes: 19 additions & 0 deletions lib/ios/AirMaps/AIRMapPolylineRenderer.h
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit fef05cd

Please sign in to comment.