Allow initializing MLNMapView with style json (#3240)
Co-authored-by: Yingfang <junew@microsoft.com> Co-authored-by: Bart Louwers <bart@emeel.net>
This commit is contained in:
@@ -70,6 +70,20 @@ MLN_EXPORT
|
||||
*/
|
||||
@property (readonly, copy, nullable) NSString *name;
|
||||
|
||||
/**
|
||||
* The style JSON representation of the map.
|
||||
*
|
||||
* Setting this property results in an asynchronous style change. If you wish to know when the style
|
||||
* change is complete, observe the ``MLNMapViewDelegate/mapView:didFinishLoadingStyle:`` method
|
||||
* on ``MLNMapViewDelegate``.
|
||||
*
|
||||
* The JSON must conform to the
|
||||
* <a href="https://maplibre.org/maplibre-style-spec/">MapLibre Style Specification</a>.
|
||||
*
|
||||
* @throws NSInvalidArgumentException if styleJSON is nil or invalid JSON
|
||||
*/
|
||||
@property (nonatomic, copy) NSString *styleJSON;
|
||||
|
||||
// MARK: Managing Sources
|
||||
|
||||
/**
|
||||
|
||||
@@ -139,6 +139,16 @@ const MLNExceptionName MLNRedundantSourceIdentifierException = @"MLNRedundantSou
|
||||
return name.empty() ? nil : @(name.c_str());
|
||||
}
|
||||
|
||||
- (NSString *)styleJSON {
|
||||
std::string styleJSON = self.rawStyle->getJSON();
|
||||
// Return empty string if no style is set
|
||||
return styleJSON.empty() ? @"" : @(styleJSON.c_str());
|
||||
}
|
||||
|
||||
- (void)setStyleJSON:(NSString *)styleJSON {
|
||||
self.rawStyle->loadJSON([styleJSON UTF8String]);
|
||||
}
|
||||
|
||||
// MARK: Sources
|
||||
|
||||
- (NSSet<__kindof MLNSource *> *)sources {
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
#endif
|
||||
|
||||
static MLNMapView *mapView;
|
||||
|
||||
@interface MLNMapViewTests : XCTestCase
|
||||
@interface MLNMapViewTests : XCTestCase <MLNMapViewDelegate>
|
||||
@end
|
||||
|
||||
@implementation MLNMapViewTests
|
||||
@implementation MLNMapViewTests {
|
||||
XCTestExpectation *_styleLoadingExpectation;
|
||||
XCTestExpectation *_styleLoadErrorExpectation;
|
||||
}
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
@@ -21,14 +23,35 @@ static MLNMapView *mapView;
|
||||
[MLNSettings setApiKey:@"pk.feedcafedeadbeefbadebede"];
|
||||
NSURL *styleURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"one-liner" withExtension:@"json"];
|
||||
mapView = [[MLNMapView alloc] initWithFrame:CGRectMake(0, 0, 64, 64) styleURL:styleURL];
|
||||
mapView.delegate = self;
|
||||
if (!mapView.style) {
|
||||
_styleLoadingExpectation = [self expectationWithDescription:@"Map view should finish loading style."];
|
||||
[self waitForExpectationsWithTimeout:10 handler:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
_styleLoadingExpectation = nil;
|
||||
mapView = nil;
|
||||
[MLNSettings setApiKey:nil];
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
- (void)mapView:(MLNMapView *)mapView didFinishLoadingStyle:(MLNStyle *)style {
|
||||
XCTAssertNotNil(mapView.style);
|
||||
XCTAssertEqual(mapView.style, style);
|
||||
|
||||
[_styleLoadingExpectation fulfill];
|
||||
}
|
||||
|
||||
- (void)mapViewDidFailLoadingMap:(MLNMapView *)mapView withError:(NSError *)error {
|
||||
XCTAssertNotNil(error);
|
||||
XCTAssertEqual(error.domain, MLNErrorDomain);
|
||||
if (error.code == MLNErrorCodeLoadStyleFailed || error.code == MLNErrorCodeParseStyleFailed) {
|
||||
[_styleLoadErrorExpectation fulfill];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testCoordinateBoundsConversion {
|
||||
[mapView setCenterCoordinate:CLLocationCoordinate2DMake(33, 179)];
|
||||
|
||||
@@ -160,5 +183,105 @@ static MLNMapView *mapView;
|
||||
XCTAssertEqual(mapView.tileCacheEnabled, YES);
|
||||
}
|
||||
|
||||
- (void)testStyleJSONWhenMapViewInitWithStyleURL {
|
||||
// Test getting style JSON
|
||||
NSString *styleJSON = mapView.styleJSON;
|
||||
XCTAssertNotNil(styleJSON, @"Style JSON should not be nil");
|
||||
NSString * expectedJSON = @"{\"version\":8,\"sources\":{},\"layers\":[]}";
|
||||
XCTAssertEqualObjects([self normalizeJSON:expectedJSON],
|
||||
[self normalizeJSON:styleJSON],
|
||||
@"Style JSON should be expected");
|
||||
|
||||
// Verify the JSON is valid
|
||||
NSError *error = nil;
|
||||
id jsonObject = [NSJSONSerialization JSONObjectWithData:[styleJSON dataUsingEncoding:NSUTF8StringEncoding]
|
||||
options:0
|
||||
error:&error];
|
||||
XCTAssertNil(error, @"Style JSON should be valid JSON");
|
||||
XCTAssertNotNil(jsonObject, @"Style JSON should parse to a valid object");
|
||||
XCTAssertTrue([jsonObject isKindOfClass:[NSDictionary class]], @"Style JSON should represent a dictionary");
|
||||
}
|
||||
|
||||
- (void)testMapViewInitWithStyleJSON {
|
||||
// Test setting style JSON
|
||||
NSString *styleJSON = @"{\"version\": 8, \"sources\": { \"mapbox\": {\"type\": \"vector\", \"tiles\": [ \"local://tiles/{z}-{x}-{y}.mvt\" ] }}, \"layers\": [], \"metadata\": { \"test\": 1, \"type\": \"template\"}}";
|
||||
MLNMapView* mapViewWithJSON = [[MLNMapView alloc] initWithFrame:CGRectMake(0, 0, 64, 64) styleJSON:styleJSON];
|
||||
|
||||
// Verify the style was updated
|
||||
NSString *loadedStyleJSON = mapViewWithJSON.styleJSON;
|
||||
XCTAssertEqualObjects([self normalizeJSON:loadedStyleJSON],
|
||||
[self normalizeJSON:styleJSON],
|
||||
@"Style JSON should match what was set");
|
||||
|
||||
XCTAssertNotNil(mapViewWithJSON.style);
|
||||
XCTAssertNotNil(mapViewWithJSON.style.sources);
|
||||
}
|
||||
|
||||
- (void)testUpdateStyleJSON {
|
||||
_styleLoadingExpectation = nil;
|
||||
_styleLoadErrorExpectation = [self expectationWithDescription:@"Style should load error"];
|
||||
|
||||
// Test setting style JSON
|
||||
NSString *newStyleJSON = @"{\"version\": 8, \"sources\": { \"mapbox\": {\"type\": \"vector\", \"tiles\": [ \"local://tiles/{z}-{x}-{y}.mvt\" ] }}, \"layers\": [], \"metadata\": { \"test\": 1, \"type\": \"template\"}}";
|
||||
mapView.styleJSON = newStyleJSON;
|
||||
|
||||
// Verify the style was updated
|
||||
NSString *updatedStyleJSON = mapView.styleJSON;
|
||||
XCTAssertEqualObjects([self normalizeJSON:updatedStyleJSON],
|
||||
[self normalizeJSON:newStyleJSON],
|
||||
@"Style JSON should match what was set");
|
||||
|
||||
XCTAssertNotNil(mapView.style);
|
||||
XCTAssertNotNil(mapView.style.sources);
|
||||
// source "org.maplibre.annotations" is added by default
|
||||
XCTAssertEqual(mapView.style.sources.count, 2UL);
|
||||
|
||||
// Test invalid JSON syntax
|
||||
NSString *invalidJSON = @"{invalid json";
|
||||
mapView.styleJSON = invalidJSON;
|
||||
[self waitForExpectations:@[_styleLoadErrorExpectation] timeout:10];
|
||||
}
|
||||
|
||||
- (void)testStyleJSONAfterAddLayer {
|
||||
// Test getting style JSON
|
||||
NSString *styleJSON = mapView.styleJSON;
|
||||
XCTAssertNotNil(styleJSON, @"Style JSON should not be nil");
|
||||
|
||||
// Add a raster tile source
|
||||
MLNVectorTileSource *vectorTileSource = [[MLNVectorTileSource alloc] initWithIdentifier:@"some-identifier" tileURLTemplates:@[] options:nil];
|
||||
[mapView.style addSource:vectorTileSource];
|
||||
|
||||
// Add a layer using it
|
||||
MLNFillStyleLayer *fillLayer = [[MLNFillStyleLayer alloc] initWithIdentifier:@"fillLayer" source:vectorTileSource];
|
||||
[mapView.style addLayer:fillLayer];
|
||||
|
||||
// Style JSON should not be updated
|
||||
NSString *updatedStyleJSON = mapView.styleJSON;
|
||||
|
||||
XCTAssertEqualObjects([self normalizeJSON:updatedStyleJSON],
|
||||
[self normalizeJSON:styleJSON],
|
||||
@"Style JSON should be updated");
|
||||
}
|
||||
|
||||
// Helper method to normalize JSON strings for comparison
|
||||
- (NSString *)normalizeJSON:(NSString *)jsonString {
|
||||
NSError *error = nil;
|
||||
id jsonObject = [NSJSONSerialization JSONObjectWithData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]
|
||||
options:0
|
||||
error:&error];
|
||||
if (error) {
|
||||
return jsonString;
|
||||
}
|
||||
|
||||
NSData *normalizedData = [NSJSONSerialization dataWithJSONObject:jsonObject
|
||||
options:0
|
||||
error:&error];
|
||||
if (error) {
|
||||
return jsonString;
|
||||
}
|
||||
|
||||
NSString *normalizedString = [[NSString alloc] initWithData:normalizedData encoding:NSUTF8StringEncoding];
|
||||
return normalizedString ?: jsonString;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -422,4 +422,19 @@
|
||||
XCTAssertFalse(self.style.performsPlacementTransitions, @"Enabling placement transitions should be NO.");
|
||||
}
|
||||
|
||||
- (void)testStyleJSON {
|
||||
// Test getting style JSON
|
||||
NSString *styleJSON = self.style.styleJSON;
|
||||
XCTAssertNotNil(styleJSON, @"Style JSON should not be nil");
|
||||
|
||||
// Verify the JSON is valid
|
||||
NSError *error = nil;
|
||||
id jsonObject = [NSJSONSerialization JSONObjectWithData:[styleJSON dataUsingEncoding:NSUTF8StringEncoding]
|
||||
options:0
|
||||
error:&error];
|
||||
XCTAssertNil(error, @"Style JSON should be valid JSON");
|
||||
XCTAssertNotNil(jsonObject, @"Style JSON should parse to a valid object");
|
||||
XCTAssertTrue([jsonObject isKindOfClass:[NSDictionary class]], @"Style JSON should represent a dictionary");
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -207,6 +207,18 @@ MLN_EXPORT
|
||||
*/
|
||||
- (instancetype)initWithFrame:(CGRect)frame styleURL:(nullable NSURL *)styleURL;
|
||||
|
||||
/**
|
||||
* Initializes and returns a newly allocated map view with the specified frame
|
||||
* and style JSON.
|
||||
*
|
||||
* @param frame The frame for the view, measured in points.
|
||||
* @param styleJSON JSON string of the map style to display. The JSON must conform to the
|
||||
* <a href="https://maplibre.org/maplibre-style-spec/">MapLibre Style Specification</a>.
|
||||
* Specify `nil` for the default style.
|
||||
* @return An initialized map view.
|
||||
*/
|
||||
- (instancetype)initWithFrame:(CGRect)frame styleJSON:(NSString *)styleJSON;
|
||||
|
||||
// MARK: Accessing the Delegate
|
||||
|
||||
/**
|
||||
@@ -253,6 +265,20 @@ MLN_EXPORT
|
||||
*/
|
||||
@property (nonatomic, null_resettable) NSURL *styleURL;
|
||||
|
||||
/**
|
||||
* The style JSON representation of the map.
|
||||
*
|
||||
* Setting this property results in an asynchronous style change. If you wish to know when the style
|
||||
* change is complete, observe the ``MLNMapViewDelegate/mapView:didFinishLoadingStyle:`` method
|
||||
* on ``MLNMapViewDelegate``.
|
||||
*
|
||||
* The JSON must conform to the
|
||||
* <a href="https://maplibre.org/maplibre-style-spec/">MapLibre Style Specification</a>.
|
||||
*
|
||||
* @throws NSInvalidArgumentException if styleJSON is nil or invalid JSON
|
||||
*/
|
||||
@property (nonatomic, copy) NSString *styleJSON;
|
||||
|
||||
/**
|
||||
Reloads the style.
|
||||
|
||||
|
||||
@@ -424,6 +424,7 @@ public:
|
||||
@property (nonatomic, copy) MLNMapCamera *residualCamera;
|
||||
@property (nonatomic) MLNMapDebugMaskOptions residualDebugMask;
|
||||
@property (nonatomic, copy) NSURL *residualStyleURL;
|
||||
@property (nonatomic, copy, nullable) NSString *initialStyleJSON;
|
||||
|
||||
/// Tilt gesture recognizer helper
|
||||
@property (nonatomic, assign) CGPoint dragGestureMiddlePoint;
|
||||
@@ -522,6 +523,20 @@ public:
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame styleJSON:(NSString *)styleJSON
|
||||
{
|
||||
if (self = [super initWithFrame:frame])
|
||||
{
|
||||
MLNLogInfo(@"Starting %@ initialization.", NSStringFromClass([self class]));
|
||||
MLNLogDebug(@"Initializing frame: %@ styleJSON: %@", NSStringFromCGRect(frame), styleJSON);
|
||||
[self commonInit];
|
||||
self.styleJSON = styleJSON;
|
||||
_initialStyleJSON = [styleJSON copy];
|
||||
MLNLogInfo(@"Finalizing %@ initialization.", NSStringFromClass([self class]));
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(nonnull NSCoder *)decoder
|
||||
{
|
||||
if (self = [super initWithCoder:decoder])
|
||||
@@ -552,6 +567,11 @@ public:
|
||||
return self.residualStyleURL;
|
||||
}
|
||||
|
||||
if (self.mbglMap.getStyle().getJSON().length() > 0 &&
|
||||
self.mbglMap.getStyle().getURL().empty()) {
|
||||
return [NSURL URLWithString:@"local://style.json"];
|
||||
}
|
||||
|
||||
NSString *styleURLString = @(self.mbglMap.getStyle().getURL().c_str()).mgl_stringOrNilIfEmpty;
|
||||
MLNAssert(styleURLString, @"Invalid style URL string %@", styleURLString);
|
||||
return styleURLString ? [NSURL URLWithString:styleURLString] : nil;
|
||||
@@ -569,6 +589,16 @@ public:
|
||||
self.mbglMap.getStyle().loadURL([[styleURL absoluteString] UTF8String]);
|
||||
}
|
||||
|
||||
- (NSString *)styleJSON {
|
||||
return self.style.styleJSON;
|
||||
}
|
||||
|
||||
- (void)setStyleJSON:(NSString *)styleJSON {
|
||||
// Reset style and load new JSON
|
||||
self.style = nil;
|
||||
self.mbglMap.getStyle().loadJSON([styleJSON UTF8String]);
|
||||
}
|
||||
|
||||
- (IBAction)reloadStyle:(__unused id)sender {
|
||||
MLNLogInfo(@"Reloading style.");
|
||||
NSURL *styleURL = self.styleURL;
|
||||
@@ -890,7 +920,9 @@ public:
|
||||
self.terminated = YES;
|
||||
self.residualCamera = self.camera;
|
||||
self.residualDebugMask = self.debugMask;
|
||||
self.residualStyleURL = self.styleURL;
|
||||
if (!_initialStyleJSON) {
|
||||
self.residualStyleURL = self.styleURL;
|
||||
}
|
||||
|
||||
// Tear down C++ objects, insuring worker threads correctly terminate.
|
||||
// Because of how _mbglMap is constructed, we need to destroy it first.
|
||||
|
||||
Reference in New Issue
Block a user