diff options
Diffstat (limited to 'platform/darwin/src/MGLCircle.mm')
-rw-r--r-- | platform/darwin/src/MGLCircle.mm | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/platform/darwin/src/MGLCircle.mm b/platform/darwin/src/MGLCircle.mm new file mode 100644 index 0000000000..f4f507ac44 --- /dev/null +++ b/platform/darwin/src/MGLCircle.mm @@ -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 |