Skip to content
Open
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
83 changes: 82 additions & 1 deletion platform/darwin/src/MLNShape.mm
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,86 @@
return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude;
}

static BOOL MLNIsGeoJSONGeometryType(NSString *type) {
return [type isEqualToString:@"Point"] || [type isEqualToString:@"MultiPoint"] ||
[type isEqualToString:@"LineString"] || [type isEqualToString:@"MultiLineString"] ||
[type isEqualToString:@"Polygon"] || [type isEqualToString:@"MultiPolygon"];
}

static NSRegularExpression *MLNEmptyArrayRegularExpression(void) {
static NSRegularExpression *regularExpression;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
regularExpression = [NSRegularExpression regularExpressionWithPattern:@"\\[\\s*\\]"
options:0
error:nil];
});
return regularExpression;
}

static BOOL MLNReplaceEmptyFeatureCoordinates(id object) {
if (![object isKindOfClass:[NSMutableDictionary class]]) {
return NO;
}

NSMutableDictionary *dictionary = object;
NSString *type = dictionary[@"type"];
if (![type isKindOfClass:[NSString class]]) {
return NO;
}
if ([type isEqualToString:@"Feature"]) {
id geometry = dictionary[@"geometry"];
if (![geometry isKindOfClass:[NSDictionary class]]) {
return NO;
}

NSString *geometryType = geometry[@"type"];
id coordinates = geometry[@"coordinates"];
if (![geometryType isKindOfClass:[NSString class]] || !MLNIsGeoJSONGeometryType(geometryType) ||
![coordinates isKindOfClass:[NSArray class]] || [coordinates count] != 0) {
return NO;
}

dictionary[@"geometry"] = [NSNull null];
return YES;
}

if (![type isEqualToString:@"FeatureCollection"]) {
return NO;
}

NSArray *features = dictionary[@"features"];
if (![features isKindOfClass:[NSArray class]]) {
return NO;
}

BOOL changed = NO;
for (id feature in features) {
changed |= MLNReplaceEmptyFeatureCoordinates(feature);
}
return changed;
}

static NSString *MLNGeoJSONStringByReplacingEmptyFeatureCoordinates(NSString *string) {
NSRange range = NSMakeRange(0, string.length);
if (![MLNEmptyArrayRegularExpression() firstMatchInString:string options:0 range:range]) {
return string;
}

NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
id jsonObject = [NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableContainers
error:nil];
if (!MLNReplaceEmptyFeatureCoordinates(jsonObject)) {
return string;
}

NSData *updatedData = [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:nil];
NSString *updatedString = [[NSString alloc] initWithData:updatedData
encoding:NSUTF8StringEncoding];
return updatedString ?: string;
}

@implementation MLNShape

+ (nullable MLNShape *)shapeWithData:(NSData *)data
Expand All @@ -24,8 +104,9 @@ + (nullable MLNShape *)shapeWithData:(NSData *)data
return nil;
}

NSString *normalizedString = MLNGeoJSONStringByReplacingEmptyFeatureCoordinates(string);
try {
const auto geojson = mapbox::geojson::parse(string.UTF8String);
const auto geojson = mapbox::geojson::parse(normalizedString.UTF8String);
return MLNShapeFromGeoJSON(geojson);
} catch (std::runtime_error &err) {
if (outError) {
Expand Down
66 changes: 66 additions & 0 deletions platform/darwin/test/MLNShapeSourceTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,72 @@ - (void)testMLNShapeSourceWithDataMultipleFeatures {
XCTAssertTrue([collection.shapes.firstObject isMemberOfClass:[MLNPolylineFeature class]]);
}

- (void)testMLNShapeSourceWithNullGeometry {
NSData *data = [@"{\"type\":\"Feature\",\"properties\":{},\"geometry\":null}"
dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
MLNShape *shape = [MLNShape shapeWithData:data encoding:NSUTF8StringEncoding error:&error];

XCTAssertNil(error);
XCTAssertTrue([shape isKindOfClass:[MLNEmptyFeature class]]);

MLNShapeSource *source;
XCTAssertNoThrow(source = [[MLNShapeSource alloc] initWithIdentifier:@"source-id"
shape:shape
options:nil]);
XCTAssertNotNil(source.shape);
}

- (void)testMLNShapeSourceWithEmptyFeatureCoordinates {
NSArray<NSString *> *geometryTypes =
@[ @"Point", @"MultiPoint", @"LineString", @"MultiLineString", @"Polygon", @"MultiPolygon" ];
for (NSString *geometryType in geometryTypes) {
NSString *geoJSON =
[NSString stringWithFormat:@"{\"type\":\"Feature\",\"properties\":{},\"geometry\":{"
@"\"type\":\"%@\",\"coordinates\":[ ]}}",
geometryType];
NSData *data = [geoJSON dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
MLNShape *shape = [MLNShape shapeWithData:data encoding:NSUTF8StringEncoding error:&error];

XCTAssertNil(error);
XCTAssertTrue([shape isKindOfClass:[MLNEmptyFeature class]]);

MLNShapeSource *source;
XCTAssertNoThrow(source = [[MLNShapeSource alloc] initWithIdentifier:@"source-id"
shape:shape
options:nil]);
XCTAssertNotNil(source.shape);
}
}

- (void)testMLNShapeSourceWithEmptyFeatureAndValidSibling {
NSString *geoJSON = @"{\"type\":\"FeatureCollection\",\"features\":["
"{\"type\":\"Feature\",\"properties\":{},\"geometry\":{\"type\":\"Point\","
"\"coordinates\":[]}},"
"{\"type\":\"Feature\",\"properties\":{\"name\":\"Café\"},"
"\"geometry\":{\"type\":\"Point\","
"\"coordinates\":[0,0]}}]}";
NSData *data = [geoJSON dataUsingEncoding:NSISOLatin1StringEncoding];
NSError *error = nil;
MLNShapeCollectionFeature *collection =
(MLNShapeCollectionFeature *)[MLNShape shapeWithData:data
encoding:NSISOLatin1StringEncoding
error:&error];

XCTAssertNil(error);
XCTAssertEqual(collection.shapes.count, 2UL);
XCTAssertTrue([collection.shapes[0] isKindOfClass:[MLNEmptyFeature class]]);
XCTAssertTrue([collection.shapes[1] isKindOfClass:[MLNPointFeature class]]);
XCTAssertEqualObjects([collection.shapes[1] attributeForKey:@"name"], @"Café");

MLNShapeSource *source;
XCTAssertNoThrow(source = [[MLNShapeSource alloc] initWithIdentifier:@"source-id"
shape:collection
options:nil]);
XCTAssertNotNil(source.shape);
}

- (void)testMLNShapeSourceWithSingleGeometry {
NSData *data =
[@"{\"type\": \"Point\", \"coordinates\": [0, 0]}" dataUsingEncoding:NSUTF8StringEncoding];
Expand Down
1 change: 1 addition & 0 deletions platform/ios/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ MapLibre welcomes participation and contributions from everyone. Please read [`M

## 6.29.0

- iOS: Accept GeoJSON features with empty coordinate arrays.
- fix(core): accept alpha in hsl colors ([#4435](https://github.com/maplibre/maplibre-native/pull/4435)).
- fix(core): repaint feature-state-driven symbol paint properties ([#4445](https://github.com/maplibre/maplibre-native/pull/4445)).
- Make string expressions operate on unicode ([#4344](https://github.com/maplibre/maplibre-native/pull/4344)).
Expand Down
1 change: 1 addition & 0 deletions platform/macos/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ MapLibre welcomes participation and contributions from everyone. Please read [`C

## 6.27.0

- Accept GeoJSON features with empty coordinate arrays.
- First automated native macOS XCFramework release ([#4088](https://github.com/maplibre/maplibre-native/issues/4088)).
- Add Bazel `MapLibre.dynamic` and `MapLibre.static` xcframework targets for macOS ([#4113](https://github.com/maplibre/maplibre-native/pull/4113)).
- Fix Xcode 26 build compatibility ([#4226](https://github.com/maplibre/maplibre-native/pull/4226)).
Expand Down