Search code examples
androidmapboxmapbox-android

Android Studio Mapbox Layer Problem with both Symbol Layer and Circle Layer Crash


I am developing a geovisualization data map that would plot the map of different colors depending on different number values. And, would want to have the number values be visualized on top of the circles too (it is a data of air pollution in a road circling around a route). The problem is when the two layers SymbolLayer and CircleLayer are applied at the same time they crash the mobile app, which does not work. I tried testing out only one of the layers they work, only when both are applied they crash. Is there a possibility for both layer tow work where the colored circle is present and the number value label is too? Also, I am using data stored in Tilesets. Below is the image link of the app. Cannot post images yet since this is my first time using StackOverflow.

Click to view the Mobile App UI

This is my Tileset that I am using

        //Map View
    mapView = findViewById(R.id.mapView);
    mapView.onCreate(savedInstanceState);
    mapView.getMapAsync(new OnMapReadyCallback() {
        @Override
        public void onMapReady(@NonNull final MapboxMap mapboxMap) {

            mapboxMap.setStyle(Style.LIGHT, new Style.OnStyleLoaded() {
                @Override
                public void onStyleLoaded(@NonNull Style style) {
                    style.addSource(new VectorSource(
                            VECTOR_SOURCE_ID,
                            "http://api.mapbox.com/v4/"+TILESET_KEY+".json?access_token=" + Mapbox.getAccessToken()
                    ));

                    SymbolLayer label =new SymbolLayer(TILESET_LAYER_ID, VECTOR_SOURCE_ID);
                    label.setSourceLayer(VECTOR_SOURCE_ID);
                    label.withProperties(
                            textField(get("gaslvl_PM")), //This contains the number values
                            textSize(17f),
                            textColor(Color.RED),
                            textVariableAnchor(
                                    new String[]{TEXT_ANCHOR_TOP, TEXT_ANCHOR_BOTTOM, TEXT_ANCHOR_LEFT, TEXT_ANCHOR_RIGHT}),
                            textJustify(TEXT_JUSTIFY_AUTO),
                            textRadialOffset(0.5f)
                    );
                    label.setFilter(all(
                    //This is to filter out the multiple rounds in the route of one full roundtrip. 
                    //To prevent the same data from the same area to overlap.
                            eq(literal("roundtrip_number"), literal(roundT_val)))); 
                    style.addLayer(label);

                    CircleLayer circleLayer = new CircleLayer(TILESET_LAYER_ID, VECTOR_SOURCE_ID);
                    circleLayer.setSourceLayer(VECTOR_SOURCE_ID);
                    circleLayer.withProperties(
                            circleRadius(
                                    interpolate(
                                            exponential(1.75f),
                                            zoom(),
                                            stop(12, 2f),
                                            stop(22, 180f)
                                    )),
                            circleColor(
                                    match(
                                            get(TILESET_LAYER_ID),rgb(0, 0, 0),
                                            stop("0", COLOR_GREEN),
                                            stop("1", COLOR_GREEN),
                                            stop("2", COLOR_GREEN),
                                            stop("3", COLOR_GREEN),
                                            stop("4", COLOR_GREEN)
                           //Didn't add the rest of the "stop colors" since it's 500 lines. 
                    ));
                    circleLayer.setFilter(all(
                            eq(literal("roundtrip_number"), literal(roundT_val))
                            //eq(literal("date"), literal(Date.valueOf(date_val)))
                    ));
                    style.addLayer(circleLayer);

                }
            });
        }
    });

Solution

  • The attached code has several issues and I do not have 100% confidence but I think the crash is happened because you assign same Layer ID for both symbol layer and circle layer.

    Did you see a crash dump similar to below? It says "Layer layer-id already exists".

    2020-11-06 22:54:49.324 7267-7267/com.example.sof95859 E/Mbgl-MapChangeReceiver: Exception in onDidFinishLoadingStyle com.mapbox.mapboxsdk.style.layers.CannotAddLayerException: Layer layer-id already exists at com.mapbox.mapboxsdk.maps.NativeMapView.nativeAddLayer(Native Method) at com.mapbox.mapboxsdk.maps.NativeMapView.addLayer(NativeMapView.java:858) at com.mapbox.mapboxsdk.maps.Style.addLayer(Style.java:191) at com.example.sof95859.MainActivity$1$1.onStyleLoaded(MainActivity.java:121) at com.mapbox.mapboxsdk.maps.MapboxMap.notifyStyleLoaded(MapboxMap.java:963) at com.mapbox.mapboxsdk.maps.MapboxMap.onFinishLoadingStyle(MapboxMap.java:225) at com.mapbox.mapboxsdk.maps.MapView$MapCallback.onDidFinishLoadingStyle(MapView.java:1373) at com.mapbox.mapboxsdk.maps.MapChangeReceiver.onDidFinishLoadingStyle(MapChangeReceiver.java:198) at com.mapbox.mapboxsdk.maps.NativeMapView.onDidFinishLoadingStyle(NativeMapView.java:1166) at android.os.MessageQueue.nativePollOnce(Native Method) at android.os.MessageQueue.next(MessageQueue.java:336) at android.os.Looper.loop(Looper.java:174) at android.app.ActivityThread.main(ActivityThread.java:7356) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

    I also attach working sample that accesses my sample layer.

    public class MainActivity extends AppCompatActivity {
        private static final String TILESET_KEY = "yochi.6v4pssqn";
        private static final String VECTOR_SOURCE_ID = "vector-source";
        private static final String VECTOR_SOURCE_LAYER_ID = "sof95859-0wzvvn";
        private static final String TILESET_SYMBOL_LAYER_ID = "symbol-layer-id";
        private static final String TILESET_CIRCLE_LAYER_ID = "circle-layer-id";
        private static final int roundT_val = 2;
        private static final Expression COLOR_GREEN = rgb(0, 255, 0);
    
        private MapView mapView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            Mapbox.getInstance(this, getString(R.string.mapbox_access_token));
    
            setContentView(R.layout.activity_main);
    
            mapView = (MapView) findViewById(R.id.mapView);
            mapView.onCreate(savedInstanceState);
            mapView.getMapAsync(new OnMapReadyCallback() {
                @Override
                public void onMapReady(@NonNull MapboxMap mapboxMap) {
                    CameraUpdate update = CameraUpdateFactory.newLatLngBounds(
                            LatLngBounds.from(1, 1, -1, -1), 0, 0, 0 );
                    mapboxMap.easeCamera(update);
    
                    mapboxMap.setStyle(Style.MAPBOX_STREETS, new Style.OnStyleLoaded() {
                        @Override
                        public void onStyleLoaded(@NonNull Style style) {
                            // tileset.json API call did not work for me
                            style.addSource(new VectorSource(VECTOR_SOURCE_ID, "mapbox://" + TILESET_KEY));
    
                            // Asssign unique layer ID
                            SymbolLayer label =new SymbolLayer(TILESET_SYMBOL_LAYER_ID, VECTOR_SOURCE_ID);
                            // Set layer name by checking Tileset in Studio
                            label.setSourceLayer(VECTOR_SOURCE_LAYER_ID);
                            label.withProperties(
                                    textField(get("gaslvl_PM")), //This contains the number values
                                    textSize(17f),
                                    textColor(Color.RED),
                                    textVariableAnchor(
                                            new String[]{TEXT_ANCHOR_TOP, TEXT_ANCHOR_BOTTOM, TEXT_ANCHOR_LEFT, TEXT_ANCHOR_RIGHT}),
                                    textJustify(TEXT_JUSTIFY_AUTO),
                                    textRadialOffset(0.5f)
                            );
                            label.setFilter(all(
                                    //This is to filter out the multiple rounds in the route of one full roundtrip.
                                    //To prevent the same data from the same area to overlap.
                                    eq(literal("roundtrip_number"), literal(roundT_val))));
                            style.addLayer(label);
    
                            CircleLayer circleLayer = new CircleLayer(TILESET_CIRCLE_LAYER_ID, VECTOR_SOURCE_ID);
                            // Set layer name by checking Tileset in Studio
                            circleLayer.setSourceLayer(VECTOR_SOURCE_LAYER_ID);
                            circleLayer.withProperties(
                                    circleRadius(
                                            interpolate(
                                                    exponential(1.75f),
                                                    zoom(),
                                                    stop(12, 2f),
                                                    stop(22, 180f)
                                            )),
                                    circleColor(
                                            match(
                                                    // I think this is wrong
                                                    get(TILESET_CIRCLE_LAYER_ID),rgb(0, 0, 0),
                                                    stop("0", COLOR_GREEN),
                                                    stop("1", COLOR_GREEN),
                                                    stop("2", COLOR_GREEN),
                                                    stop("3", COLOR_GREEN),
                                                    stop("4", COLOR_GREEN)
                            // the number of parentheses is wrong
                            )));
                            circleLayer.setFilter(all(
                                    eq(literal("roundtrip_number"), literal(roundT_val))
                                    //eq(literal("date"), literal(Date.valueOf(date_val)))
                            ));
                            style.addLayer(circleLayer);
    
                        }
                    });
                }
            });
        }
    

    comments added

    The important thing is "layerId" parameter for SymbolLayer, CircleLayer constructor is not the layer name in the tileset. You define any unique "layerId" in each constructor of SymbolLayer, CircleLayer etc...

    You can use same data source for each layer by putting same Source ID in the second parameter of SymbolLayer, CircleLayer constructor. Please note that Source ID is also you define any Source ID in style.addSource.

    Then to select which Layer should be used in each layer, you call label.setSourceLayer(VECTOR_SOURCE_LAYER_ID);

    Here's the summary of IDs

    • TILESET_KEY: Tileset ID provided by Studio
    • VECTOR_SOURCE_LAYER_ID: Layer Name provided by Studio
    • VECTOR_SOURCE_ID: Any source ID you can define
    • TILESET_SYMBOL_LAYER_ID, TILESET_CIRCLE_LAYER_ID: Any layer ID you can define

    enter image description here