无法在Mapbox地图Android中添加圆形图层标记聚类

时间:2019-04-30 11:11:08

标签: android maps mapbox-android mapbox-marker

我正在尝试在基于Android的应用中添加标记聚类功能,并且正在使用以下示例:  https://github.com/mapbox/mapbox-android-demo/blob/master/MapboxAndroidDemo/src/main/java/com/mapbox/mapboxandroiddemo/examples/dds/CircleLayerClusteringActivity.java 但我无法正确实施。

这是我的代码,集群的主要逻辑在“ setUpMarkerLayersClustered”函数中:

package com.tripmate.travelguidePakistan;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PointF;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;

import com.google.gson.Gson;
import com.mapbox.geojson.Feature;
import com.mapbox.geojson.FeatureCollection;
import com.mapbox.geojson.Point;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.annotations.BubbleLayout;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.style.expressions.Expression;
import com.mapbox.mapboxsdk.style.layers.CircleLayer;
import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
import com.mapbox.mapboxsdk.style.sources.GeoJsonOptions;
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;

import org.json.JSONObject;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import static android.graphics.Color.rgb;
import static com.mapbox.mapboxsdk.style.expressions.Expression.all;
import static com.mapbox.mapboxsdk.style.expressions.Expression.division;
import static com.mapbox.mapboxsdk.style.expressions.Expression.eq;
import static com.mapbox.mapboxsdk.style.expressions.Expression.exponential;
import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
import static com.mapbox.mapboxsdk.style.expressions.Expression.gt;
import static com.mapbox.mapboxsdk.style.expressions.Expression.gte;
import static com.mapbox.mapboxsdk.style.expressions.Expression.has;
import static com.mapbox.mapboxsdk.style.expressions.Expression.interpolate;
import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
import static com.mapbox.mapboxsdk.style.expressions.Expression.lt;
import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;
import static com.mapbox.mapboxsdk.style.expressions.Expression.toNumber;
import static com.mapbox.mapboxsdk.style.layers.Property.ICON_ANCHOR_BOTTOM;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleColor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleRadius;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAnchor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconColor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconOffset;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textAllowOverlap;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textColor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textField;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textIgnorePlacement;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textSize;

/**
 * Use a SymbolLayer to show a BubbleLayout above a SymbolLayer icon. This is a more performant
 * way to show the BubbleLayout that appears when using the MapboxMap.addMarker() method.
 */
public class InfoWindowSymbolLayerActivity extends AppCompatActivity implements
        OnMapReadyCallback, MapboxMap.OnMapClickListener {

    private static final String GEOJSON_SOURCE_ID = "GEOJSON_SOURCE_ID";
    private static final String MARKER_IMAGE_ID = "MARKER_IMAGE_ID";
    private static final String MARKER_LAYER_ID_UNCLUSTERED = "MARKER_LAYER_ID_UNCLUSTERED";
    private static final String MARKER_LAYER_ID_CLUSTERED = "MARKER_LAYER_ID_CLUSTERED";
    private static final String CALLOUT_LAYER_ID = "CALLOUT_LAYER_ID";
    private static final String PROPERTY_SELECTED = "selected";
    private static final String PROPERTY_NAME = "name";
    private static final String PROPERTY_CAPITAL = "capital";
    private MapView mapView;
    private MapboxMap mapboxMap;
    private GeoJsonSource source;
    private FeatureCollection featureCollection;
    private LatLng baseLocation;
    private ArrayList attractionsDataList;
    private static List<Feature> markerCoordinates = new ArrayList<>();
    private String attractionType;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Mapbox access token is configured here. This needs to be called either in your application
        // object or in the same activity which contains the mapview.
        Mapbox.getInstance(this, getString(R.string.MAPBOX_ACCESS_KEY));

        // This contains the MapView in XML and needs to be called after the access token is configured.
        setContentView(R.layout.activity_info_window_symbol_layer);

        attractionsDataList = MainActivity.getArrayList();
        attractionType = MainActivity.getAttractionType();


        // Initialize the map view
        mapView = findViewById(R.id.mapView);
        mapView.onCreate(savedInstanceState);
        mapView.getMapAsync(this);
    }

    @Override
    public void onMapReady(@NonNull final MapboxMap mapboxMap) {
        this.mapboxMap = mapboxMap;
        mapboxMap.setStyle(Style.MAPBOX_STREETS, new Style.OnStyleLoaded() {
            @Override
            public void onStyleLoaded(@NonNull Style style) {
                createListOfFeatures();
                loadGeoJsonData();
                //new LoadGeoJsonDataTask(InfoWindowSymbolLayerActivity.this).execute();
                //addClusteredGeoJsonSource(style);
                mapboxMap.addOnMapClickListener(InfoWindowSymbolLayerActivity.this);

                //Set Camera to base location
                int cameraZoom = 7;
                int cameraBearing = 0;
                int cameraTilt = 10;

                CameraPosition position = new CameraPosition.Builder()
                        .target(baseLocation) // Sets the new camera position
                        .zoom(cameraZoom) // Sets the zoom
                        .bearing(cameraBearing) // Rotate the camera
                        .tilt(cameraTilt) // Set the camera tilt
                        .build(); // Creates a CameraPosition from the builder

                //Animate the camera towards base Location
                mapboxMap.animateCamera(CameraUpdateFactory
                        .newCameraPosition(position), 2000);
            }
        });


    }

    @Override
    public boolean onMapClick(@NonNull LatLng point) {
        return handleClickIcon(mapboxMap.getProjection().toScreenLocation(point));
    }

    /**
     * Sets up all of the sources and layers needed for this example
     *
     * @param collection the FeatureCollection to set equal to the globally-declared FeatureCollection
     */
    public void setUpData(final FeatureCollection collection) {
        featureCollection = collection;
        if (mapboxMap != null) {
            Style style = mapboxMap.getStyle();
            if (style != null) {
                setupSource(style);
                setUpImage(style);
                setUpMarkerLayerUnclustered(style);
                setUpMarkerLayersClustered(style);
                setUpInfoWindowLayer(style);
            }
        }
    }

    /**
     * Adds the GeoJSON source to the map
     */
    private void setupSource(@NonNull Style loadedStyle) {
        source = new GeoJsonSource(GEOJSON_SOURCE_ID, featureCollection,
                new GeoJsonOptions()
                        .withCluster(true)
                        .withClusterMaxZoom(14)
                        .withClusterRadius(50));
        loadedStyle.addSource(source);
        //Log.d("debug", "@setupSource: Source: " + source.toString());
    }

    /**
     * Adds the marker image to the map for use as a SymbolLayer icon
     */
    private void setUpImage(@NonNull Style loadedStyle) {
        Log.d("debug", "@setUpImage: Called");
        switch (attractionType) {
            case "Banks":
                Log.d("debug", "@setUpImage: Bank Tile");
                loadedStyle.addImage(MARKER_IMAGE_ID, BitmapFactory.decodeResource(
                        this.getResources(), R.drawable.banks));
                break;
            case "Gas Stations":
                loadedStyle.addImage(MARKER_IMAGE_ID, BitmapFactory.decodeResource(
                        this.getResources(), R.drawable.gas_stations));
                break;
            case "Hotels":
                loadedStyle.addImage(MARKER_IMAGE_ID, BitmapFactory.decodeResource(
                        this.getResources(), R.drawable.hotels));
                break;
            case "Mobile Wallets":
                loadedStyle.addImage(MARKER_IMAGE_ID, BitmapFactory.decodeResource(
                        this.getResources(), R.drawable.mobile_wallets));
                break;
            case "Rental Services":
                loadedStyle.addImage(MARKER_IMAGE_ID, BitmapFactory.decodeResource(
                        this.getResources(), R.drawable.rental_services));
                break;
            case "Restaurants":
                loadedStyle.addImage(MARKER_IMAGE_ID, BitmapFactory.decodeResource(
                        this.getResources(), R.drawable.resturants));
                break;
            case "Shopping Marts":
                loadedStyle.addImage(MARKER_IMAGE_ID, BitmapFactory.decodeResource(
                        this.getResources(), R.drawable.shopping_marts));
                break;
            case "Workshops":
                loadedStyle.addImage(MARKER_IMAGE_ID, BitmapFactory.decodeResource(
                        this.getResources(), R.drawable.workshops));
                break;
            case "Attraction Points":
                loadedStyle.addImage(MARKER_IMAGE_ID, BitmapFactory.decodeResource(
                        this.getResources(), R.drawable.attraction_point));
                break;
            default:
                Log.d("debug", "Match couldn't found");
                break;
        }
    }

    /**
     * Updates the display of data on the map after the FeatureCollection has been modified
     */
    private void refreshSource() {
        if (source != null && featureCollection != null) {
            source.setGeoJson(featureCollection);
        }
    }

    /**
     * Setup a layer with maki icons, eg. west coast city.
     */
    private void setUpMarkerLayerUnclustered(@NonNull Style loadedStyle) {
        Log.d("debug", "@setUpMarkerLayerUnclustered: Called");
        loadedStyle.addLayer(new SymbolLayer(MARKER_LAYER_ID_UNCLUSTERED, GEOJSON_SOURCE_ID)
                .withProperties(
                        iconImage(MARKER_IMAGE_ID),
                        iconAllowOverlap(true)
                ));
    }

    private void setUpMarkerLayersClustered(@NonNull Style loadedStyle) {
        Log.d("debug", "@setUpMarkerLayersClustered: Called");
        // Use the earthquakes GeoJSON source to create three layers: One layer for each cluster category.
        // Each point range gets a different fill color.
        int[][] layers = new int[][] {
                new int[] {150, ContextCompat.getColor(this, R.color.mapboxRed)},
                new int[] {20, ContextCompat.getColor(this, R.color.mapboxGreen)},
                new int[] {0, ContextCompat.getColor(this, R.color.mapbox_blue)}
        };

        for (int i = 0; i < layers.length; i++) {
            //Add clusters' circles
            CircleLayer circles = new CircleLayer("cluster-" + i, "earthquakes");
            circles.setProperties(
                    circleColor(layers[i][1]),
                    circleRadius(18f)
            );
            Expression pointCount = toNumber(get("point_count"));

            // Add a filter to the cluster layer that hides the circles based on "point_count"
            circles.setFilter(
                    i == 0
                            ? all(has("point_count"),
                            gte(pointCount, literal(layers[i][0]))
                    ) : all(has("point_count"),
                            gt(pointCount, literal(layers[i][0])),
                            lt(pointCount, literal(layers[i - 1][0]))
                    )
            );
            loadedStyle.addLayer(circles);
        }

        //Add the count labels
        Log.d("debug", "@setUpMarkerLayersClustered: Adding counts label");
        SymbolLayer count = new SymbolLayer("count", "earthquakes");
        count.setProperties(
                textField(Expression.toString(get("point_count"))),
                textSize(12f),
                textColor(Color.WHITE),
                textIgnorePlacement(true),
                textAllowOverlap(true)
        );
        loadedStyle.addLayer(count);
    }

    /**
     * Setup a layer with Android SDK call-outs
     * <p>
     * name of the feature is used as key for the iconImage
     * </p>
     */
    private void setUpInfoWindowLayer(@NonNull Style loadedStyle) {
        loadedStyle.addLayer(new SymbolLayer(CALLOUT_LAYER_ID, GEOJSON_SOURCE_ID)
                .withProperties(
                        /* show image with id title based on the value of the name feature property */
                        iconImage("{name}"),

                        /* set anchor of icon to bottom-left */
                        iconAnchor(ICON_ANCHOR_BOTTOM),

                        /* all info window and marker image to appear at the same time*/
                        iconAllowOverlap(true),

                        /* offset the info window to be above the marker */
                        iconOffset(new Float[]{-2f, -25f})
                )
                /* add a filter to show only when selected feature property is true */
                .withFilter(eq((get(PROPERTY_SELECTED)), literal(true))));
    }

    /**
     * This method handles click events for SymbolLayer symbols.
     * <p>
     * When a SymbolLayer icon is clicked, we moved that feature to the selected state.
     * </p>
     *
     * @param screenPoint the point on screen clicked
     */
    private boolean handleClickIcon(PointF screenPoint) {
        List<Feature> features = mapboxMap.queryRenderedFeatures(screenPoint, MARKER_LAYER_ID_UNCLUSTERED);
        if (!features.isEmpty()) {
            String name = features.get(0).getStringProperty(PROPERTY_NAME);
            List<Feature> featureList = featureCollection.features();
            for (int i = 0; i < featureList.size(); i++) {
                if (featureList.get(i).getStringProperty(PROPERTY_NAME).equals(name)) {
                    if (featureSelectStatus(i)) {
                        setFeatureSelectState(featureList.get(i), false);
                    } else {
                        setSelected(i);
                    }
                }
            }
            return true;
        } else {
            return false;
        }
    }

    /**
     * Set a feature selected state.
     *
     * @param index the index of selected feature
     */
    private void setSelected(int index) {
        Feature feature = featureCollection.features().get(index);
        setFeatureSelectState(feature, true);
        refreshSource();
    }

    /**
     * Selects the state of a feature
     *
     * @param feature the feature to be selected.
     */
    private void setFeatureSelectState(Feature feature, boolean selectedState) {
        feature.properties().addProperty(PROPERTY_SELECTED, selectedState);
        refreshSource();
    }

    /**
     * Checks whether a Feature's boolean "selected" property is true or false
     *
     * @param index the specific Feature's index position in the FeatureCollection's list of Features.
     * @return true if "selected" is true. False if the boolean property is false.
     */
    private boolean featureSelectStatus(int index) {
        if (featureCollection == null) {
            return false;
        }
        return featureCollection.features().get(index).getBooleanProperty(PROPERTY_SELECTED);
    }

    /**
     * Invoked when the bitmaps have been generated from a view.
     */
    public void setImageGenResults(HashMap<String, Bitmap> imageMap) {
        if (mapboxMap != null) {
            Style style = mapboxMap.getStyle();
            if (style != null) {
                // calling addImages is faster as separate addImage calls for each bitmap.
                style.addImages(imageMap);
            }
        }
    }

    private void loadGeoJsonData(){
        featureCollection = FeatureCollection.fromFeatures(markerCoordinates);
        if(featureCollection != null){
            for (Feature singleFeature : featureCollection.features()) {
                singleFeature.addBooleanProperty(PROPERTY_SELECTED, false);
            }
            setUpData(featureCollection);
            new GenerateViewIconTask(InfoWindowSymbolLayerActivity.this).execute(featureCollection);
        }else {
            Log.d("debug", "FeatureCollection is null");
            return;
        }
    }
    /**
     * AsyncTask to generate Bitmap from Views to be used as iconImage in a SymbolLayer.
     * <p>
     * Call be optionally be called to update the underlying data source after execution.
     * </p>
     * <p>
     * Generating Views on background thread since we are not going to be adding them to the view hierarchy.
     * </p>
     */
    private static class GenerateViewIconTask extends AsyncTask<FeatureCollection, Void, HashMap<String, Bitmap>> {

        private final HashMap<String, View> viewMap = new HashMap<>();
        private final WeakReference<InfoWindowSymbolLayerActivity> activityRef;
        private final boolean refreshSource;

        GenerateViewIconTask(InfoWindowSymbolLayerActivity activity, boolean refreshSource) {
            this.activityRef = new WeakReference<>(activity);
            this.refreshSource = refreshSource;
        }

        GenerateViewIconTask(InfoWindowSymbolLayerActivity activity) {
            this(activity, false);
        }

        @SuppressWarnings("WrongThread")
        @Override
        protected HashMap<String, Bitmap> doInBackground(FeatureCollection... params) {
            InfoWindowSymbolLayerActivity activity = activityRef.get();
            if (activity != null) {
                HashMap<String, Bitmap> imagesMap = new HashMap<>();
                LayoutInflater inflater = LayoutInflater.from(activity);

                FeatureCollection featureCollection = params[0];
                Log.d("debug", "@doInBackground: Called for setting data on symbolLayerInfo");

                for (Feature feature : featureCollection.features()) {
                    try {
                        BubbleLayout bubbleLayout = (BubbleLayout)
                                inflater.inflate(R.layout.symbol_layer_info_window_layout_callout, null);

                        if (feature != null) {
                            Log.d("debug", "@doInBackground: Feature: " + feature.toString());
                        } else {
                            Log.d("debug", "@doInBackground: Feature is null");
                        }
                        String name = feature.getStringProperty(PROPERTY_NAME);
                        TextView titleTextView = bubbleLayout.findViewById(R.id.info_window_title);
                        titleTextView.setText(name);

                        //String style = feature.getStringProperty(PROPERTY_CAPITAL);
                        TextView descriptionTextView = bubbleLayout.findViewById(R.id.info_window_description);
                        descriptionTextView.setText("Capital");

                        int measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
                        bubbleLayout.measure(measureSpec, measureSpec);

                        int measuredWidth = bubbleLayout.getMeasuredWidth();

                        bubbleLayout.setArrowPosition(measuredWidth / 2 - 5);

                        Bitmap bitmap = SymbolGenerator.generate(bubbleLayout);
                        imagesMap.put(name, bitmap);
                        viewMap.put(name, bubbleLayout);
                    } catch (Exception e) {
                        Log.d("debug", "@doInBackground: Exception: " + e.getMessage());
                    }
                }

                return imagesMap;
            } else {
                return null;
            }
        }

        @Override
        protected void onPostExecute(HashMap<String, Bitmap> bitmapHashMap) {
            super.onPostExecute(bitmapHashMap);
            InfoWindowSymbolLayerActivity activity = activityRef.get();
            if (activity != null && bitmapHashMap != null) {
                activity.setImageGenResults(bitmapHashMap);
                if (refreshSource) {
                    activity.refreshSource();
                }
            }
            //Toast.makeText(activity,"text", Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * Utility class to generate Bitmaps for Symbol.
     */
    private static class SymbolGenerator {

        /**
         * Generate a Bitmap from an Android SDK View.
         *
         * @param view the View to be drawn to a Bitmap
         * @return the generated bitmap
         */
        static Bitmap generate(@NonNull View view) {
            int measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
            view.measure(measureSpec, measureSpec);

            int measuredWidth = view.getMeasuredWidth();
            int measuredHeight = view.getMeasuredHeight();

            view.layout(0, 0, measuredWidth, measuredHeight);
            Bitmap bitmap = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888);
            bitmap.eraseColor(Color.TRANSPARENT);
            Canvas canvas = new Canvas(bitmap);
            view.draw(canvas);
            return bitmap;
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        mapView.onStart();
    }

    @Override
    public void onResume() {
        super.onResume();
        mapView.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();
        mapView.onPause();
    }

    @Override
    protected void onStop() {
        super.onStop();
        mapView.onStop();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        mapView.onSaveInstanceState(outState);
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        mapView.onLowMemory();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mapboxMap != null) {
            mapboxMap.removeOnMapClickListener(this);
        }
        markerCoordinates.clear();
        mapView.onDestroy();
    }

    private void createListOfFeatures() {
        Log.d("debug", "@createListOfFeatures: Called");
        Gson gson = new Gson();
        String jsonString;
        String name;
        double lat = 30.375320;
        double lng = 69.345116;

        Iterator iterator = attractionsDataList.iterator();

        //Building list of Markers co-ordinates to be placed on map
        while (iterator.hasNext()) {

            jsonString = gson.toJson(iterator.next());
            try {
                JSONObject jsonObject = new JSONObject(jsonString);

                //Getting data from jsonObject
                name = jsonObject.getString("name");
                lat = jsonObject.getJSONObject("Location").getDouble("latitude");
                lng = jsonObject.getJSONObject("Location").getDouble("longitude");
                //attractionType = jsonObject.getString("class");

                //Creating feature from data extracted form jsonObject
                Feature feature = Feature.fromGeometry(Point.fromLngLat(lng, lat));
                feature.addStringProperty(PROPERTY_NAME, name);
                feature.addBooleanProperty(PROPERTY_SELECTED, false);

                //Adding feature to the list of Features
                markerCoordinates.add(feature);
                Log.d("debug", "@createListOfFeatures:" + feature.toString());
            } catch (Exception e) {
                Log.d("debug", "@createListOfFeatures: Exception: " + e.getMessage());
            }
        }
        //Setting base location
        baseLocation = new LatLng(lat, lng);
        Log.d("debug", "@createListOfFeatures: attractionType: " + attractionType);

    }
}

预期结果是:Expected 实际结果是:Actual 带有以下日志:

找不到图层“ cluster-0”的来源 找不到图层“ cluster-1”的来源 找不到图层“ cluster-3”的来源

1 个答案:

答案 0 :(得分:0)

日志告诉您正在创建的CircleLayers的来源不存在。检查earthquakes是否为GeoJsonSource且已被初始化。 最终,您可以在circles.setSourceLayer("NAME_OF_THE_SOURCE")之后添加CircleLayer circles = new CircleLayer("cluster-" + i, "earthquakes");,其中"NAME_OF_THE_SOURCE"是源图层的名称。