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:
wangyingfang
2025-04-21 19:36:23 +08:00
committed by GitHub
parent b03d4b348e
commit c67dfc85dd
6 changed files with 224 additions and 4 deletions

View File

@@ -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
/**

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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.