summaryrefslogtreecommitdiff
path: root/platform/macos/src
diff options
context:
space:
mode:
Diffstat (limited to 'platform/macos/src')
-rw-r--r--platform/macos/src/MGLMapView.h52
-rw-r--r--platform/macos/src/MGLMapView.mm190
-rw-r--r--platform/macos/src/Mapbox.h5
-rw-r--r--platform/macos/src/NSColor+MGLAdditions.h9
-rw-r--r--platform/macos/src/NSColor+MGLAdditions.mm90
5 files changed, 257 insertions, 89 deletions
diff --git a/platform/macos/src/MGLMapView.h b/platform/macos/src/MGLMapView.h
index 96b0932c14..e3de1069a9 100644
--- a/platform/macos/src/MGLMapView.h
+++ b/platform/macos/src/MGLMapView.h
@@ -397,11 +397,10 @@ MGL_EXPORT IB_DESIGNABLE
want to animate the change, use the `-setVisibleCoordinateBounds:animated:`
method instead.
- If a longitude is less than −180 degrees or greater than 180 degrees, the visible
- bounds straddles the antimeridian or international date line.
-
- For example, a visible bounds that stretches from Tokyo to San Francisco would have
- coordinates of (35.68476, -220.24257) and (37.78428, -122.41310).
+ If a longitude is less than −180 degrees or greater than 180 degrees, the
+ visible bounds straddles the antimeridian or international date line. For
+ example, if both Tokyo and San Francisco are visible, the visible bounds might
+ extend from (35.68476, −220.24257) to (37.78428, −122.41310).
*/
@property (nonatomic) MGLCoordinateBounds visibleCoordinateBounds;
@@ -409,11 +408,10 @@ MGL_EXPORT IB_DESIGNABLE
Changes the receiver’s viewport to fit the given coordinate bounds, optionally
animating the change.
- To make the visible bounds go across the antimeridian or international date line,
- specify some longitudes less than −180 degrees or greater than 180 degrees.
-
- For example, a visible bounds that stretches from Tokyo to San Francisco would have
- coordinates of (35.68476, -220.24257) and (37.78428, -122.41310).
+ To bring both sides of the antimeridian or international date line into view,
+ specify some longitudes less than −180 degrees or greater than 180 degrees. For
+ example, to show both Tokyo and San Francisco simultaneously, you could set the
+ visible bounds to extend from (35.68476, −220.24257) to (37.78428, −122.41310).
@param bounds The bounds that the viewport will show in its entirety.
@param animated Specify `YES` to animate the change by smoothly scrolling and
@@ -723,16 +721,27 @@ MGL_EXPORT IB_DESIGNABLE
Assigning a new array to this property selects only the first annotation in the
array.
+
+ If the annotation is of type `MGLPointAnnotation` and is offscreen, the map is
+ panned so that the annotation and its callout are brought just onscreen. The
+ annotation is *not* centered within the viewport.
+
+ @note In versions prior to `4.0.0` if the annotation was offscreen it was not
+ selected.
*/
@property (nonatomic, copy) NS_ARRAY_OF(id <MGLAnnotation>) *selectedAnnotations;
/**
Selects an annotation and displays a callout popover for it.
- If the given annotation is not visible within the current viewport, this method
- has no effect.
+ If the annotation is of type `MGLPointAnnotation` and is offscreen, the map is
+ panned so that the annotation and its callout are brought just onscreen. The
+ annotation is *not* centered within the viewport.
@param annotation The annotation object to select.
+
+ @note In versions prior to `4.0.0` selecting an offscreen annotation did not
+ change the camera.
*/
- (void)selectAnnotation:(id <MGLAnnotation>)annotation;
@@ -866,9 +875,9 @@ MGL_EXPORT IB_DESIGNABLE
Each object in the returned array represents a feature rendered by the
current style and provides access to attributes specified by the relevant map
content sources. The returned array includes features loaded by
- `MGLShapeSource` and `MGLVectorSource` objects but does not include anything
- from `MGLRasterSource` objects, or from image, video, or canvas sources, which
- are unsupported by this SDK.
+ `MGLShapeSource` and `MGLVectorTileSource` objects but does not include
+ anything from `MGLRasterTileSource` objects, or from video or canvas sources,
+ which are unsupported by this SDK.
The returned features are drawn by a style layer in the current style. For
example, suppose the current style uses the
@@ -900,7 +909,7 @@ MGL_EXPORT IB_DESIGNABLE
Only visible features are returned. To obtain features regardless of
visibility, use the
- `-[MGLVectorSource featuresInSourceLayersWithIdentifiers:predicate:]` and
+ `-[MGLVectorTileSource featuresInSourceLayersWithIdentifiers:predicate:]` and
`-[MGLShapeSource featuresMatchingPredicate:]` methods on the relevant sources.
@note Layer identifiers are not guaranteed to exist across styles or different
@@ -963,9 +972,9 @@ MGL_EXPORT IB_DESIGNABLE
Each object in the returned array represents a feature rendered by the
current style and provides access to attributes specified by the relevant map
content sources. The returned array includes features loaded by
- `MGLShapeSource` and `MGLVectorSource` objects but does not include anything
- from `MGLRasterSource` objects, or from image, video, or canvas sources, which
- are unsupported by this SDK.
+ `MGLShapeSource` and `MGLVectorTileSource` objects but does not include
+ anything from `MGLRasterTileSource` objects, or from video or canvas sources,
+ which are unsupported by this SDK.
The returned features are drawn by a style layer in the current style. For
example, suppose the current style uses the
@@ -998,7 +1007,7 @@ MGL_EXPORT IB_DESIGNABLE
Only visible features are returned. To obtain features regardless of
visibility, use the
- `-[MGLVectorSource featuresInSourceLayersWithIdentifiers:predicate:]` and
+ `-[MGLVectorTileSource featuresInSourceLayersWithIdentifiers:predicate:]` and
`-[MGLShapeSource featuresMatchingPredicate:]` methods on the relevant sources.
@note Layer identifiers are not guaranteed to exist across styles or different
@@ -1060,6 +1069,9 @@ MGL_EXPORT IB_DESIGNABLE
/**
Converts a rectangle in the given view’s coordinate system to a geographic
bounding box.
+
+ If a longitude is less than −180 degrees or greater than 180 degrees, the
+ bounding box straddles the antimeridian or international date line.
@param rect The rectangle to convert.
@param view The view in whose coordinate system the rectangle is expressed.
diff --git a/platform/macos/src/MGLMapView.mm b/platform/macos/src/MGLMapView.mm
index 9ec9012198..9cab9a76da 100644
--- a/platform/macos/src/MGLMapView.mm
+++ b/platform/macos/src/MGLMapView.mm
@@ -97,6 +97,9 @@ const CGFloat MGLAnnotationImagePaddingForHitTest = 4;
/// Distance from the callout’s anchor point to the annotation it points to.
const CGFloat MGLAnnotationImagePaddingForCallout = 4;
+/// Padding to edge of view that an offscreen annotation must have when being brought onscreen (by being selected)
+const NSEdgeInsets MGLMapViewOffscreenAnnotationPadding = NSEdgeInsetsMake(-30.0f, -30.0f, -30.0f, -30.0f);
+
/// Unique identifier representing a single annotation in mbgl.
typedef uint32_t MGLAnnotationTag;
@@ -2099,44 +2102,41 @@ public:
MGLAnnotationTag hitAnnotationTag = MGLAnnotationTagNotFound;
if (nearbyAnnotations.size()) {
- // The annotation tags need to be stable in order to compare them with
- // the remembered tags.
- std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end());
-
+ // The first selection in the cycle should be the one nearest to the
+ // tap. Also the annotation tags need to be stable in order to compare them with
+ // the remembered tags _annotationsNearbyLastClick.
+ CLLocationCoordinate2D currentCoordinate = [self convertPoint:point toCoordinateFromView:self];
+ std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) {
+ CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate];
+ CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate];
+ CLLocationDegrees deltaA = hypot(coordinateA.latitude - currentCoordinate.latitude,
+ coordinateA.longitude - currentCoordinate.longitude);
+ CLLocationDegrees deltaB = hypot(coordinateB.latitude - currentCoordinate.latitude,
+ coordinateB.longitude - currentCoordinate.longitude);
+ return deltaA < deltaB;
+ });
+
if (nearbyAnnotations == _annotationsNearbyLastClick) {
- // The first selection in the cycle should be the one nearest to the
- // click.
- CLLocationCoordinate2D currentCoordinate = [self convertPoint:point toCoordinateFromView:self];
- std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) {
- CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate];
- CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate];
- CLLocationDegrees distanceA = hypot(coordinateA.latitude - currentCoordinate.latitude,
- coordinateA.longitude - currentCoordinate.longitude);
- CLLocationDegrees distanceB = hypot(coordinateB.latitude - currentCoordinate.latitude,
- coordinateB.longitude - currentCoordinate.longitude);
- return distanceA < distanceB;
- });
-
// The last time we persisted a set of annotations, we had the same
// set of annotations as we do now. Cycle through them.
if (_lastSelectedAnnotationTag == MGLAnnotationTagNotFound
- || _lastSelectedAnnotationTag == _annotationsNearbyLastClick.back()) {
+ || _lastSelectedAnnotationTag == nearbyAnnotations.back()) {
// Either no annotation is selected or the last annotation in
// the set was selected. Wrap around to the first annotation in
// the set.
- hitAnnotationTag = _annotationsNearbyLastClick.front();
+ hitAnnotationTag = nearbyAnnotations.front();
} else {
- auto result = std::find(_annotationsNearbyLastClick.begin(),
- _annotationsNearbyLastClick.end(),
+ auto result = std::find(nearbyAnnotations.begin(),
+ nearbyAnnotations.end(),
_lastSelectedAnnotationTag);
- if (result == _annotationsNearbyLastClick.end()) {
+ if (result == nearbyAnnotations.end()) {
// An annotation from this set hasn’t been selected before.
// Select the first (nearest) one.
- hitAnnotationTag = _annotationsNearbyLastClick.front();
+ hitAnnotationTag = nearbyAnnotations.front();
} else {
// Step to the next annotation in the set.
- auto distance = std::distance(_annotationsNearbyLastClick.begin(), result);
- hitAnnotationTag = _annotationsNearbyLastClick[distance + 1];
+ auto distance = std::distance(nearbyAnnotations.begin(), result);
+ hitAnnotationTag = nearbyAnnotations[distance + 1];
}
}
} else {
@@ -2208,10 +2208,12 @@ public:
return;
}
- // Select the annotation if it’s visible.
- if (MGLCoordinateInCoordinateBounds(firstAnnotation.coordinate, self.visibleCoordinateBounds)) {
- [self selectAnnotation:firstAnnotation];
- }
+ [self selectAnnotation:firstAnnotation];
+}
+
+- (BOOL)isBringingAnnotationOnscreenSupportedForAnnotation:(id<MGLAnnotation>)annotation animated:(BOOL)animated {
+ // Consider delegating
+ return animated && [annotation isKindOfClass:[MGLPointAnnotation class]];
}
- (void)selectAnnotation:(id <MGLAnnotation>)annotation
@@ -2221,6 +2223,11 @@ public:
- (void)selectAnnotation:(id <MGLAnnotation>)annotation atPoint:(NSPoint)gesturePoint
{
+ [self selectAnnotation:annotation atPoint:gesturePoint moveOnscreen:YES animateSelection:YES];
+}
+
+- (void)selectAnnotation:(id <MGLAnnotation>)annotation atPoint:(NSPoint)gesturePoint moveOnscreen:(BOOL)moveOnscreen animateSelection:(BOOL)animateSelection
+{
id <MGLAnnotation> selectedAnnotation = self.selectedAnnotation;
if (annotation == selectedAnnotation) {
return;
@@ -2235,9 +2242,14 @@ public:
[self addAnnotation:annotation];
}
+ if (moveOnscreen) {
+ moveOnscreen = [self isBringingAnnotationOnscreenSupportedForAnnotation:annotation animated:animateSelection];
+ }
+
// The annotation's anchor will bounce to the current click.
NSRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag];
- if (NSIsEmptyRect(NSIntersectionRect(positioningRect, self.bounds))) {
+
+ if (!moveOnscreen && NSIsEmptyRect(NSIntersectionRect(positioningRect, self.bounds))) {
positioningRect = CGRectMake(gesturePoint.x, gesturePoint.y, positioningRect.size.width, positioningRect.size.height);
}
@@ -2257,11 +2269,65 @@ public:
// alignment rect, or off the left edge in a right-to-left UI.
callout.delegate = self;
self.calloutForSelectedAnnotation = callout;
+
NSRectEdge edge = (self.userInterfaceLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft
? NSMinXEdge
: NSMaxXEdge);
+
+ // The following will do nothing if the positioning rect is not on-screen. See
+ // `-[MGLMapView updateAnnotationCallouts]` for presenting the callout when the selected
+ // annotation comes back on-screen.
[callout showRelativeToRect:positioningRect ofView:self preferredEdge:edge];
}
+
+ if (moveOnscreen)
+ {
+ moveOnscreen = NO;
+
+ NSRect (^edgeInsetsInsetRect)(NSRect, NSEdgeInsets) = ^(NSRect rect, NSEdgeInsets insets) {
+ return NSMakeRect(rect.origin.x + insets.left,
+ rect.origin.y + insets.top,
+ rect.size.width - insets.left - insets.right,
+ rect.size.height - insets.top - insets.bottom);
+ };
+
+ // Add padding around the positioning rect (in essence an inset from the edge of the viewport
+ NSRect expandedPositioningRect = edgeInsetsInsetRect(positioningRect, MGLMapViewOffscreenAnnotationPadding);
+
+ // Used for callout positioning, and moving offscreen annotations onscreen.
+ CGRect constrainedRect = edgeInsetsInsetRect(self.bounds, self.contentInsets);
+ CGRect bounds = constrainedRect;
+
+ // Any one of these cases should trigger a move onscreen
+ if (CGRectGetMinX(positioningRect) < CGRectGetMinX(bounds))
+ {
+ constrainedRect.origin.x = expandedPositioningRect.origin.x;
+ moveOnscreen = YES;
+ }
+ else if (CGRectGetMaxX(positioningRect) > CGRectGetMaxX(bounds))
+ {
+ constrainedRect.origin.x = CGRectGetMaxX(expandedPositioningRect) - constrainedRect.size.width;
+ moveOnscreen = YES;
+ }
+
+ if (CGRectGetMinY(positioningRect) < CGRectGetMinY(bounds))
+ {
+ constrainedRect.origin.y = expandedPositioningRect.origin.y;
+ moveOnscreen = YES;
+ }
+ else if (CGRectGetMaxY(positioningRect) > CGRectGetMaxY(bounds))
+ {
+ constrainedRect.origin.y = CGRectGetMaxY(expandedPositioningRect) - constrainedRect.size.height;
+ moveOnscreen = YES;
+ }
+
+ if (moveOnscreen)
+ {
+ CGPoint center = CGPointMake(CGRectGetMidX(constrainedRect), CGRectGetMidY(constrainedRect));
+ CLLocationCoordinate2D centerCoord = [self convertPoint:center toCoordinateFromView:self];
+ [self setCenterCoordinate:centerCoord animated:animateSelection];
+ }
+ }
}
- (void)showAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations animated:(BOOL)animated {
@@ -2396,7 +2462,25 @@ public:
- (void)updateAnnotationCallouts {
NSPopover *callout = self.calloutForSelectedAnnotation;
if (callout) {
- callout.positioningRect = [self positioningRectForCalloutForAnnotationWithTag:_selectedAnnotationTag];
+ NSRect rect = [self positioningRectForCalloutForAnnotationWithTag:_selectedAnnotationTag];
+
+ if (!NSIsEmptyRect(NSIntersectionRect(rect, self.bounds))) {
+
+ // It's possible that the current callout hasn't been presented (since the original
+ // positioningRect was offscreen). We can check that the callout has a valid window
+ // This results in the callout being presented just as the annotation comes on screen
+ // which matches MapKit, but (currently) not iOS.
+ if (!callout.contentViewController.view.window) {
+ NSRectEdge edge = (self.userInterfaceLayoutDirection == NSUserInterfaceLayoutDirectionRightToLeft
+ ? NSMinXEdge
+ : NSMaxXEdge);
+ // Re-present the callout
+ [callout showRelativeToRect:rect ofView:self preferredEdge:edge];
+ }
+ else {
+ callout.positioningRect = rect;
+ }
+ }
}
}
@@ -2717,32 +2801,24 @@ public:
/// Converts a rectangle in the given view’s coordinate system to a geographic
/// bounding box.
- (mbgl::LatLngBounds)convertRect:(NSRect)rect toLatLngBoundsFromView:(nullable NSView *)view {
- mbgl::LatLngBounds bounds = mbgl::LatLngBounds::empty();
- bounds.extend([self convertPoint:rect.origin toLatLngFromView:view]);
- bounds.extend([self convertPoint:{ NSMaxX(rect), NSMinY(rect) } toLatLngFromView:view]);
- bounds.extend([self convertPoint:{ NSMaxX(rect), NSMaxY(rect) } toLatLngFromView:view]);
- bounds.extend([self convertPoint:{ NSMinX(rect), NSMaxY(rect) } toLatLngFromView:view]);
-
- // The world is wrapping if a point just outside the bounds is also within
- // the rect.
- mbgl::LatLng outsideLatLng;
- if (bounds.west() > -180) {
- outsideLatLng = {
- (bounds.south() + bounds.north()) / 2,
- bounds.west() - 1,
- };
- } else if (bounds.northeast().longitude() < 180) {
- outsideLatLng = {
- (bounds.south() + bounds.north()) / 2,
- bounds.east() + 1,
- };
- }
-
- // If the world is wrapping, extend the bounds to cover all longitudes.
- if (NSPointInRect([self convertLatLng:outsideLatLng toPointToView:view], rect)) {
- bounds.extend(mbgl::LatLng(bounds.south(), -180));
- bounds.extend(mbgl::LatLng(bounds.south(), 180));
- }
+ auto bounds = mbgl::LatLngBounds::empty();
+ auto bottomLeft = [self convertPoint:{ NSMinX(rect), NSMinY(rect) } toLatLngFromView:view];
+ auto bottomRight = [self convertPoint:{ NSMaxX(rect), NSMinY(rect) } toLatLngFromView:view];
+ auto topRight = [self convertPoint:{ NSMaxX(rect), NSMaxY(rect) } toLatLngFromView:view];
+ auto topLeft = [self convertPoint:{ NSMinX(rect), NSMaxY(rect) } toLatLngFromView:view];
+
+ // If the bounds straddles the antimeridian, unwrap it so that one side
+ // extends beyond ±180° longitude.
+ auto center = [self convertPoint:{ NSMidX(rect), NSMidY(rect) } toLatLngFromView:view];
+ bottomLeft.unwrapForShortestPath(center);
+ bottomRight.unwrapForShortestPath(center);
+ topRight.unwrapForShortestPath(center);
+ topLeft.unwrapForShortestPath(center);
+
+ bounds.extend(bottomLeft);
+ bounds.extend(bottomRight);
+ bounds.extend(topRight);
+ bounds.extend(topLeft);
return bounds;
}
diff --git a/platform/macos/src/Mapbox.h b/platform/macos/src/Mapbox.h
index 0e4b546cf7..fcf41203cf 100644
--- a/platform/macos/src/Mapbox.h
+++ b/platform/macos/src/Mapbox.h
@@ -49,11 +49,10 @@ FOUNDATION_EXPORT MGL_EXPORT const unsigned char MapboxVersionString[];
#import "MGLOpenGLStyleLayer.h"
#import "MGLSource.h"
#import "MGLTileSource.h"
-#import "MGLVectorSource.h"
+#import "MGLVectorTileSource.h"
#import "MGLShapeSource.h"
-#import "MGLAbstractShapeSource.h"
#import "MGLComputedShapeSource.h"
-#import "MGLRasterSource.h"
+#import "MGLRasterTileSource.h"
#import "MGLRasterDEMSource.h"
#import "MGLImageSource.h"
#import "MGLTilePyramidOfflineRegion.h"
diff --git a/platform/macos/src/NSColor+MGLAdditions.h b/platform/macos/src/NSColor+MGLAdditions.h
index 8dd8c1c17c..21c939fec6 100644
--- a/platform/macos/src/NSColor+MGLAdditions.h
+++ b/platform/macos/src/NSColor+MGLAdditions.h
@@ -6,7 +6,7 @@
@interface NSColor (MGLAdditions)
/**
- Converts the color into an mbgl::Color in calibrated RGB space.
+ Converts the color into an mbgl::Color in sRGB space.
*/
- (mbgl::Color)mgl_color;
@@ -18,3 +18,10 @@
- (mbgl::style::PropertyValue<mbgl::Color>)mgl_colorPropertyValue;
@end
+
+@interface NSExpression (MGLColorAdditions)
+
++ (NSExpression *)mgl_expressionForRGBComponents:(NSArray<NSExpression *> *)components;
++ (NSExpression *)mgl_expressionForRGBAComponents:(NSArray<NSExpression *> *)components;
+
+@end
diff --git a/platform/macos/src/NSColor+MGLAdditions.mm b/platform/macos/src/NSColor+MGLAdditions.mm
index 5288f2bc61..8c9086ccf7 100644
--- a/platform/macos/src/NSColor+MGLAdditions.mm
+++ b/platform/macos/src/NSColor+MGLAdditions.mm
@@ -2,24 +2,98 @@
@implementation NSColor (MGLAdditions)
-- (mbgl::Color)mgl_color
-{
+- (mbgl::Color)mgl_color {
CGFloat r, g, b, a;
- [[self colorUsingColorSpaceName:NSCalibratedRGBColorSpace] getRed:&r green:&g blue:&b alpha:&a];
+ // The Mapbox Style Specification does not specify a color space, but it is
+ // assumed to be sRGB for consistency with CSS.
+ NSColor *srgbColor = self;
+ if ([NSColor redColor].colorSpaceName == NSCalibratedRGBColorSpace) {
+ srgbColor = [srgbColor colorUsingColorSpaceName:NSCalibratedRGBColorSpace];
+ } else {
+ srgbColor = [srgbColor colorUsingColorSpace:[NSColorSpace sRGBColorSpace]];
+ }
+ [srgbColor getRed:&r green:&g blue:&b alpha:&a];
return { (float)r, (float)g, (float)b, (float)a };
}
-+ (NSColor *)mgl_colorWithColor:(mbgl::Color)color
-{
- return [NSColor colorWithCalibratedRed:color.r green:color.g blue:color.b alpha:color.a];
++ (NSColor *)mgl_colorWithColor:(mbgl::Color)color {
+ // macOS 10.12 Sierra and below uses calibrated RGB by default.
+ if ([NSColor redColor].colorSpaceName == NSCalibratedRGBColorSpace) {
+ return [NSColor colorWithCalibratedRed:color.r green:color.g blue:color.b alpha:color.a];
+ } else {
+ return [NSColor colorWithRed:color.r green:color.g blue:color.b alpha:color.a];
+ }
}
-- (mbgl::style::PropertyValue<mbgl::Color>)mgl_colorPropertyValue
-{
+- (mbgl::style::PropertyValue<mbgl::Color>)mgl_colorPropertyValue {
mbgl::Color color = self.mgl_color;
return {{ color.r, color.g, color.b, color.a }};
}
@end
+
+@implementation NSExpression (MGLColorAdditions)
+
++ (NSExpression *)mgl_expressionForRGBComponents:(NSArray<NSExpression *> *)components {
+ if (NSColor *color = [self mgl_colorWithComponentExpressions:components]) {
+ return [NSExpression expressionForConstantValue:color];
+ }
+
+ NSExpression *color = [NSExpression expressionForConstantValue:[NSColor class]];
+ NSExpression *alpha = [NSExpression expressionForConstantValue:@1.0];
+ return [NSExpression expressionForFunction:color
+ selectorName:@"colorWithRed:green:blue:alpha:"
+ arguments:[components arrayByAddingObject:alpha]];
+}
+
++ (NSExpression *)mgl_expressionForRGBAComponents:(NSArray<NSExpression *> *)components {
+ if (NSColor *color = [self mgl_colorWithComponentExpressions:components]) {
+ return [NSExpression expressionForConstantValue:color];
+ }
+
+ NSExpression *color = [NSExpression expressionForConstantValue:[NSColor class]];
+ return [NSExpression expressionForFunction:color
+ selectorName:@"colorWithRed:green:blue:alpha:"
+ arguments:components];
+}
+
+/**
+ Returns a color object corresponding to the given component expressions.
+ */
++ (NSColor *)mgl_colorWithComponentExpressions:(NSArray<NSExpression *> *)componentExpressions {
+ // Map the component expressions to constant components. If any component is
+ // a non-constant expression, the components cannot be converted into a
+ // constant color value.
+ std::vector<CGFloat> components;
+ for (NSExpression *componentExpression in componentExpressions) {
+ if (componentExpression.expressionType != NSConstantValueExpressionType) {
+ return nil;
+ }
+
+ NSNumber *component = (NSNumber *)componentExpression.constantValue;
+ if (![component isKindOfClass:[NSNumber class]]) {
+ return nil;
+ }
+
+ components.push_back(component.doubleValue / 255.0);
+ }
+ // Alpha
+ components.back() *= 255.0;
+
+ // macOS 10.12 Sierra and below uses calibrated RGB by default.
+ if ([NSColor redColor].colorSpaceName == NSCalibratedRGBColorSpace) {
+ return [NSColor colorWithCalibratedRed:components[0]
+ green:components[1]
+ blue:components[2]
+ alpha:components[3]];
+ }
+ // The Mapbox Style Specification does not specify a color space, but it is
+ // assumed to be sRGB for consistency with CSS.
+ return [NSColor colorWithColorSpace:[NSColorSpace sRGBColorSpace]
+ components:&components[0]
+ count:components.size()];
+}
+
+@end