From 6ad47cfbad038ee8330fece5da7438d084e67a68 Mon Sep 17 00:00:00 2001 From: Jesse Bounds Date: Wed, 11 Jan 2017 16:05:38 -0800 Subject: [PATCH] [ios, macos] Add convenience initializers to shape source (#7665) This adds two new convenience initializers to MGLShapeSource: -initWithIdentifier:features:options: takes an array of shape objects that conform to MGLFeature, inserts them in a shape collection feature and creates a source with that shape. -initWithIdentifier:shapes:options does the same but with concrete MGLShape objects that get added to a shape collection. Throw an exception if an shape source is created with the features initializer but is sent an array of features that contains something that is not actually an object that conforms to the feature protocol. Updates to geojson data guide Qualify APIs that take arrays of shapes that are features --- .../docs/guides/Working with GeoJSON Data.md | 10 ++++ platform/darwin/src/MGLFeature.mm | 2 +- platform/darwin/src/MGLShapeSource.h | 46 +++++++++++++++++-- platform/darwin/src/MGLShapeSource.mm | 15 ++++++ .../test/MGLDocumentationExampleTests.swift | 3 +- platform/darwin/test/MGLShapeSourceTests.mm | 39 ++++++++++++++++ platform/ios/app/MBXViewController.m | 3 +- 7 files changed, 110 insertions(+), 8 deletions(-) diff --git a/platform/darwin/docs/guides/Working with GeoJSON Data.md b/platform/darwin/docs/guides/Working with GeoJSON Data.md index 0fffed06ddb..adaf053e418 100644 --- a/platform/darwin/docs/guides/Working with GeoJSON Data.md +++ b/platform/darwin/docs/guides/Working with GeoJSON Data.md @@ -36,6 +36,16 @@ resulting shape or feature object into the the map, or you can use the object and its properties to power non-map-related functionality in your application. +To include multiple shapes in the source, create and pass an `MGLShapeCollection` or + `MGLShapeCollectionFeature` object to + `-[MGLShapeSource initWithIdentifier:shape:options:]`. Alternatively, use the + `-[MGLShapeSource initWithIdentifier:features:options:]` or + `-[MGLShapeSource initWithIdentifier:shapes:options:]` method to create a shape source + with an array. `-[MGLShapeSource initWithIdentifier:features:options:]` accepts only `MGLFeature` + instances, such as `MGLPointFeature` objects, whose attributes you can use when + applying a predicate to `MGLVectorStyleLayer` or configuring a style layer’s + appearance. + ## Extracting GeoJSON data from the map Any `MGLShape`, `MGLFeature`, or `MGLShapeCollectionFeature` object has an diff --git a/platform/darwin/src/MGLFeature.mm b/platform/darwin/src/MGLFeature.mm index 5b63beae872..3bd7eae535a 100644 --- a/platform/darwin/src/MGLFeature.mm +++ b/platform/darwin/src/MGLFeature.mm @@ -184,7 +184,7 @@ @implementation MGLShapeCollectionFeature @dynamic shapes; -+ (instancetype)shapeCollectionWithShapes:(NSArray *)shapes { ++ (instancetype)shapeCollectionWithShapes:(NS_ARRAY_OF(MGLShape *) *)shapes { return [super shapeCollectionWithShapes:shapes]; } diff --git a/platform/darwin/src/MGLShapeSource.h b/platform/darwin/src/MGLShapeSource.h index 8c8a2ab4e58..7d8a6459594 100644 --- a/platform/darwin/src/MGLShapeSource.h +++ b/platform/darwin/src/MGLShapeSource.h @@ -103,8 +103,7 @@ extern const MGLShapeSourceOption MGLShapeSourceOptionSimplificationTolerance; CLLocationCoordinate2D(latitude: 38.91, longitude: -77.04), ] let polyline = MGLPolylineFeature(coordinates: &coordinates, count: UInt(coordinates.count)) - let shape = MGLShapeCollectionFeature(shapes: [polyline]) - let source = MGLShapeSource(identifier: "lines", shape: shape, options: nil) + let source = MGLShapeSource(identifier: "lines", features: [polyline], options: nil) mapView.style?.addSource(source) ``` */ @@ -131,7 +130,9 @@ extern const MGLShapeSourceOption MGLShapeSourceOptionSimplificationTolerance; To specify attributes about the shape, use an instance of an `MGLShape` subclass that conforms to the `MGLFeature` protocol, such as `MGLPointFeature`. To include multiple shapes in the source, use an `MGLShapeCollection` or - `MGLShapeCollectionFeature` object. + `MGLShapeCollectionFeature` object, or use the + `-initWithIdentifier:features:options:` or + `-initWithIdentifier:shapes:options:` methods. To create a shape from GeoJSON source code, use the `+[MGLShape shapeWithData:encoding:error:]` method. @@ -143,6 +144,45 @@ extern const MGLShapeSourceOption MGLShapeSourceOptionSimplificationTolerance; */ - (instancetype)initWithIdentifier:(NSString *)identifier shape:(nullable MGLShape *)shape options:(nullable NS_DICTIONARY_OF(MGLShapeSourceOption, id) *)options NS_DESIGNATED_INITIALIZER; +/** + Returns a shape source with an identifier, an array of features, and a dictionary + of options for the source. + + Unlike `-initWithIdentifier:shapes:options:`, this method accepts `MGLFeature` + instances, such as `MGLPointFeature` objects, whose attributes you can use when + applying a predicate to `MGLVectorStyleLayer` or configuring a style layer’s + appearance. + + To create a shape from GeoJSON source code, use the + `+[MGLShape shapeWithData:encoding:error:]` method. + + @param identifier A string that uniquely identifies the source. + @param features An array of objects that conform to the MGLFeature protocol. + @param options An `NSDictionary` of options for this source. + @return An initialized shape source. + */ +- (instancetype)initWithIdentifier:(NSString *)identifier features:(NS_ARRAY_OF(MGLShape *) *)features options:(nullable NS_DICTIONARY_OF(MGLShapeSourceOption, id) *)options; + +/** + Returns a shape source with an identifier, an array of shapes, and a dictionary of + options for the source. + + Any `MGLFeature` instance passed into this initializer is treated as an ordinary + shape, causing any attributes to be inaccessible to an `MGLVectorStyleLayer` when + evaluating a predicate or configuring certain layout or paint attributes. To + preserve the attributes associated with each feature, use the + `-initWithIdentifier:features:options:` method instead. + + To create a shape from GeoJSON source code, use the + `+[MGLShape shapeWithData:encoding:error:]` method. + + @param identifier A string that uniquely identifies the source. + @param shapes An array of shapes; each shape is a member of a concrete subclass of MGLShape. + @param options An `NSDictionary` of options for this source. + @return An initialized shape source. + */ +- (instancetype)initWithIdentifier:(NSString *)identifier shapes:(NS_ARRAY_OF(MGLShape *) *)shapes options:(nullable NS_DICTIONARY_OF(MGLShapeSourceOption, id) *)options; + #pragma mark Accessing a Source’s Content /** diff --git a/platform/darwin/src/MGLShapeSource.mm b/platform/darwin/src/MGLShapeSource.mm index 0647236e809..50322f300c7 100644 --- a/platform/darwin/src/MGLShapeSource.mm +++ b/platform/darwin/src/MGLShapeSource.mm @@ -56,6 +56,21 @@ - (instancetype)initWithIdentifier:(NSString *)identifier shape:(nullable MGLSha return self; } +- (instancetype)initWithIdentifier:(NSString *)identifier features:(NS_ARRAY_OF(MGLShape *) *)features options:(nullable NS_DICTIONARY_OF(MGLShapeSourceOption, id) *)options { + for (id feature in features) { + if (![feature conformsToProtocol:@protocol(MGLFeature)]) { + [NSException raise:NSInvalidArgumentException format:@"The object %@ included in the features argument does not conform to the MGLFeature protocol.", feature]; + } + } + MGLShapeCollectionFeature *shapeCollectionFeature = [MGLShapeCollectionFeature shapeCollectionWithShapes:features]; + return [self initWithIdentifier:identifier shape:shapeCollectionFeature options:options]; +} + +- (instancetype)initWithIdentifier:(NSString *)identifier shapes:(NS_ARRAY_OF(MGLShape *) *)shapes options:(nullable NS_DICTIONARY_OF(MGLShapeSourceOption, id) *)options { + MGLShapeCollection *shapeCollection = [MGLShapeCollection shapeCollectionWithShapes:shapes]; + return [self initWithIdentifier:identifier shape:shapeCollection options:options]; +} + - (instancetype)initWithRawSource:(mbgl::style::GeoJSONSource *)rawSource { return [super initWithRawSource:rawSource]; } diff --git a/platform/darwin/test/MGLDocumentationExampleTests.swift b/platform/darwin/test/MGLDocumentationExampleTests.swift index 5f0e83b2cac..d796b4e7087 100644 --- a/platform/darwin/test/MGLDocumentationExampleTests.swift +++ b/platform/darwin/test/MGLDocumentationExampleTests.swift @@ -66,8 +66,7 @@ class MGLDocumentationExampleTests: XCTestCase, MGLMapViewDelegate { CLLocationCoordinate2D(latitude: 38.91, longitude: -77.04), ] let polyline = MGLPolylineFeature(coordinates: &coordinates, count: UInt(coordinates.count)) - let shape = MGLShapeCollectionFeature(shapes: [polyline]) - let source = MGLShapeSource(identifier: "lines", shape: shape, options: nil) + let source = MGLShapeSource(identifier: "lines", features: [polyline], options: nil) mapView.style?.addSource(source) //#-end-example-code diff --git a/platform/darwin/test/MGLShapeSourceTests.mm b/platform/darwin/test/MGLShapeSourceTests.mm index 2662d4b6f08..cf32b5c821a 100644 --- a/platform/darwin/test/MGLShapeSourceTests.mm +++ b/platform/darwin/test/MGLShapeSourceTests.mm @@ -278,4 +278,43 @@ - (void)testMGLShapeSourceWithShapeCollectionFeatures { XCTAssert(shape.shapes.count == 6, @"Shape collection should contain 6 shapes"); } +- (void)testMGLShapeSourceWithFeaturesConvenienceInitializer { + CLLocationCoordinate2D coordinates[] = { + CLLocationCoordinate2DMake(100.0, 0.0), + CLLocationCoordinate2DMake(101.0, 0.0), + CLLocationCoordinate2DMake(101.0, 1.0), + CLLocationCoordinate2DMake(100.0, 1.0), + CLLocationCoordinate2DMake(100.0, 0.0)}; + + MGLPolygonFeature *polygonFeature = [MGLPolygonFeature polygonWithCoordinates:coordinates count:sizeof(coordinates)/sizeof(coordinates[0]) interiorPolygons:nil]; + + MGLShapeSource *source = [[MGLShapeSource alloc] initWithIdentifier:@"source-id" features:@[polygonFeature] options:nil]; + MGLShapeCollectionFeature *shape = (MGLShapeCollectionFeature *)source.shape; + + XCTAssertTrue([shape isKindOfClass:[MGLShapeCollectionFeature class]]); + XCTAssertEqual(shape.shapes.count, 1, @"Shape collection should contain 1 shape"); + + // when a shape is included in the features array + MGLPolygon *polygon = [MGLPolygon polygonWithCoordinates:coordinates count:sizeof(coordinates)/sizeof(coordinates[0]) interiorPolygons:nil]; + + XCTAssertThrowsSpecificNamed([[MGLShapeSource alloc] initWithIdentifier:@"source-id-invalid" features:@[polygon] options:nil], NSException, NSInvalidArgumentException, @"Shape source should raise an exception if a shape is sent to the features initializer"); +} + +- (void)testMGLShapeSourceWithShapesConvenienceInitializer { + CLLocationCoordinate2D coordinates[] = { + CLLocationCoordinate2DMake(100.0, 0.0), + CLLocationCoordinate2DMake(101.0, 0.0), + CLLocationCoordinate2DMake(101.0, 1.0), + CLLocationCoordinate2DMake(100.0, 1.0), + CLLocationCoordinate2DMake(100.0, 0.0)}; + + MGLPolygon *polygon = [MGLPolygon polygonWithCoordinates:coordinates count:sizeof(coordinates)/sizeof(coordinates[0]) interiorPolygons:nil]; + + MGLShapeSource *source = [[MGLShapeSource alloc] initWithIdentifier:@"source-id" shapes:@[polygon] options:nil]; + MGLShapeCollectionFeature *shape = (MGLShapeCollectionFeature *)source.shape; + + XCTAssertTrue([shape isKindOfClass:[MGLShapeCollection class]]); + XCTAssertEqual(shape.shapes.count, 1, @"Shape collection should contain 1 shape"); +} + @end diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m index 13401997a0c..ff69581dacc 100644 --- a/platform/ios/app/MBXViewController.m +++ b/platform/ios/app/MBXViewController.m @@ -919,8 +919,7 @@ - (void)styleQuery } dispatch_async(dispatch_get_main_queue(), ^{ - MGLShapeCollectionFeature *features = [MGLShapeCollectionFeature shapeCollectionWithShapes:visibleFeatures]; - MGLShapeSource *source = [[MGLShapeSource alloc] initWithIdentifier:querySourceID shape:features options:nil]; + MGLShapeSource *source = [[MGLShapeSource alloc] initWithIdentifier:querySourceID features:visibleFeatures options:nil]; [self.mapView.style addSource:source]; MGLFillStyleLayer *fillLayer = [[MGLFillStyleLayer alloc] initWithIdentifier:queryLayerID source:source];