This repository has been archived by the owner on Aug 8, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Add MGLCircle (with radius expressed in physical units) #14534
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
#import <Foundation/Foundation.h> | ||
#import <CoreLocation/CoreLocation.h> | ||
|
||
#import "MGLShape.h" | ||
#import "MGLGeometry.h" | ||
|
||
#import "MGLTypes.h" | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
/** | ||
An `MGLCircle` object represents a closed, circular shape also known as a | ||
<a href="https://en.wikipedia.org/wiki/Spherical_cap">spherical cap</a>. A | ||
circle is defined by a center coordinate, specified as a | ||
`CLLocationCoordinate2D` instance, and a physical radius measured in meters. | ||
The circle is approximated as a polygon with a large number of vertices and | ||
edges. Due to the map’s Spherical Mercator projection, a large enough circle | ||
appears as an elliptical or even sinusoidal shape. You could use a circle to | ||
visualize for instance an impact zone, the `CLLocation.horizontalAccuracy` of a | ||
GPS location update, the regulated airspace around an airport, or the | ||
addressable consumer market around a city. | ||
|
||
You can add a circle overlay directly to a map view using the | ||
`-[MGLMapView addAnnotation:]` or `-[MGLMapView addOverlay:]` method. Configure | ||
a circle overlay’s appearance using | ||
`-[MGLMapViewDelegate mapView:strokeColorForShapeAnnotation:]` and | ||
`-[MGLMapViewDelegate mapView:fillColorForShape:]`. | ||
|
||
Alternatively, you can add a circle to the map by adding it to an | ||
`MGLShapeSource` object. Because GeoJSON cannot represent a curve per se, the | ||
circle is automatically converted to a polygon. See the `MGLPolygon` class for | ||
more information about polygons. | ||
|
||
Do not confuse this class with `MGLCircleStyleLayer`, which renders a circle | ||
defined by a center coordinate and a fixed _screen_ radius measured in points | ||
regardless of the map’s zoom level. | ||
|
||
The polygon that approximates an `MGLCircle` has a large number of vertices. | ||
If you do not need the circle to appear smooth at every possible zoom level, | ||
use a many-sided regular `MGLPolygon` instead for better performance. | ||
*/ | ||
MGL_EXPORT | ||
@interface MGLCircle : MGLShape | ||
|
||
/** | ||
The coordinate around which the circle is centered. | ||
|
||
Each coordinate along the circle’s edge is equidistant from this coordinate. | ||
The center coordinate’s latitude helps determine the minimum spacing between | ||
each vertex along the edge of the polygon that approximates the circle. | ||
*/ | ||
@property (nonatomic) CLLocationCoordinate2D coordinate; | ||
|
||
/** | ||
The radius of the circular area, measured in meters across the Earth’s surface. | ||
*/ | ||
@property (nonatomic) CLLocationDistance radius; | ||
|
||
/** | ||
Creates and returns an `MGLCircle` object centered around the given coordinate | ||
and extending in all directions by the given physical distance. | ||
|
||
@param centerCoordinate The coordinate around which the circle is centered. | ||
@param radius The radius of the circular area, measured in meters across the | ||
Earth’s surface. | ||
@return A new circle object. | ||
*/ | ||
+ (instancetype)circleWithCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate radius:(CLLocationDistance)radius; | ||
|
||
/** | ||
Creates and returns an `MGLCircle` object that fills the given coordinate | ||
bounds. | ||
|
||
@param coordinateBounds The coordinate bounds to fill. The circle is centered | ||
around the center of the coordinate bounds. The circle’s edge touches at | ||
least two of the sides of the coordinate bounds. If the coordinate bounds | ||
does not represent a square area, the circle extends beyond two of its | ||
sides. | ||
@return A new circle object. | ||
*/ | ||
+ (instancetype)circleWithCoordinateBounds:(MGLCoordinateBounds)coordinateBounds; | ||
|
||
/** | ||
The smallest coordinate rectangle that completely encompasses the circle. | ||
|
||
If the circle spans the antimeridian, its bounds may extend west of −180 | ||
degrees longitude or east of 180 degrees longitude. For example, a circle | ||
covering the Pacific Ocean from Tokyo to San Francisco might have a bounds | ||
extending from (35.68476, −220.24257) to (37.78428, −122.41310). | ||
*/ | ||
@property (nonatomic, readonly) MGLCoordinateBounds coordinateBounds; | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
#import "MGLCircle.h" | ||
|
||
#import "MGLGeometry_Private.h" | ||
#import "MGLMultiPoint_Private.h" | ||
#import "NSCoder+MGLAdditions.h" | ||
|
||
#import <mbgl/util/projection.hpp> | ||
|
||
#import <vector> | ||
|
||
@implementation MGLCircle | ||
|
||
@synthesize coordinate = _coordinate; | ||
|
||
+ (instancetype)circleWithCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate radius:(CLLocationDistance)radius { | ||
return [[self alloc] initWithCenterCoordinate:centerCoordinate radius:radius]; | ||
} | ||
|
||
+ (instancetype)circleWithCoordinateBounds:(MGLCoordinateBounds)coordinateBounds { | ||
MGLCoordinateSpan span = MGLCoordinateBoundsGetCoordinateSpan(coordinateBounds); | ||
BOOL latitudinal = span.latitudeDelta > span.longitudeDelta; | ||
// TODO: Latitudinal distances aren’t uniform, so get the mean northing. | ||
CLLocationCoordinate2D center = CLLocationCoordinate2DMake(coordinateBounds.ne.latitude - span.latitudeDelta / 2.0, | ||
coordinateBounds.ne.longitude - span.longitudeDelta / 2.0); | ||
CLLocationCoordinate2D southOrWest = CLLocationCoordinate2DMake(latitudinal ? coordinateBounds.sw.latitude : 0, | ||
latitudinal ? 0 : coordinateBounds.sw.longitude); | ||
CLLocationCoordinate2D northOrEast = CLLocationCoordinate2DMake(latitudinal ? coordinateBounds.ne.latitude : 0, | ||
latitudinal ? 0 : coordinateBounds.ne.longitude); | ||
CLLocationDistance majorAxis = MGLDistanceBetweenLocationCoordinates(southOrWest, northOrEast); | ||
return [[self alloc] initWithCenterCoordinate:center radius:majorAxis / 2.0]; | ||
} | ||
|
||
- (instancetype)initWithCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate radius:(CLLocationDistance)radius { | ||
if (self = [super init]) { | ||
_coordinate = centerCoordinate; | ||
_radius = radius; | ||
} | ||
return self; | ||
} | ||
|
||
- (instancetype)initWithCoder:(NSCoder *)decoder { | ||
if (self = [super initWithCoder:decoder]) { | ||
_coordinate = [decoder decodeMGLCoordinateForKey:@"coordinate"]; | ||
_radius = [decoder decodeDoubleForKey:@"radius"]; | ||
} | ||
return self; | ||
} | ||
|
||
- (void)encodeWithCoder:(NSCoder *)coder { | ||
[super encodeWithCoder:coder]; | ||
[coder encodeMGLCoordinate:_coordinate forKey:@"coordinate"]; | ||
[coder encodeDouble:_radius forKey:@"radius"]; | ||
} | ||
|
||
- (BOOL)isEqual:(id)other { | ||
if (other == self) { | ||
return YES; | ||
} | ||
if (![other isKindOfClass:[MGLCircle class]]) { | ||
return NO; | ||
} | ||
|
||
MGLCircle *otherCircle = other; | ||
return ([super isEqual:other] | ||
&& self.coordinate.latitude == otherCircle.coordinate.latitude | ||
&& self.coordinate.longitude == otherCircle.coordinate.longitude | ||
&& self.radius == otherCircle.radius); | ||
} | ||
|
||
- (NSUInteger)hash { | ||
return super.hash + @(self.coordinate.latitude).hash + @(self.coordinate.longitude).hash; | ||
} | ||
|
||
- (NSUInteger)numberOfVertices { | ||
// Due to the z16 zoom level and Douglas–Peucker tolerance specified by | ||
// mbgl::ShapeAnnotationImpl::updateTileData() and GeoJSONVT, the smallest | ||
// circle that can be displayed at z22 at the poles has a radius of about | ||
// 5 centimeters and is simplified to four sides each about 0.31 meters | ||
// (50 points) long. The smallest displayable circle at the Equator has a | ||
// radius of about 5 decimeters and is simplified to four sides each about | ||
// 3.1 meters (75 points) long. | ||
constexpr NSUInteger maximumZoomLevel = 16; | ||
CLLocationDistance maximumEdgeLength = mbgl::Projection::getMetersPerPixelAtLatitude(self.coordinate.latitude, maximumZoomLevel); | ||
CLLocationDistance circumference = 2 * M_PI * self.radius; | ||
NSUInteger maximumSides = ceil(fabs(circumference) / maximumEdgeLength); | ||
|
||
// The smallest perceptible angle is about 1 arcminute. | ||
// https://en.wikipedia.org/wiki/Naked_eye#Small_objects_and_maps | ||
constexpr CLLocationDirection maximumInternalAngle = 180.0 - 1.0 / 60; | ||
constexpr CLLocationDirection maximumCentralAngle = 180.0 - maximumInternalAngle; | ||
constexpr CGFloat maximumVertices = 360.0 / maximumCentralAngle; | ||
|
||
// Make the circle’s resolution high enough that the user can’t perceive any | ||
// angles, but not so high that detail would be lost through simplification. | ||
return ceil(MIN(maximumSides, maximumVertices)); | ||
} | ||
|
||
- (mbgl::LinearRing<double>)linearRingWithNumberOfVertices:(NSUInteger)numberOfVertices { | ||
CLLocationCoordinate2D center = self.coordinate; | ||
CLLocationDistance radius = fabs(self.radius); | ||
|
||
mbgl::LinearRing<double> ring; | ||
ring.reserve(numberOfVertices); | ||
for (NSUInteger i = 0; i < numberOfVertices; i++) { | ||
// Start at due north and go counterclockwise, or phase shift by 90° if | ||
// centered in the southern hemisphere, so it’s easy to fix up for ±90° | ||
// latitude in the conditional below. | ||
CLLocationDirection direction = 360.0 / numberOfVertices * i + (center.latitude >= 0 ? 0 : 180); | ||
CLLocationCoordinate2D vertex = MGLCoordinateAtDistanceFacingDirection(center, radius, direction); | ||
// If the circle extends to ±90° latitude and has wrapped around, extend | ||
// the polygon to include all of ±90° latitude and beyond. | ||
if (i == 0 && radius > 1 | ||
&& fabs(vertex.latitude) < fabs(MGLCoordinateAtDistanceFacingDirection(center, radius - 1, direction).latitude)) { | ||
short hemisphere = center.latitude >= 0 ? 1 : -1; | ||
ring.push_back({ center.longitude - 180.0, vertex.latitude }); | ||
ring.push_back({ center.longitude - 180.0, 90.0 * hemisphere }); | ||
ring.push_back({ center.longitude + 180.0, 90.0 * hemisphere }); | ||
} | ||
ring.push_back(MGLPointFromLocationCoordinate2D(vertex)); | ||
} | ||
return ring; | ||
} | ||
|
||
- (mbgl::Polygon<double>)polygon { | ||
mbgl::Polygon<double> polygon; | ||
polygon.push_back([self linearRingWithNumberOfVertices:self.numberOfVertices]); | ||
return polygon; | ||
} | ||
|
||
- (mbgl::Geometry<double>)geometryObject { | ||
return [self polygon]; | ||
} | ||
|
||
- (NSDictionary *)geoJSONDictionary { | ||
return @{ | ||
@"type": @"Polygon", | ||
@"coordinates": self.geoJSONGeometry, | ||
}; | ||
} | ||
|
||
- (NSArray<id> *)geoJSONGeometry { | ||
NSMutableArray *coordinates = [NSMutableArray array]; | ||
|
||
mbgl::LinearRing<double> ring = [self polygon][0]; | ||
NSMutableArray *geoJSONRing = [NSMutableArray array]; | ||
for (auto &point : ring) { | ||
[geoJSONRing addObject:@[@(point.x), @(point.y)]]; | ||
} | ||
[coordinates addObject:geoJSONRing]; | ||
|
||
return [coordinates copy]; | ||
} | ||
|
||
- (mbgl::Annotation)annotationObjectWithDelegate:(id <MGLMultiPointDelegate>)delegate { | ||
|
||
mbgl::FillAnnotation annotation { [self polygon] }; | ||
annotation.opacity = { static_cast<float>([delegate alphaForShapeAnnotation:self]) }; | ||
annotation.outlineColor = { [delegate strokeColorForShapeAnnotation:self] }; | ||
annotation.color = { [delegate fillColorForShape:self] }; | ||
|
||
return annotation; | ||
} | ||
|
||
- (MGLCoordinateBounds)coordinateBounds { | ||
mbgl::LinearRing<double> ring = [self linearRingWithNumberOfVertices:4]; | ||
CLLocationCoordinate2D southWest = CLLocationCoordinate2DMake(ring[2].y, ring[3].x); | ||
CLLocationCoordinate2D northEast = CLLocationCoordinate2DMake(ring[0].y, ring[1].x); | ||
return MGLCoordinateBoundsMake(southWest, northEast); | ||
} | ||
|
||
- (NSString *)description { | ||
return [NSString stringWithFormat:@"<%@: %p; coordinate = %@; radius = %f m>", | ||
NSStringFromClass([self class]), (void *)self, | ||
MGLStringFromCLLocationCoordinate2D(self.coordinate), self.radius]; | ||
} | ||
|
||
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
#import "MGLCircle.h" | ||
|
||
#import <mbgl/annotation/annotation.hpp> | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
@protocol MGLMultiPointDelegate; | ||
|
||
@interface MGLCircle (Private) | ||
|
||
/** | ||
The optimal number of vertices in the circle’s polygonal approximation. | ||
*/ | ||
@property (nonatomic, readonly) NSUInteger numberOfVertices; | ||
|
||
/** | ||
Returns a linear ring with the given number of vertices. | ||
*/ | ||
- (mbgl::LinearRing<double>)linearRingWithNumberOfVertices:(NSUInteger)numberOfVertices; | ||
|
||
/** Constructs a circle annotation object, asking the delegate for style values. */ | ||
- (mbgl::Annotation)annotationObjectWithDelegate:(id <MGLMultiPointDelegate>)delegate; | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it is necessary to call out that this type does not support all the same paint/layout options as a shape added to an
MGLFillStyleLayer
? I'm thinking specifically of fill patterns, but there are probably others as well.