Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Add MGLCircle (with radius expressed in physical units) #14534

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion platform/darwin/scripts/style-spec-overrides-v8.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"examples": "See the <a href=\"https://docs.mapbox.com/ios/maps/examples/runtime-multiple-annotations/\">Dynamically style interactive points</a> and <a href=\"https://docs.mapbox.com/ios/maps/examples/clustering-with-images/\">Use images to cluster point data</a> examples learn how to style data on your map using this layer."
},
"circle": {
"doc": "An `MGLCircleStyleLayer` is a style layer that renders one or more filled circles on the map.\n\nUse a circle style layer to configure the visual appearance of point or point collection features. These features can come from vector tiles loaded by an `MGLVectorTileSource` object, or they can be `MGLPointAnnotation`, `MGLPointFeature`, `MGLPointCollection`, or `MGLPointCollectionFeature` instances in an `MGLShapeSource` or `MGLComputedShapeSource` object.\n\nA circle style layer renders circles whose radii are measured in screen units. To display circles on the map whose radii correspond to real-world distances, use many-sided regular polygons and configure their appearance using an `MGLFillStyleLayer` object.",
"doc": "An `MGLCircleStyleLayer` is a style layer that renders one or more filled circles on the map.\n\nUse a circle style layer to configure the visual appearance of point or point collection features. These features can come from vector tiles loaded by an `MGLVectorTileSource` object, or they can be `MGLPointAnnotation`, `MGLPointFeature`, `MGLPointCollection`, or `MGLPointCollectionFeature` instances in an `MGLShapeSource` or `MGLComputedShapeSource` object.\n\nA circle style layer renders circles whose radii are measured in screen units. To display circles on the map whose radii correspond to real-world distances, use `MGLCircle` or many-sided regular `MGLPolygon` objects and configure their appearance using an `MGLFillStyleLayer` object.",
"examples": "See the <a href=\"https://docs.mapbox.com/ios/maps/examples/dds-circle-layer/\">Data-driven circles</a>, <a href=\"https://docs.mapbox.com/ios/maps/examples/shape-collection/\">Add multiple shapes from a single shape source</a>, and <a href=\"https://docs.mapbox.com/ios/maps/examples/clustering/\">Cluster point data</a> examples to learn how to add circles to your map using this style layer."
},
"heatmap": {
Expand Down
95 changes: 95 additions & 0 deletions platform/darwin/src/MGLCircle.h
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
Copy link
Contributor

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.

`-[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
177 changes: 177 additions & 0 deletions platform/darwin/src/MGLCircle.mm
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
4 changes: 2 additions & 2 deletions platform/darwin/src/MGLCircleStyleLayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ typedef NS_ENUM(NSUInteger, MGLCircleTranslationAnchor) {

A circle style layer renders circles whose radii are measured in screen units.
To display circles on the map whose radii correspond to real-world distances,
use many-sided regular polygons and configure their appearance using an
`MGLFillStyleLayer` object.
use `MGLCircle` or many-sided regular `MGLPolygon` objects and configure their
appearance using an `MGLFillStyleLayer` object.

You can access an existing circle style layer using the
`-[MGLStyle layerWithIdentifier:]` method if you know its identifier;
Expand Down
26 changes: 26 additions & 0 deletions platform/darwin/src/MGLCircle_Private.h
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
17 changes: 17 additions & 0 deletions platform/darwin/src/MGLGeometry.mm
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#import "MGLFoundation.h"

#import <mbgl/util/projection.hpp>
#import <mbgl/util/constants.hpp>

#if !TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
#import <Cocoa/Cocoa.h>
Expand Down Expand Up @@ -67,6 +68,12 @@ MGLRadianDistance MGLDistanceBetweenRadianCoordinates(MGLRadianCoordinate2D from
return 2 * atan2(sqrt(a), sqrt(1 - a));
}

CLLocationDistance MGLDistanceBetweenLocationCoordinates(CLLocationCoordinate2D from, CLLocationCoordinate2D to) {
MGLRadianDistance radianDistance = MGLDistanceBetweenRadianCoordinates(MGLRadianCoordinateFromLocationCoordinate(from),
MGLRadianCoordinateFromLocationCoordinate(to));
return radianDistance * mbgl::util::EARTH_RADIUS_M;
}

MGLRadianDirection MGLRadianCoordinatesDirection(MGLRadianCoordinate2D from, MGLRadianCoordinate2D to) {
double a = sin(to.longitude - from.longitude) * cos(to.latitude);
double b = cos(from.latitude) * sin(to.latitude)
Expand All @@ -84,6 +91,16 @@ MGLRadianCoordinate2D MGLRadianCoordinateAtDistanceFacingDirection(MGLRadianCoor
return MGLRadianCoordinate2DMake(otherLatitude, otherLongitude);
}

CLLocationCoordinate2D MGLCoordinateAtDistanceFacingDirection(CLLocationCoordinate2D coordinate,
CLLocationDistance distance,
CLLocationDirection direction) {
MGLRadianCoordinate2D radianCenter = MGLRadianCoordinateFromLocationCoordinate(coordinate);
MGLRadianCoordinate2D radianVertex = MGLRadianCoordinateAtDistanceFacingDirection(radianCenter,
distance / mbgl::util::EARTH_RADIUS_M,
MGLRadiansFromDegrees(direction));
return MGLLocationCoordinateFromRadianCoordinate(radianVertex);
}

CLLocationDirection MGLDirectionBetweenCoordinates(CLLocationCoordinate2D firstCoordinate, CLLocationCoordinate2D secondCoordinate) {
// Ported from https://github.com/mapbox/turf-swift/blob/857e2e8060678ef4a7a9169d4971b0788fdffc37/Turf/Turf.swift#L23-L31
MGLRadianCoordinate2D firstRadianCoordinate = MGLRadianCoordinateFromLocationCoordinate(firstCoordinate);
Expand Down
14 changes: 14 additions & 0 deletions platform/darwin/src/MGLGeometry_Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,21 @@ NS_INLINE MGLRadianCoordinate2D MGLRadianCoordinateFromLocationCoordinate(CLLoca
MGLRadiansFromDegrees(locationCoordinate.longitude));
}

NS_INLINE CLLocationCoordinate2D MGLLocationCoordinateFromRadianCoordinate(MGLRadianCoordinate2D radianCoordinate) {
return CLLocationCoordinate2DMake(MGLDegreesFromRadians(radianCoordinate.latitude),
MGLDegreesFromRadians(radianCoordinate.longitude));
}

/**
Returns the distance in radians given two coordinates.
*/
MGLRadianDistance MGLDistanceBetweenRadianCoordinates(MGLRadianCoordinate2D from, MGLRadianCoordinate2D to);

/**
Returns the distance given two coordinates.
*/
CLLocationDistance MGLDistanceBetweenLocationCoordinates(CLLocationCoordinate2D from, CLLocationCoordinate2D to);

/**
Returns direction in radians given two coordinates.
*/
Expand All @@ -129,6 +139,10 @@ MGLRadianCoordinate2D MGLRadianCoordinateAtDistanceFacingDirection(MGLRadianCoor
MGLRadianDistance distance,
MGLRadianDirection direction);

CLLocationCoordinate2D MGLCoordinateAtDistanceFacingDirection(CLLocationCoordinate2D coordinate,
CLLocationDistance distance,
CLLocationDirection direction);

/**
Returns the direction from one coordinate to another.
*/
Expand Down
Loading