Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NSURL on iOS 17 fails to parse double protocols for PMTiles #3151

Closed
KiwiKilian opened this issue Jan 17, 2025 · 7 comments · Fixed by #3163
Closed

NSURL on iOS 17 fails to parse double protocols for PMTiles #3151

KiwiKilian opened this issue Jan 17, 2025 · 7 comments · Fixed by #3163

Comments

@KiwiKilian
Copy link
Contributor

KiwiKilian commented Jan 17, 2025

MapLibre iOS Version

6.10.0

iOS Version

iOS 17.5

Device

Simulator

What happened?

Using PMTiles like https://example.com/some.pmtiles within a MLNVectorTileSource on iOS 17 simulator fails (no crash) with the following error:

'MapLibre error', 'Requesting: https//example.com/some.pmtiles failed with error: Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL"

Note the missing : in the parsed URL.

Steps to reproduce

To break down the problem, it's enough to parse the URL. I've also included the proposal from @bdon for a more appropriate scheme (pmtiles+https://):

NSURL *url = [NSURL URLWithString:@"pmtiles://https://example.com/some.pmtiles"];
NSLog(@"%@", url.absoluteString);
NSURL *urlAlternative = [NSURL URLWithString:@"pmtiles+https://example.com/some.pmtiles"];
NSLog(@"%@", urlAlternative.absoluteString);

We also used a basic PMTiles example within MapLibre React Native. It basically it creates an MLNVectorTileSource

[MLNVectorTileSource alloc] initWithIdentifier:self.id configurationURL:[NSURL URLWithString:self.url]
  • iOS 15.5
  • iOS 16.4
    • Example works
    • pmtiles://https://example.com/some.pmtiles
    • pmtiles+https://example.com/some.pmtiles
  • iOS 17.0 and iOS 17.5 ❌
    • Example is broken due to (URL changed for readability, note the missing :):

      'MapLibre error', 'Requesting: https//example.com/some.pmtiles failed with error: Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL"

    • pmtiles://https//example.com/some.pmtiles ❌ missing :
    • pmtiles+https://example.com/some.pmtiles
  • iOS 18.2
    • Example works
    • pmtiles://https://example.com/some.pmtiles
    • pmtiles+https://example.com/some.pmtiles

So it seems NSURL is only in iOS 17.x problematic, as I don't have any device running iOS 17 I can only reproduce using the simulator.

The problem does not occur when using a pmtiles://https:// scheme within a style JSON set as styleURL – through this use case the URLs aren't parsed as NSURL. The problem only occurs when creating a MLNVectorTileSource.

@KiwiKilian
Copy link
Contributor Author

There are currently two proposals to fixing this:

@louwers
Copy link
Collaborator

louwers commented Jan 20, 2025

I was able to reproduce this. I'll make a PR with a suggested solution.

@louwers
Copy link
Collaborator

louwers commented Jan 20, 2025

@KiwiKilian We already have a good workaround available, since the TileJSON constructor already uses NSString.

Please see below. Is this an acceptable solution for you?

- (void)addFoursquarePOIsPMTilesLayer {
    // NOTE: the line below works on iOS 18 and up
    // However, on iOS 17 the second : is stripped from the URL
    // that is why we are using the TileJSON constructor as a workaround for this problem below
    //    MLNVectorTileSource *foursquareSource = [[MLNVectorTileSource alloc] initWithIdentifier:@"foursquare-10M" configurationURL:pmtilesURL];

    MLNVectorTileSource *foursquareSource =
      [[MLNVectorTileSource alloc] initWithIdentifier:@"foursquare-10M"
                                     tileURLTemplates:@[@"pmtiles://https://oliverwipfli.ch/data/foursquare-os-places-10M-2024-11-20.pmtiles"]
                                              options:nil];

    // Add the source to the map style
    [self.mapView.style addSource:foursquareSource];

    // Initialize the circle style layer
    MLNCircleStyleLayer *circleLayer = [[MLNCircleStyleLayer alloc] initWithIdentifier:@"foursquare-10M" source:foursquareSource];
    circleLayer.sourceLayerIdentifier = @"place";
    circleLayer.maximumZoomLevel = 11;

    // Set the circle color
    circleLayer.circleColor = [NSExpression expressionForConstantValue:[UIColor colorWithRed:0.8 green:0.2 blue:0.2 alpha:1.0]];
    
    // Set the circle opacity with interpolation
    circleLayer.circleOpacity = [NSExpression expressionWithMLNJSONObject:@[
        @"interpolate",
        @[@"linear"],
        @[@"zoom"],
        @6, @0.5,
        @11, @0.5,
        @14, @1.0
    ]];

    // Set the circle radius with interpolation
    circleLayer.circleRadius = [NSExpression expressionWithMLNJSONObject:@[
        @"interpolate",
        @[@"linear"],
        @[@"zoom"],
        @6, @1,
        @11, @3,
        @16, @4,
        @18, @8
    ]];

    // Add the layer to the map style
    [self.mapView.style addLayer:circleLayer];
}

@KiwiKilian
Copy link
Contributor Author

The workaround solves the problem, but it feels a bit hacky, as it's not an actual tileURLTemplates. It requires us to check, if the URL starts with pmtiles:// and then use the other constructor:

- (nullable MLNSource*)makeSource
{
    if (self.url != nil) {
      if([self.url hasPrefix:@"pmtiles://"]) {
        return [[MLNVectorTileSource alloc] initWithIdentifier:self.id tileURLTemplates:@[self.url] options:nil];
      } else {
        return [[MLNVectorTileSource alloc] initWithIdentifier:self.id configurationURL:[NSURL URLWithString:self.url]];
      }
    }
    return [[MLNVectorTileSource alloc] initWithIdentifier:self.id tileURLTemplates:self.tileUrlTemplates options:[self getOptions]];
}

Of course we could also direct users to use the tileUrlTemplates – but it feels wrong, when the main problem is the usage of malformed URLs for PMTiles. Longterm, I would prefer a cleaner solution.

@louwers
Copy link
Collaborator

louwers commented Jan 20, 2025

Why do you add this explicit check? Is there any downside from always using that approach?

@KiwiKilian
Copy link
Contributor Author

KiwiKilian commented Jan 20, 2025

I would have guessed the two constructors are there for a reason? From the docs:

configurationURL
A URL to a TileJSON configuration file describing the source’s contents and other metadata.

https://maplibre.org/maplibre-native/ios/latest/documentation/maplibre/mlnvectortilesource/initwithidentifier:configurationurl:#parameters

tileURLTemplates
An array of tile URL template strings. Only the first string is used; any additional strings are ignored.

https://maplibre.org/maplibre-native/ios/latest/documentation/maplibre/mlnvectortilesource/initwithidentifier:tileurltemplates:options:#parameters

So don't they work partially different? The first one fetching a JSON and applying the options from there, while the other one just tries to load tiles from the URL and passing the options manually?

Therefore I think its cleaner to properly use these constructors as intended?

@louwers
Copy link
Collaborator

louwers commented Jan 21, 2025

You're completely right. It just happens to work for PMTiles because the same URL can be used for both a TileJSON or a tile.

I'll look into adding an NSString constructor then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants