Skip to content

Conversation

@AtlasProgramming
Copy link
Collaborator

This is a collection of plug-in capability. This was originally multiple PRs, but due to the complexity of trying to keep the PRs open while they were being developed, it became clear we needed to combine into a single PR for the community to review and we can use this to build consensus on how plugins can work and then if specific categories of plug-ins are going to be used, we can always create a separate PR to pull in a specific set of plugins.

The different types of plugin supported in this PR are:

  • Feature Geometry: Adds a call back to plugin layers that receives filtered data from tiles during loading
  • Protocol Handlers: Adds ability to fetch data from arbitrary sources
  • Style Filters: Ability to apply transformations to styles before they are parsed by the engine
  • There are new delegate methods on the iOS HTTP fetch for intercepting and adjusting the request and response

Feature Geometry

This adds the ability to latch into the tile loading and receive the geometry and properties for features in the tiles. This is just a natural extension of the plug in layers. The only thing the plugin implementor needs to do is add a new override to the plugin layer class to support receiving the geometry. Then the geometry features that are passed to the method is filtered using the same paradigm as all the other layer types. Just set a source, source layer and filter in the style.

For the layer definition, the supportsReadingTileFeatures property needs to be set to true.

+(MLNPluginLayerCapabilities *)layerCapabilities {

    MLNPluginLayerCapabilities *tempResult = [[MLNPluginLayerCapabilities alloc] init];
    tempResult.layerID = @"maplibre::filter_features";
    tempResult.requiresPass3D = YES;
    tempResult.supportsReadingTileFeatures = YES;
    return tempResult;

}

The layer is then inserted into the style with the type, source and source-layer.

  {   "id": "centroid-features",
            "type": "maplibre::filter_features",
            "source": "maplibre",
            "source-layer": "countries",
            "maxzoom": 24,
            "minzoom": 1
        },

Simple handler just logging all the features that pass the filter, their properties and coordinates.

-(void)featureLoaded:(MLNPluginLayerTileFeature *)tileFeature {

    // Writing a single string since the tile loading is multithreaded and the output can get interwoven
    NSMutableString *outputStr = [NSMutableString string];
    [outputStr appendFormat:@"Tile Feature (id:%@) Properties: %@\n", tileFeature.featureID, tileFeature.featureProperties];

    for (NSValue *v in tileFeature.featureCoordinates) {

        CLLocationCoordinate2D coord;
        [v getValue:&coord];

        [outputStr appendFormat:@"  -> (%f, %f) \n", coord.latitude, coord.longitude];

    }

    NSLog(@"Feature: %@", outputStr);
}

Protocol Handler

This adds support similar to plugin-layers and style filters by exposing a data loading paradigm via a plug-in class and lambda implementation. This does not create or implement a way to chain FileSources together.

To add a custom handler, descend from MLNPluginProtocolHandler and implement the two methods to let the system know if this handler can handle the request and then return data when the system asks for it. There's a sample implementation which just loads the local test style file when the url starts with "pluginProtocol"

Like other plug in items, these handlers are added to the map view at runtime with a single call.

    [self.mapView addPluginProtocolHandler:[PluginProtocolExample class]];
#import "PluginProtocolExample.h"

@implementation PluginProtocolExample

-(BOOL)canRequestResource:(MLNPluginProtocolHandlerResource *)resource {
    if ([resource.resourceURL containsString:@"pluginProtocol"]) {
        return YES;
    }
    return NO;
}

-(MLNPluginProtocolHandlerResponse *)requestResource:(MLNPluginProtocolHandlerResource *)resource {
    
    NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"PluginLayerTestStyle.json" ofType:nil]];
    
    MLNPluginProtocolHandlerResponse *response = [[MLNPluginProtocolHandlerResponse alloc] init];
    response.data = data;
    return response;
    
}

@end

Supported on Android as well. Just descend from the PluginProtocolHandler class and implement the two methods, one for if this handler can support the request and returning the actual data for the request.

package org.maplibre.android.testapp.activity.plugin

import org.maplibre.android.plugin.PluginProtocolHandler
import org.maplibre.android.plugin.PluginProtocolHandlerResource
import org.maplibre.android.plugin.PluginProtocolHandlerResponse

class PluginProtocolExample : PluginProtocolHandler() {

    override fun canRequestResource(resource: PluginProtocolHandlerResource?): Boolean {

      // Put whatever logic is needed to determine if the protocol handler should handle this request here
    }


    override fun requestResource(resource: PluginProtocolHandlerResource?): PluginProtocolHandlerResponse? {
        // Return some data here
        return null;
    }

}

and register the plugin using the mapview

mapView.getMapAsync {
            val protocolExample = PluginProtocolExample();
            mapView.addPluginProtocolHandler(protocolExample);

Style Filters

This will add the ability to latch into the style loading and modify the style before it gets to the actual style parsing. There are two parts to this functionality. The first is a C++ based latch into the actual style that will allow a lambda to be assigned to each filter that will receive (in order of add) the data after it's loaded from the source (e.g. http/file/etc). Each filter is passed the output of the previous filter.

The second part is a platform implementation of this for iOS that creates a base filter class that the user can descend from, override a single method and then get the input data and return the data to pass along the filter chain. Since the filters are added at runtime, this will allow run-time filter plugins to be registered and utilized.

Potential uses for this are libraries of different filter types (e.g. uncompressing a compressed/encrypted format, filtering or translating layer properties before they are parsed, injecting specific layer types for all styles, etc).

An example implementation is included in the PR. This filter gets the style passed to it before it's parsed and it adjusts the style to remove some layer types. Since the plug-in receives the style before it goes to the parser, any transformation of the style could happen in the filter.

@implementation StyleFilterExample

// This will filter the data passed in
-(NSData *)filterData:(NSData *)data {
    // Don't call super

    // This example will remove any layer that has "metal-rendering-layer" in the id

    // Parse the JSON: Make the containers mutable
    NSError *error = nil;
    NSMutableDictionary *styleDictionary = [NSJSONSerialization JSONObjectWithData:data
                                                                          options:NSJSONReadingMutableContainers
                                                                            error:&error];

    NSData *tempResult = data;
    if (styleDictionary) {

        // Get the layer array
        NSMutableArray *layerArray = [styleDictionary objectForKey:@"layers"];

        // Create an array to hold which objects to remove since we can't remove them in the loop
        NSMutableArray *removedLayers = [NSMutableArray array];

        // Loop the layers and look for any layers that have the search string in them
        for (NSMutableDictionary *layer in layerArray) {
            NSString *layerID = [layer objectForKey:@"id"];

            // If we find the layers we're looking for, add them to the list of layers to remove
            if ([layerID containsString:@"metal-rendering-layer"]) {
                [removedLayers addObject:layer];
            }
        }

        // Go through and remove any layers that were found
        for (NSMutableDictionary *l in removedLayers) {
            [layerArray removeObject:l];
        }

        // Re-create the JSON, this time the layers we filtered out won't be there
        NSData *filteredStyleJSON = [NSJSONSerialization dataWithJSONObject:styleDictionary
                                                                    options:0
                                                                      error:&error];

        // If the JSON write is successful, then set the output to the new json style
        if (filteredStyleJSON) {
            tempResult = filteredStyleJSON;
        }

    }

    // Return the data
    return tempResult;

}

Like all the plugins, the style filters are coupled to the engine at runtime using the addStyleFilter method on the map view.

    [self.mapView addStyleFilter:[[StyleFilterExample alloc] init]];

“Malcolm and others added 30 commits July 7, 2025 01:50
…ing/maplibre-native into feature/2025-07-raw-bucket

# Conflicts:
#	src/mbgl/tile/geometry_tile_worker.cpp
…amming/maplibre-native into feature/2025-07-style-filters

# Conflicts:
#	src/mbgl/style/style_impl.cpp
…amming/maplibre-native into feature/2025-07-style-filters
@github-actions
Copy link

github-actions bot commented Aug 6, 2025

Bloaty Results (iOS) 🐋

Compared to main

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.0%  +150Ki  +1.2%  +176Ki    TOTAL

Full report: https://maplibre-native.s3.eu-central-1.amazonaws.com/bloaty-results-ios/pr-3703-compared-to-main.txt

@github-actions
Copy link

github-actions bot commented Aug 6, 2025

Bloaty Results 🐋

Compared to main

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1%  +131Ki  +0.1% +25.6Ki    TOTAL

Full report: https://maplibre-native.s3.eu-central-1.amazonaws.com/bloaty-results/pr-3703-compared-to-main.txt

Compared to d387090 (legacy)

    FILE SIZE        VM SIZE    
 --------------  -------------- 
   +46% +53.5Mi  +483% +28.9Mi    TOTAL

Full report: https://maplibre-native.s3.eu-central-1.amazonaws.com/bloaty-results/pr-3703-compared-to-legacy.txt

@github-actions
Copy link

github-actions bot commented Aug 6, 2025

Benchmark Results ⚡

Benchmark                                                     Time             CPU      Time Old      Time New       CPU Old       CPU New
------------------------------------------------------------------------------------------------------------------------------------------
OVERALL_GEOMEAN                                            +0.0028         +0.0025             0             0             0             0

Full report: https://maplibre-native.s3.eu-central-1.amazonaws.com/benchmark-results/pr-3703-compared-to-main.txt

@AtlasProgramming AtlasProgramming force-pushed the maplibre/feature/2025-08-plugins branch from 51e3eed to 7f96a34 Compare August 11, 2025 13:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

android build Related to build, configuration or CI/CD cpp-core iOS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant