535 lines
18 KiB
Plaintext
535 lines
18 KiB
Plaintext
#import "MGLFoundation_Private.h"
|
|
#import "MGLFeature_Private.h"
|
|
#import "MGLCluster.h"
|
|
|
|
#import "MGLPointAnnotation.h"
|
|
#import "MGLPolyline.h"
|
|
#import "MGLPolygon.h"
|
|
#import "MGLValueEvaluator.h"
|
|
|
|
#import "MGLShape_Private.h"
|
|
#import "MGLPointCollection_Private.h"
|
|
#import "MGLPolyline_Private.h"
|
|
#import "MGLPolygon_Private.h"
|
|
|
|
#import "NSDictionary+MGLAdditions.h"
|
|
#import "NSArray+MGLAdditions.h"
|
|
#import "NSExpression+MGLPrivateAdditions.h"
|
|
#import "MGLLoggingConfiguration_Private.h"
|
|
|
|
#import <mbgl/util/geometry.hpp>
|
|
#import <mbgl/style/conversion/geojson.hpp>
|
|
#import <mapbox/feature.hpp>
|
|
|
|
// Cluster constants
|
|
static NSString * const MGLClusterIdentifierKey = @"cluster_id";
|
|
static NSString * const MGLClusterCountKey = @"point_count";
|
|
const NSUInteger MGLClusterIdentifierInvalid = NSUIntegerMax;
|
|
|
|
@interface MGLEmptyFeature ()
|
|
@end
|
|
|
|
@implementation MGLEmptyFeature
|
|
|
|
@synthesize identifier;
|
|
@synthesize attributes = _attributes;
|
|
|
|
MGL_DEFINE_FEATURE_INIT_WITH_CODER();
|
|
MGL_DEFINE_FEATURE_ENCODE();
|
|
MGL_DEFINE_FEATURE_IS_EQUAL();
|
|
MGL_DEFINE_FEATURE_ATTRIBUTES_GETTER();
|
|
|
|
- (id)attributeForKey:(NSString *)key {
|
|
MGLLogDebug(@"Retrieving attributeForKey: %@", key);
|
|
return self.attributes[key];
|
|
}
|
|
|
|
- (NSDictionary *)geoJSONDictionary {
|
|
return NSDictionaryFeatureForGeometry([super geoJSONDictionary], self.attributes, self.identifier);
|
|
}
|
|
|
|
- (mbgl::GeoJSON)geoJSONObject {
|
|
return mbglFeature({[self geometryObject]}, identifier, self.attributes);
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
return [NSString stringWithFormat:@"<%@: %p; identifier = %@, attributes = %@>",
|
|
NSStringFromClass([self class]), (void *)self,
|
|
self.identifier ? [NSString stringWithFormat:@"\"%@\"", self.identifier] : self.identifier,
|
|
self.attributes.count ? self.attributes : @"none"];
|
|
}
|
|
|
|
@end
|
|
|
|
@interface MGLPointFeature ()
|
|
@end
|
|
|
|
@implementation MGLPointFeature
|
|
|
|
@synthesize identifier;
|
|
@synthesize attributes = _attributes;
|
|
|
|
MGL_DEFINE_FEATURE_INIT_WITH_CODER();
|
|
MGL_DEFINE_FEATURE_ENCODE();
|
|
MGL_DEFINE_FEATURE_IS_EQUAL();
|
|
MGL_DEFINE_FEATURE_ATTRIBUTES_GETTER();
|
|
|
|
- (id)attributeForKey:(NSString *)key {
|
|
MGLLogDebug(@"Retrieving attributeForKey: %@", key);
|
|
return self.attributes[key];
|
|
}
|
|
|
|
- (NSDictionary *)geoJSONDictionary {
|
|
return NSDictionaryFeatureForGeometry([super geoJSONDictionary], self.attributes, self.identifier);
|
|
}
|
|
|
|
- (mbgl::GeoJSON)geoJSONObject {
|
|
return mbglFeature({[self geometryObject]}, identifier, self.attributes);
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
return [NSString stringWithFormat:@"<%@: %p; identifier = %@, coordinate = %f, %f, attributes = %@>",
|
|
NSStringFromClass([self class]), (void *)self,
|
|
self.identifier ? [NSString stringWithFormat:@"\"%@\"", self.identifier] : self.identifier,
|
|
self.coordinate.latitude, self.coordinate.longitude,
|
|
self.attributes.count ? self.attributes : @"none"];
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation MGLPointFeatureCluster
|
|
|
|
- (NSUInteger)clusterIdentifier {
|
|
NSNumber *clusterNumber = MGL_OBJC_DYNAMIC_CAST([self attributeForKey:MGLClusterIdentifierKey], NSNumber);
|
|
MGLAssert(clusterNumber, @"Clusters should have a cluster_id");
|
|
|
|
if (!clusterNumber) {
|
|
return MGLClusterIdentifierInvalid;
|
|
}
|
|
|
|
NSUInteger clusterIdentifier = [clusterNumber unsignedIntegerValue];
|
|
MGLAssert(clusterIdentifier <= UINT32_MAX, @"Cluster identifiers are 32bit");
|
|
|
|
return clusterIdentifier;
|
|
}
|
|
|
|
- (NSUInteger)clusterPointCount {
|
|
NSNumber *count = MGL_OBJC_DYNAMIC_CAST([self attributeForKey:MGLClusterCountKey], NSNumber);
|
|
MGLAssert(count, @"Clusters should have a point_count");
|
|
|
|
return [count unsignedIntegerValue];
|
|
}
|
|
@end
|
|
|
|
|
|
@interface MGLPolylineFeature ()
|
|
@end
|
|
|
|
@implementation MGLPolylineFeature
|
|
|
|
@synthesize identifier;
|
|
@synthesize attributes = _attributes;
|
|
|
|
MGL_DEFINE_FEATURE_INIT_WITH_CODER();
|
|
MGL_DEFINE_FEATURE_ENCODE();
|
|
MGL_DEFINE_FEATURE_IS_EQUAL();
|
|
MGL_DEFINE_FEATURE_ATTRIBUTES_GETTER();
|
|
|
|
- (id)attributeForKey:(NSString *)key {
|
|
MGLLogDebug(@"Retrieving attributeForKey: %@", key);
|
|
return self.attributes[key];
|
|
}
|
|
|
|
- (NSDictionary *)geoJSONDictionary {
|
|
return NSDictionaryFeatureForGeometry([super geoJSONDictionary], self.attributes, self.identifier);
|
|
}
|
|
|
|
- (mbgl::GeoJSON)geoJSONObject {
|
|
return mbglFeature({[self geometryObject]}, identifier, self.attributes);
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
return [NSString stringWithFormat:@"<%@: %p; identifier = %@, count = %lu, bounds = %@, attributes = %@>",
|
|
NSStringFromClass([self class]), (void *)self,
|
|
self.identifier ? [NSString stringWithFormat:@"\"%@\"", self.identifier] : self.identifier,
|
|
(unsigned long)[self pointCount],
|
|
MGLStringFromCoordinateBounds(self.overlayBounds),
|
|
self.attributes.count ? self.attributes : @"none"];
|
|
}
|
|
|
|
@end
|
|
|
|
@interface MGLPolygonFeature ()
|
|
@end
|
|
|
|
@implementation MGLPolygonFeature
|
|
|
|
@synthesize identifier;
|
|
@synthesize attributes = _attributes;
|
|
|
|
MGL_DEFINE_FEATURE_INIT_WITH_CODER();
|
|
MGL_DEFINE_FEATURE_ENCODE();
|
|
MGL_DEFINE_FEATURE_IS_EQUAL();
|
|
MGL_DEFINE_FEATURE_ATTRIBUTES_GETTER();
|
|
|
|
- (id)attributeForKey:(NSString *)key {
|
|
MGLLogDebug(@"Retrieving attributeForKey: %@", key);
|
|
return self.attributes[key];
|
|
}
|
|
|
|
- (NSDictionary *)geoJSONDictionary {
|
|
return NSDictionaryFeatureForGeometry([super geoJSONDictionary], self.attributes, self.identifier);
|
|
}
|
|
|
|
- (mbgl::GeoJSON)geoJSONObject {
|
|
return mbglFeature({[self geometryObject]}, identifier, self.attributes);
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
return [NSString stringWithFormat:@"<%@: %p; identifier = %@, count = %lu, bounds = %@, attributes = %@>",
|
|
NSStringFromClass([self class]), (void *)self,
|
|
self.identifier ? [NSString stringWithFormat:@"\"%@\"", self.identifier] : self.identifier,
|
|
(unsigned long)[self pointCount],
|
|
MGLStringFromCoordinateBounds(self.overlayBounds),
|
|
self.attributes.count ? self.attributes : @"none"];
|
|
}
|
|
|
|
@end
|
|
|
|
@interface MGLPointCollectionFeature ()
|
|
@end
|
|
|
|
@implementation MGLPointCollectionFeature
|
|
|
|
@synthesize identifier;
|
|
@synthesize attributes = _attributes;
|
|
|
|
MGL_DEFINE_FEATURE_INIT_WITH_CODER();
|
|
MGL_DEFINE_FEATURE_ENCODE();
|
|
MGL_DEFINE_FEATURE_IS_EQUAL();
|
|
MGL_DEFINE_FEATURE_ATTRIBUTES_GETTER();
|
|
|
|
- (id)attributeForKey:(NSString *)key {
|
|
MGLLogDebug(@"Retrieving attributeForKey: %@", key);
|
|
return self.attributes[key];
|
|
}
|
|
|
|
- (NSDictionary *)geoJSONDictionary {
|
|
return NSDictionaryFeatureForGeometry([super geoJSONDictionary], self.attributes, self.identifier);
|
|
}
|
|
|
|
- (mbgl::GeoJSON)geoJSONObject {
|
|
return mbglFeature({[self geometryObject]}, identifier, self.attributes);
|
|
}
|
|
|
|
@end
|
|
|
|
@interface MGLMultiPolylineFeature ()
|
|
@end
|
|
|
|
@implementation MGLMultiPolylineFeature
|
|
|
|
@synthesize identifier;
|
|
@synthesize attributes = _attributes;
|
|
|
|
MGL_DEFINE_FEATURE_INIT_WITH_CODER();
|
|
MGL_DEFINE_FEATURE_ENCODE();
|
|
MGL_DEFINE_FEATURE_IS_EQUAL();
|
|
MGL_DEFINE_FEATURE_ATTRIBUTES_GETTER();
|
|
|
|
- (id)attributeForKey:(NSString *)key {
|
|
MGLLogDebug(@"Retrieving attributeForKey: %@", key);
|
|
return self.attributes[key];
|
|
}
|
|
|
|
- (NSDictionary *)geoJSONDictionary {
|
|
return NSDictionaryFeatureForGeometry([super geoJSONDictionary], self.attributes, self.identifier);
|
|
}
|
|
|
|
- (mbgl::GeoJSON)geoJSONObject {
|
|
return mbglFeature({[self geometryObject]}, identifier, self.attributes);
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
return [NSString stringWithFormat:@"<%@: %p; identifier = %@, count = %lu, bounds = %@, attributes = %@>",
|
|
NSStringFromClass([self class]), (void *)self,
|
|
self.identifier ? [NSString stringWithFormat:@"\"%@\"", self.identifier] : self.identifier,
|
|
(unsigned long)self.polylines.count,
|
|
MGLStringFromCoordinateBounds(self.overlayBounds),
|
|
self.attributes.count ? self.attributes : @"none"];
|
|
}
|
|
|
|
@end
|
|
|
|
@interface MGLMultiPolygonFeature ()
|
|
@end
|
|
|
|
@implementation MGLMultiPolygonFeature
|
|
|
|
@synthesize identifier;
|
|
@synthesize attributes = _attributes;
|
|
|
|
MGL_DEFINE_FEATURE_INIT_WITH_CODER();
|
|
MGL_DEFINE_FEATURE_ENCODE();
|
|
MGL_DEFINE_FEATURE_IS_EQUAL();
|
|
MGL_DEFINE_FEATURE_ATTRIBUTES_GETTER();
|
|
|
|
- (id)attributeForKey:(NSString *)key {
|
|
MGLLogDebug(@"Retrieving attributeForKey: %@", key);
|
|
return self.attributes[key];
|
|
}
|
|
|
|
- (NSDictionary *)geoJSONDictionary {
|
|
return NSDictionaryFeatureForGeometry([super geoJSONDictionary], self.attributes, self.identifier);
|
|
}
|
|
|
|
- (mbgl::GeoJSON)geoJSONObject {
|
|
return mbglFeature({[self geometryObject]}, identifier, self.attributes);
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
return [NSString stringWithFormat:@"<%@: %p; identifier = %@, count = %lu, bounds = %@, attributes = %@>",
|
|
NSStringFromClass([self class]), (void *)self,
|
|
self.identifier ? [NSString stringWithFormat:@"\"%@\"", self.identifier] : self.identifier,
|
|
(unsigned long)self.polygons.count,
|
|
MGLStringFromCoordinateBounds(self.overlayBounds),
|
|
self.attributes.count ? self.attributes : @"none"];
|
|
}
|
|
|
|
@end
|
|
|
|
@interface MGLShapeCollectionFeature ()
|
|
@end
|
|
|
|
@implementation MGLShapeCollectionFeature
|
|
|
|
@synthesize identifier;
|
|
@synthesize attributes = _attributes;
|
|
|
|
@dynamic shapes;
|
|
|
|
+ (instancetype)shapeCollectionWithShapes:(NSArray<MGLShape<MGLFeature> *> *)shapes {
|
|
return [super shapeCollectionWithShapes:shapes];
|
|
}
|
|
|
|
MGL_DEFINE_FEATURE_INIT_WITH_CODER();
|
|
MGL_DEFINE_FEATURE_ENCODE();
|
|
MGL_DEFINE_FEATURE_IS_EQUAL();
|
|
MGL_DEFINE_FEATURE_ATTRIBUTES_GETTER();
|
|
|
|
- (id)attributeForKey:(NSString *)key {
|
|
return self.attributes[key];
|
|
}
|
|
|
|
- (NSDictionary *)geoJSONDictionary {
|
|
return NSDictionaryFeatureForGeometry([super geoJSONDictionary], self.attributes, self.identifier);
|
|
}
|
|
|
|
- (mbgl::GeoJSON)geoJSONObject {
|
|
mbgl::FeatureCollection featureCollection;
|
|
featureCollection.reserve(self.shapes.count);
|
|
for (MGLShape <MGLFeature> *feature in self.shapes) {
|
|
auto geoJSONObject = feature.geoJSONObject;
|
|
MGLAssert(geoJSONObject.is<mbgl::Feature>(), @"Feature collection must only contain features.");
|
|
featureCollection.push_back(geoJSONObject.get<mbgl::Feature>());
|
|
}
|
|
return featureCollection;
|
|
}
|
|
|
|
@end
|
|
|
|
/**
|
|
Transforms an `mbgl::geometry::geometry` type into an instance of the
|
|
corresponding Objective-C geometry class.
|
|
*/
|
|
template <typename T>
|
|
class GeometryEvaluator {
|
|
private:
|
|
const mbgl::PropertyMap *shared_properties;
|
|
|
|
public:
|
|
GeometryEvaluator(const mbgl::PropertyMap *properties = nullptr):
|
|
shared_properties(properties)
|
|
{}
|
|
|
|
MGLShape <MGLFeature> * operator()(const mbgl::EmptyGeometry &) const {
|
|
MGLEmptyFeature *feature = [[MGLEmptyFeature alloc] init];
|
|
return feature;
|
|
}
|
|
|
|
MGLShape <MGLFeature> * operator()(const mbgl::Point<T> &geometry) const {
|
|
Class pointFeatureClass = [MGLPointFeature class];
|
|
|
|
// If we're dealing with a cluster, we should change the class type.
|
|
// This could be generic and build the subclass at runtime if it turns
|
|
// out we need to support more than point clusters.
|
|
if (shared_properties) {
|
|
auto clusterIt = shared_properties->find("cluster");
|
|
if (clusterIt != shared_properties->end()) {
|
|
auto clusterValue = clusterIt->second;
|
|
if (clusterValue.template is<bool>()) {
|
|
if (clusterValue.template get<bool>()) {
|
|
pointFeatureClass = [MGLPointFeatureCluster class];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MGLPointFeature *feature = [[pointFeatureClass alloc] init];
|
|
feature.coordinate = toLocationCoordinate2D(geometry);
|
|
return feature;
|
|
}
|
|
|
|
MGLShape <MGLFeature> * operator()(const mbgl::LineString<T> &geometry) const {
|
|
std::vector<CLLocationCoordinate2D> coordinates = toLocationCoordinates2D(geometry);
|
|
return [MGLPolylineFeature polylineWithCoordinates:&coordinates[0] count:coordinates.size()];
|
|
}
|
|
|
|
MGLShape <MGLFeature> * operator()(const mbgl::Polygon<T> &geometry) const {
|
|
return toShape<MGLPolygonFeature>(geometry);
|
|
}
|
|
|
|
MGLShape <MGLFeature> * operator()(const mbgl::MultiPoint<T> &geometry) const {
|
|
std::vector<CLLocationCoordinate2D> coordinates = toLocationCoordinates2D(geometry);
|
|
return [[MGLPointCollectionFeature alloc] initWithCoordinates:&coordinates[0] count:coordinates.size()];
|
|
}
|
|
|
|
MGLShape <MGLFeature> * operator()(const mbgl::MultiLineString<T> &geometry) const {
|
|
NSMutableArray *polylines = [NSMutableArray arrayWithCapacity:geometry.size()];
|
|
for (auto &lineString : geometry) {
|
|
std::vector<CLLocationCoordinate2D> coordinates = toLocationCoordinates2D(lineString);
|
|
MGLPolyline *polyline = [MGLPolyline polylineWithCoordinates:&coordinates[0] count:coordinates.size()];
|
|
[polylines addObject:polyline];
|
|
}
|
|
|
|
return [MGLMultiPolylineFeature multiPolylineWithPolylines:polylines];
|
|
}
|
|
|
|
MGLShape <MGLFeature> * operator()(const mbgl::MultiPolygon<T> &geometry) const {
|
|
NSMutableArray *polygons = [NSMutableArray arrayWithCapacity:geometry.size()];
|
|
for (auto &polygon : geometry) {
|
|
[polygons addObject:toShape(polygon)];
|
|
}
|
|
|
|
return [MGLMultiPolygonFeature multiPolygonWithPolygons:polygons];
|
|
}
|
|
|
|
MGLShape <MGLFeature> * operator()(const mapbox::geometry::geometry_collection<T> &collection) const {
|
|
NSMutableArray *shapes = [NSMutableArray arrayWithCapacity:collection.size()];
|
|
for (auto &geometry : collection) {
|
|
// This is very much like the transformation that happens in MGLFeaturesFromMBGLFeatures(), but these are raw geometries with no associated feature IDs or attributes.
|
|
MGLShape <MGLFeature> *shape = mapbox::geometry::geometry<T>::visit(geometry, *this);
|
|
[shapes addObject:shape];
|
|
}
|
|
return [MGLShapeCollectionFeature shapeCollectionWithShapes:shapes];
|
|
}
|
|
|
|
private:
|
|
static CLLocationCoordinate2D toLocationCoordinate2D(const mbgl::Point<T> &point) {
|
|
return CLLocationCoordinate2DMake(point.y, point.x);
|
|
}
|
|
|
|
static std::vector<CLLocationCoordinate2D> toLocationCoordinates2D(const std::vector<mbgl::Point<T>> &points) {
|
|
std::vector<CLLocationCoordinate2D> coordinates;
|
|
coordinates.reserve(points.size());
|
|
std::transform(points.begin(), points.end(), std::back_inserter(coordinates), toLocationCoordinate2D);
|
|
return coordinates;
|
|
}
|
|
|
|
template<typename U = MGLPolygon>
|
|
static U *toShape(const mbgl::Polygon<T> &geometry) {
|
|
auto &linearRing = geometry.front();
|
|
std::vector<CLLocationCoordinate2D> coordinates = toLocationCoordinates2D(linearRing);
|
|
NSMutableArray *innerPolygons;
|
|
if (geometry.size() > 1) {
|
|
innerPolygons = [NSMutableArray arrayWithCapacity:geometry.size() - 1];
|
|
for (auto iter = geometry.begin() + 1; iter != geometry.end(); iter++) {
|
|
auto &innerRing = *iter;
|
|
std::vector<CLLocationCoordinate2D> innerCoordinates = toLocationCoordinates2D(innerRing);
|
|
MGLPolygon *innerPolygon = [MGLPolygon polygonWithCoordinates:&innerCoordinates[0] count:innerCoordinates.size()];
|
|
[innerPolygons addObject:innerPolygon];
|
|
}
|
|
}
|
|
|
|
return [U polygonWithCoordinates:&coordinates[0] count:coordinates.size() interiorPolygons:innerPolygons];
|
|
}
|
|
};
|
|
|
|
template <typename T>
|
|
class GeoJSONEvaluator {
|
|
public:
|
|
MGLShape <MGLFeature> * operator()(const mbgl::Geometry<T> &geometry) const {
|
|
GeometryEvaluator<T> evaluator;
|
|
MGLShape <MGLFeature> *shape = mapbox::geometry::geometry<T>::visit(geometry, evaluator);
|
|
return shape;
|
|
}
|
|
|
|
MGLShape <MGLFeature> * operator()(const mbgl::Feature &feature) const {
|
|
MGLShape <MGLFeature> *shape = (MGLShape <MGLFeature> *)MGLFeatureFromMBGLFeature(feature);
|
|
return shape;
|
|
}
|
|
|
|
MGLShape <MGLFeature> * operator()(const mbgl::FeatureCollection &collection) const {
|
|
NSMutableArray *shapes = [NSMutableArray arrayWithCapacity:collection.size()];
|
|
for (const auto &feature : collection) {
|
|
[shapes addObject:MGLFeatureFromMBGLFeature(feature)];
|
|
}
|
|
return [MGLShapeCollectionFeature shapeCollectionWithShapes:shapes];
|
|
}
|
|
};
|
|
|
|
NSArray<MGLShape <MGLFeature> *> *MGLFeaturesFromMBGLFeatures(const std::vector<mbgl::Feature> &features) {
|
|
NSMutableArray *shapes = [NSMutableArray arrayWithCapacity:features.size()];
|
|
for (const auto &feature : features) {
|
|
[shapes addObject:MGLFeatureFromMBGLFeature(feature)];
|
|
}
|
|
return shapes;
|
|
}
|
|
|
|
id <MGLFeature> MGLFeatureFromMBGLFeature(const mbgl::Feature &feature) {
|
|
NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithCapacity:feature.properties.size()];
|
|
for (auto &pair : feature.properties) {
|
|
auto &value = pair.second;
|
|
ValueEvaluator evaluator;
|
|
attributes[@(pair.first.c_str())] = mbgl::Value::visit(value, evaluator);
|
|
}
|
|
GeometryEvaluator<double> evaluator(&feature.properties);
|
|
MGLShape <MGLFeature> *shape = mapbox::geometry::geometry<double>::visit(feature.geometry, evaluator);
|
|
if (!feature.id.is<mapbox::feature::null_value_t>()) {
|
|
shape.identifier = mbgl::FeatureIdentifier::visit(feature.id, ValueEvaluator());
|
|
}
|
|
shape.attributes = attributes;
|
|
|
|
return shape;
|
|
}
|
|
|
|
MGLShape* MGLShapeFromGeoJSON(const mapbox::geojson::geojson &geojson) {
|
|
GeoJSONEvaluator<double> evaluator;
|
|
MGLShape *shape = mapbox::geojson::geojson::visit(geojson, evaluator);
|
|
return shape;
|
|
}
|
|
|
|
mbgl::Feature mbglFeature(mbgl::Feature feature, id identifier, NSDictionary *attributes)
|
|
{
|
|
if (identifier) {
|
|
NSExpression *identifierExpression = [NSExpression expressionForConstantValue:identifier];
|
|
feature.id = [identifierExpression mgl_featureIdentifier];
|
|
}
|
|
feature.properties = [attributes mgl_propertyMap];
|
|
return feature;
|
|
}
|
|
|
|
NSDictionary<NSString *, id> *NSDictionaryFeatureForGeometry(NSDictionary *geometry, NSDictionary *attributes, id identifier) {
|
|
NSMutableDictionary *feature = [@{@"type": @"Feature",
|
|
@"properties": attributes,
|
|
@"geometry": geometry} mutableCopy];
|
|
feature[@"id"] = identifier;
|
|
return [feature copy];
|
|
}
|