シンボルレイヤアイコン
<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:mapbox="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"android:animateLayoutChanges="true"> <com.mapbox.mapboxsdk.maps.MapViewandroid:id="@+id/mapView"android:layout_width="match_parent"android:layout_height="match_parent"mapbox:mapbox_cameraTargetLat="37.753971"mapbox:mapbox_cameraTargetLng="-122.439004"mapbox:mapbox_cameraTilt="25"mapbox:mapbox_cameraZoom="11"/> <androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/rv_on_top_of_map"android:layout_width="match_parent"android:layout_height="178dp"android:layout_gravity="bottom"android:layout_marginBottom="8dp"android:visibility="gone" /> </FrameLayout>
SymbolLayerMapillaryActivity.java
package com.mapbox.mapboxandroiddemo.examples.labs; import android.animation.Animator;import android.animation.AnimatorSet;import android.animation.TypeEvaluator;import android.animation.ValueAnimator;import android.annotation.SuppressLint;import android.content.Context;import android.graphics.Bitmap;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.PointF;import android.graphics.PorterDuff;import android.graphics.PorterDuffXfermode;import android.graphics.Rect;import android.os.AsyncTask;import android.os.Bundle;import android.os.Handler;import androidx.annotation.IntDef;import androidx.annotation.NonNull;import androidx.interpolator.view.animation.FastOutSlowInInterpolator;import androidx.appcompat.app.AppCompatActivity;import androidx.cardview.widget.CardView;import androidx.recyclerview.widget.DefaultItemAnimator;import androidx.recyclerview.widget.LinearLayoutManager;import androidx.recyclerview.widget.PagerSnapHelper;import androidx.recyclerview.widget.RecyclerView;import androidx.recyclerview.widget.SnapHelper;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.ImageView;import android.widget.TextView;import android.widget.Toast; import com.mapbox.geojson.Feature;import com.mapbox.geojson.FeatureCollection;import com.mapbox.geojson.Point;import com.mapbox.mapboxandroiddemo.R;import com.mapbox.mapboxsdk.Mapbox;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.Layer;import com.mapbox.mapboxsdk.style.layers.LineLayer;import com.mapbox.mapboxsdk.style.layers.Property;import com.mapbox.mapboxsdk.style.layers.SymbolLayer;import com.mapbox.mapboxsdk.style.sources.GeoJsonOptions;import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;import com.mapbox.mapboxsdk.style.sources.Source;import com.mapbox.mapboxsdk.style.sources.TileSet;import com.mapbox.mapboxsdk.style.sources.VectorSource;import com.squareup.picasso.Picasso; import java.io.InputStream;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.ref.WeakReference;import java.nio.charset.Charset;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map; import okhttp3.OkHttpClient;import okhttp3.Request;import okhttp3.Response;import timber.log.Timber; import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;import static com.mapbox.mapboxsdk.style.expressions.Expression.all;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.gte;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.match;import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;import static com.mapbox.mapboxsdk.style.expressions.Expression.toNumber;import static com.mapbox.mapboxsdk.style.expressions.Expression.zoom;import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleColor;import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleOpacity;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.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.lineCap;import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineColor;import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineJoin;import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineOpacity;import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineWidth;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.textOffset;import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textSize;import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility; public class SymbolLayerMapillaryActivity extends AppCompatActivity implements OnMapReadyCallback,MapboxMap.OnMapClickListener {private static final String SOURCE_ID = "mapbox.poi";private static final String MAKI_LAYER_ID = "mapbox.poi.maki";private static final String LOADING_LAYER_ID = "mapbox.poi.loading";private static final String CALLOUT_LAYER_ID = "mapbox.poi.callout"; private static final String PROPERTY_SELECTED = "selected";private static final String PROPERTY_LOADING = "loading";private static final String PROPERTY_LOADING_PROGRESS = "loading_progress";private static final String PROPERTY_TITLE = "title";private static final String PROPERTY_FAVOURITE = "favourite";private static final String PROPERTY_DESCRIPTION = "description";private static final String PROPERTY_POI = "poi";private static final String PROPERTY_STYLE = "style"; private static final long CAMERA_ANIMATION_TIME = 1950;private static final float LOADING_CIRCLE_RADIUS = 60;private static final int LOADING_PROGRESS_STEPS = 25; //number of steps in a progress animationprivate static final int LOADING_STEP_DURATION = 50; //duration between each step private MapView mapView;private MapboxMap mapboxMap;private RecyclerView recyclerView; private GeoJsonSource source;private FeatureCollection featureCollection;private HashMap<String, View> viewMap;private AnimatorSet animatorSet; private LoadMapillaryDataTask loadMapillaryDataTask; @ActivityStepprivate int currentStep; @Retention(RetentionPolicy.SOURCE)@IntDef( {STEP_INITIAL, STEP_LOADING, STEP_READY})public @interface ActivityStep {} private static final int STEP_INITIAL = 0;private static final int STEP_LOADING = 1;private static final int STEP_READY = 2; private static final Map<Integer, Double> stepZoomMap = new HashMap<>(); static {stepZoomMap.put(STEP_INITIAL, 11.0);stepZoomMap.put(STEP_LOADING, 13.5);stepZoomMap.put(STEP_READY, 18.0);} @Overrideprotected 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.access_token)); // This contains the MapView in XML and needs to be called after the access token is configured.setContentView(R.layout.activity_symbol_layer_mapillary); recyclerView = findViewById(R.id.rv_on_top_of_map); // Initialize the map viewmapView = findViewById(R.id.mapView);mapView.onCreate(savedInstanceState);mapView.getMapAsync(this);} @Overridepublic void onMapReady(@NonNull final MapboxMap mapboxMap) {this.mapboxMap = mapboxMap;mapboxMap.setStyle(Style.DARK, new Style.OnStyleLoaded() {@Overridepublic void onStyleLoaded(@NonNull Style style) {mapboxMap.getUiSettings().setCompassEnabled(false);mapboxMap.getUiSettings().setLogoEnabled(false);mapboxMap.getUiSettings().setAttributionEnabled(false);new LoadPoiDataTask(SymbolLayerMapillaryActivity.this).execute();mapboxMap.addOnMapClickListener(SymbolLayerMapillaryActivity.this);}});} @Overridepublic boolean onMapClick(@NonNull LatLng point) {PointF screenPoint = mapboxMap.getProjection().toScreenLocation(point);List<Feature> features = mapboxMap.queryRenderedFeatures(screenPoint, CALLOUT_LAYER_ID);if (!features.isEmpty()) {// we received a click event on the callout layerFeature feature = features.get(0);PointF symbolScreenPoint = mapboxMap.getProjection().toScreenLocation(convertToLatLng(feature));handleClickCallout(feature, screenPoint, symbolScreenPoint);} else {// we didn't find a click event on callout layer, try clicking maki layerreturn handleClickIcon(screenPoint);}return true;} public void setupData(final FeatureCollection collection) {if (mapboxMap == null) {return;}featureCollection = collection;mapboxMap.getStyle(new Style.OnStyleLoaded() {@Overridepublic void onStyleLoaded(@NonNull Style style) {setupSource(style);setupMakiLayer(style);setupLoadingLayer(style);setupCalloutLayer(style);setupRecyclerView();hideLabelLayers(style);setupMapillaryTiles(style);}});} private void setupSource(@NonNull Style loadedMapStyle) {source = new GeoJsonSource(SOURCE_ID, featureCollection);loadedMapStyle.addSource(source);} private void refreshSource() {if (source != null && featureCollection != null) {source.setGeoJson(featureCollection);}} /*** Setup a layer with maki icons, eg. restaurant.*/private void setupMakiLayer(@NonNull Style loadedMapStyle) {loadedMapStyle.addLayer(new SymbolLayer(MAKI_LAYER_ID, SOURCE_ID).withProperties(/* show maki icon based on the value of poi feature property* https://www.mapbox.com/maki-icons/*/iconImage("{poi}-15"), /* allows show all icons */iconAllowOverlap(true), /* when feature is in selected state, grow icon */iconSize(match(Expression.toString(get(PROPERTY_SELECTED)), literal(1.0f),stop("true", 1.5f)))));} /*** Setup layer indicating that there is an ongoing progress.*/private void setupLoadingLayer(@NonNull Style loadedMapStyle) {loadedMapStyle.addLayerBelow(new CircleLayer(LOADING_LAYER_ID, SOURCE_ID).withProperties(circleRadius(interpolate(exponential(1), get(PROPERTY_LOADING_PROGRESS), getLoadingAnimationStops())),circleColor(Color.GRAY),circleOpacity(0.6f)).withFilter(eq(get(PROPERTY_LOADING), literal(true))), MAKI_LAYER_ID);} private Expression.Stop[] getLoadingAnimationStops() {List<Expression.Stop> stops = new ArrayList<>();for (int i = 0; i < LOADING_PROGRESS_STEPS; i++) {stops.add(stop(i, LOADING_CIRCLE_RADIUS * i / LOADING_PROGRESS_STEPS));} return stops.toArray(new Expression.Stop[LOADING_PROGRESS_STEPS]);} /*** Setup a layer with Android SDK call-outs* <p>* title of the feature is used as key for the iconImage* </p>*/private void setupCalloutLayer(@NonNull Style loadedMapStyle) {loadedMapStyle.addLayer(new SymbolLayer(CALLOUT_LAYER_ID, SOURCE_ID).withProperties(/* show image with id title based on the value of the title feature property */iconImage("{title}"), /* set anchor of icon to bottom-left */iconAnchor(Property.ICON_ANCHOR_BOTTOM_LEFT), /* offset icon slightly to match bubble layout */iconOffset(new Float[] {-20.0f, -10.0f})) /* add a filter to show only when selected feature property is true */.withFilter(eq((get(PROPERTY_SELECTED)), literal(true))));} private void setupRecyclerView() {RecyclerView.Adapter adapter = new LocationRecyclerViewAdapter(this, featureCollection);final LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);recyclerView.setLayoutManager(layoutManager);recyclerView.setItemAnimator(new DefaultItemAnimator());recyclerView.setAdapter(adapter);recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {@Overridepublic void onScrollStateChanged(RecyclerView recyclerView, int newState) {super.onScrollStateChanged(recyclerView, newState);if (newState == SCROLL_STATE_IDLE) {int index = layoutManager.findFirstVisibleItemPosition();setSelected(index, false);}}});SnapHelper snapHelper = new PagerSnapHelper();snapHelper.attachToRecyclerView(recyclerView);} private void hideLabelLayers(@NonNull Style style) {String id;for (Layer layer : style.getLayers()) {id = layer.getId();if (id.startsWith("place") || id.startsWith("poi") || id.startsWith("marine") || id.startsWith("road-label")) {layer.setProperties(visibility(Property.NONE));}}} private void setupMapillaryTiles(@NonNull Style loadedMapStyle) {loadedMapStyle.addSource(MapillaryTiles.createSource());loadedMapStyle.addLayerBelow(MapillaryTiles.createLineLayer(), LOADING_LAYER_ID);} /*** This method handles click events for callout symbols.* <p>* It creates a hit rectangle based on the the textView, offsets that rectangle to the location* of the symbol on screen and hit tests that with the screen point.* </p>** @param feature the feature that was clicked* @param screenPoint the point on screen clicked* @param symbolScreenPoint the point of the symbol on screen*/private void handleClickCallout(Feature feature, PointF screenPoint, PointF symbolScreenPoint) {View view = viewMap.get(feature.getStringProperty(PROPERTY_TITLE));View textContainer = view.findViewById(R.id.text_container); // create hitbox for textViewRect hitRectText = new Rect();textContainer.getHitRect(hitRectText); // move hitbox to location of symbolhitRectText.offset((int) symbolScreenPoint.x, (int) symbolScreenPoint.y); // offset vertically to match anchor behaviourhitRectText.offset(0, -view.getMeasuredHeight()); // hit test if clicked point is in textview hitboxif (hitRectText.contains((int) screenPoint.x, (int) screenPoint.y)) {// user clicked on textString callout = feature.getStringProperty("call-out");Toast.makeText(this, callout, Toast.LENGTH_LONG).show();} else {// user clicked on iconList<Feature> featureList = featureCollection.features();for (int i = 0; i < featureList.size(); i++) {if (featureList.get(i).getStringProperty(PROPERTY_TITLE).equals(feature.getStringProperty(PROPERTY_TITLE))) {toggleFavourite(i);}}}} /*** This method handles click events for maki symbols.* <p>* When a maki symbol 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, MAKI_LAYER_ID);if (!features.isEmpty()) {String title = features.get(0).getStringProperty(PROPERTY_TITLE);List<Feature> featureList = featureCollection.features();for (int i = 0; i < featureList.size(); i++) {if (featureList.get(i).getStringProperty(PROPERTY_TITLE).equals(title)) {setSelected(i, true);}} return true;}return false;} /*** Set a feature selected state with the ability to scroll the RecycleViewer to the provided index.** @param index the index of selected feature* @param withScroll indicates if the recyclerView position should be updated*/private void setSelected(int index, boolean withScroll) {if (recyclerView.getVisibility() == View.GONE) {recyclerView.setVisibility(View.VISIBLE);} deselectAll(false); Feature feature = featureCollection.features().get(index);selectFeature(feature);animateCameraToSelection(feature);refreshSource();loadMapillaryData(feature); if (withScroll) {recyclerView.scrollToPosition(index);}} /*** Deselects the state of all the features*/private void deselectAll(boolean hideRecycler) {for (Feature feature : featureCollection.features()) {feature.properties().addProperty(PROPERTY_SELECTED, false);} if (hideRecycler) {recyclerView.setVisibility(View.GONE);}} /*** Selects the state of a feature** @param feature the feature to be selected.*/private void selectFeature(Feature feature) {feature.properties().addProperty(PROPERTY_SELECTED, true);} private Feature getSelectedFeature() {if (featureCollection != null) {for (Feature feature : featureCollection.features()) {if (feature.getBooleanProperty(PROPERTY_SELECTED)) {return feature;}}} return null;} /*** Animate camera to a feature.** @param feature the feature to animate to*/private void animateCameraToSelection(Feature feature, double newZoom) {CameraPosition cameraPosition = mapboxMap.getCameraPosition(); if (animatorSet != null) {animatorSet.cancel();} animatorSet = new AnimatorSet();animatorSet.playTogether(createLatLngAnimator(cameraPosition.target, convertToLatLng(feature)),createZoomAnimator(cameraPosition.zoom, newZoom),createBearingAnimator(cameraPosition.bearing, feature.getNumberProperty("bearing").doubleValue()),createTiltAnimator(cameraPosition.tilt, feature.getNumberProperty("tilt").doubleValue()));animatorSet.start();} private void animateCameraToSelection(Feature feature) {double zoom = feature.getNumberProperty("zoom").doubleValue();animateCameraToSelection(feature, zoom);} private void loadMapillaryData(Feature feature) {if (loadMapillaryDataTask != null) {loadMapillaryDataTask.cancel(true);} loadMapillaryDataTask = new LoadMapillaryDataTask(this,mapboxMap, Picasso.with(getApplicationContext()), new Handler(), feature);loadMapillaryDataTask.execute(50);} /*** Set the favourite state of a feature based on the index.** @param index the index of the feature to favourite/de-favourite*/private void toggleFavourite(int index) {Feature feature = featureCollection.features().get(index);String title = feature.getStringProperty(PROPERTY_TITLE);boolean currentState = feature.getBooleanProperty(PROPERTY_FAVOURITE);feature.properties().addProperty(PROPERTY_FAVOURITE, !currentState);View view = viewMap.get(title); ImageView imageView = view.findViewById(R.id.logoView);imageView.setImageResource(currentState ? R.drawable.ic_favorite : R.drawable.ic_favorite_border);Bitmap bitmap = SymbolGenerator.generate(view);mapboxMap.getStyle(new Style.OnStyleLoaded() {@Overridepublic void onStyleLoaded(@NonNull Style style) {style.addImage(title, bitmap);refreshSource();}});} /*** Invoked when the bitmaps have been generated from a view.*/public void setImageGenResults(HashMap<String, View> viewMap, HashMap<String, Bitmap> imageMap) {mapboxMap.getStyle(new Style.OnStyleLoaded() {@Overridepublic void onStyleLoaded(@NonNull Style style) {// calling addImages is faster as separate addImage calls for each bitmap.style.addImages(imageMap);}});// need to store reference to views to be able to use them as hitboxes for click events.SymbolLayerMapillaryActivity.this.viewMap = viewMap;} private void setActivityStep(@ActivityStep int activityStep) {Feature selectedFeature = getSelectedFeature();double zoom = stepZoomMap.get(activityStep);animateCameraToSelection(selectedFeature, zoom); currentStep = activityStep;} @Overrideprotected void onStart() {super.onStart();mapView.onStart();} @Overridepublic void onResume() {super.onResume();mapView.onResume();} @Overridepublic void onPause() {super.onPause();mapView.onPause();} @Overrideprotected void onStop() {super.onStop(); if (loadMapillaryDataTask != null) {loadMapillaryDataTask.cancel(true);}mapView.onStop();} @Overrideprotected void onSaveInstanceState(Bundle outState) {super.onSaveInstanceState(outState);mapView.onSaveInstanceState(outState);} @Overridepublic void onLowMemory() {super.onLowMemory();mapView.onLowMemory();} @Overrideprotected void onDestroy() {super.onDestroy();if (mapboxMap != null) {mapboxMap.removeOnMapClickListener(this);}mapView.onDestroy();} @Overridepublic void onBackPressed() {if (currentStep == STEP_LOADING || currentStep == STEP_READY) {if (loadMapillaryDataTask != null) {loadMapillaryDataTask.cancel(true);}setActivityStep(STEP_INITIAL);deselectAll(true);refreshSource();} else {super.onBackPressed();}} private LatLng convertToLatLng(Feature feature) {Point symbolPoint = (Point) feature.geometry();return new LatLng(symbolPoint.latitude(), symbolPoint.longitude());} private Animator createLatLngAnimator(LatLng currentPosition, LatLng targetPosition) {ValueAnimator latLngAnimator = ValueAnimator.ofObject(new LatLngEvaluator(), currentPosition, targetPosition);latLngAnimator.setDuration(CAMERA_ANIMATION_TIME);latLngAnimator.setInterpolator(new FastOutSlowInInterpolator());latLngAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mapboxMap.moveCamera(CameraUpdateFactory.newLatLng((LatLng) animation.getAnimatedValue()));}});return latLngAnimator;} private Animator createZoomAnimator(double currentZoom, double targetZoom) {ValueAnimator zoomAnimator = ValueAnimator.ofFloat((float) currentZoom, (float) targetZoom);zoomAnimator.setDuration(CAMERA_ANIMATION_TIME);zoomAnimator.setInterpolator(new FastOutSlowInInterpolator());zoomAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mapboxMap.moveCamera(CameraUpdateFactory.zoomTo((Float) animation.getAnimatedValue()));}});return zoomAnimator;} private Animator createBearingAnimator(double currentBearing, double targetBearing) {ValueAnimator bearingAnimator = ValueAnimator.ofFloat((float) currentBearing, (float) targetBearing);bearingAnimator.setDuration(CAMERA_ANIMATION_TIME);bearingAnimator.setInterpolator(new FastOutSlowInInterpolator());bearingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mapboxMap.moveCamera(CameraUpdateFactory.bearingTo((Float) animation.getAnimatedValue()));}});return bearingAnimator;} private Animator createTiltAnimator(double currentTilt, double targetTilt) {ValueAnimator tiltAnimator = ValueAnimator.ofFloat((float) currentTilt, (float) targetTilt);tiltAnimator.setDuration(CAMERA_ANIMATION_TIME);tiltAnimator.setInterpolator(new FastOutSlowInInterpolator());tiltAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mapboxMap.moveCamera(CameraUpdateFactory.tiltTo((Float) animation.getAnimatedValue()));}});return tiltAnimator;} /*** Helper class to evaluate LatLng objects with a ValueAnimator*/private static class LatLngEvaluator implements TypeEvaluator<LatLng> { private final LatLng latLng = new LatLng(); @Overridepublic LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) {latLng.setLatitude(startValue.getLatitude()+ ((endValue.getLatitude() - startValue.getLatitude()) * fraction));latLng.setLongitude(startValue.getLongitude()+ ((endValue.getLongitude() - startValue.getLongitude()) * fraction));return latLng;}} /*** AsyncTask to load data from the assets folder.*/private static class LoadPoiDataTask extends AsyncTask<Void, Void, FeatureCollection> { private final WeakReference<SymbolLayerMapillaryActivity> activityRef; LoadPoiDataTask(SymbolLayerMapillaryActivity activity) {this.activityRef = new WeakReference<>(activity);} @Overrideprotected FeatureCollection doInBackground(Void... params) {SymbolLayerMapillaryActivity activity = activityRef.get(); if (activity == null) {return null;} String geoJson = loadGeoJsonFromAsset(activity, "sf_poi.geojson");return FeatureCollection.fromJson(geoJson);} @Overrideprotected void onPostExecute(FeatureCollection featureCollection) {super.onPostExecute(featureCollection);SymbolLayerMapillaryActivity activity = activityRef.get();if (featureCollection == null || activity == null) {return;}activity.setupData(featureCollection);new GenerateViewIconTask(activity).execute(featureCollection);} static String loadGeoJsonFromAsset(Context context, String filename) {try {// Load GeoJSON file from local asset folderInputStream is = context.getAssets().open(filename);int size = is.available();byte[] buffer = new byte[size];is.read(buffer);is.close();return new String(buffer, Charset.forName("UTF-8"));} catch (Exception exception) {throw new RuntimeException(exception);}}} /*** 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<SymbolLayerMapillaryActivity> activityRef;private final boolean refreshSource; GenerateViewIconTask(SymbolLayerMapillaryActivity activity, boolean refreshSource) {this.activityRef = new WeakReference<>(activity);this.refreshSource = refreshSource;} GenerateViewIconTask(SymbolLayerMapillaryActivity activity) {this(activity, false);} @SuppressWarnings("WrongThread")@Overrideprotected HashMap<String, Bitmap> doInBackground(FeatureCollection... params) {SymbolLayerMapillaryActivity activity = activityRef.get();if (activity != null) {HashMap<String, Bitmap> imagesMap = new HashMap<>();LayoutInflater inflater = LayoutInflater.from(activity);FeatureCollection featureCollection = params[0]; for (Feature feature : featureCollection.features()) {View view = inflater.inflate(R.layout.mapillary_layout_callout, null); String name = feature.getStringProperty(PROPERTY_TITLE);TextView titleTv = view.findViewById(R.id.title);titleTv.setText(name); String style = feature.getStringProperty(PROPERTY_STYLE);TextView styleTv = view.findViewById(R.id.style);styleTv.setText(style); boolean favourite = feature.getBooleanProperty(PROPERTY_FAVOURITE);ImageView imageView = view.findViewById(R.id.logoView);imageView.setImageResource(favourite ? R.drawable.ic_favorite : R.drawable.ic_favorite_border); Bitmap bitmap = SymbolGenerator.generate(view);imagesMap.put(name, bitmap);viewMap.put(name, view);} return imagesMap;} else {return null;}} @Overrideprotected void onPostExecute(HashMap<String, Bitmap> bitmapHashMap) {super.onPostExecute(bitmapHashMap);SymbolLayerMapillaryActivity activity = activityRef.get();if (activity != null && bitmapHashMap != null) { activity.setImageGenResults(viewMap, bitmapHashMap);if (refreshSource) {activity.refreshSource();}}}} /*** Async task which fetches pictures from around the POI using Mapillary services.* https://www.mapillary.com/developer/api-documentation/*/private static class LoadMapillaryDataTask extends AsyncTask<Integer, Void, MapillaryDataLoadResult> { static final String URL_IMAGE_PLACEHOLDER = "https://d1cuyjsrcm0gby.cloudfront.net/%s/thumb-320.jpg";static final String KEY_UNIQUE_FEATURE = "key";static final String TOKEN_UNIQUE_FEATURE = "{" + KEY_UNIQUE_FEATURE + "}";static final String ID_SOURCE = "cluster_source";static final String ID_LAYER_UNCLUSTERED = "unclustered_layer";static final int IMAGE_SIZE = 128;static final String API_URL = "https://a.mapillary.com/v3/images/"+ "?lookat=%f,%f&closeto=%f,%f&radius=%d"+ "&client_id=bjgtc1FDTnFPaXpxeTZuUDNabmJ5dzozOGE1ODhkMmEyYTkyZTI4"; private WeakReference<SymbolLayerMapillaryActivity> activityRef;private MapboxMap map;private Picasso picasso;private final Handler progressHandler;private int loadingProgress;private boolean loadingIncrease = true;private Feature feature; public LoadMapillaryDataTask(SymbolLayerMapillaryActivity activity, MapboxMap map, Picasso picasso,Handler progressHandler, Feature feature) {this.activityRef = new WeakReference<>(activity);this.map = map;this.picasso = picasso;this.progressHandler = progressHandler;this.feature = feature;} @Overrideprotected void onPreExecute() {super.onPreExecute();loadingProgress = 0;setLoadingState(true, false);} @Overrideprotected MapillaryDataLoadResult doInBackground(Integer... radius) {progressHandler.post(progressRunnable);try {Thread.sleep(2500); //ensure loading visualisation} catch (InterruptedException exception) {exception.printStackTrace();}OkHttpClient okHttpClient = new OkHttpClient();try {Point poiPosition = (Point) feature.geometry(); @SuppressLint("DefaultLocale") Request request = new Request.Builder().url(String.format(API_URL,poiPosition.longitude(), poiPosition.latitude(),poiPosition.longitude(), poiPosition.latitude(),radius[0])).build(); Response response = okHttpClient.newCall(request).execute();FeatureCollection featureCollection = FeatureCollection.fromJson(response.body().string());MapillaryDataLoadResult mapillaryDataLoadResult = new MapillaryDataLoadResult(featureCollection);for (Feature feature : featureCollection.features()) {String imageId = feature.getStringProperty(KEY_UNIQUE_FEATURE);String imageUrl = String.format(URL_IMAGE_PLACEHOLDER, imageId);Bitmap bitmap = picasso.load(imageUrl).resize(IMAGE_SIZE, IMAGE_SIZE).get(); //cropping bitmap to be circularbitmap = getCroppedBitmap(bitmap); mapillaryDataLoadResult.add(feature, bitmap);}return mapillaryDataLoadResult; } catch (Exception exception) {Timber.e(exception);}return null;} @Overrideprotected void onPostExecute(MapillaryDataLoadResult mapillaryDataLoadResult) {super.onPostExecute(mapillaryDataLoadResult);setLoadingState(false, true);if (mapillaryDataLoadResult == null) {SymbolLayerMapillaryActivity activity = activityRef.get();if (activity != null) {Toast.makeText(activity, "Error. Unable to load Mapillary data.", Toast.LENGTH_LONG).show();}return;} FeatureCollection featureCollection = mapillaryDataLoadResult.mapillaryFeatureCollection; Map<Feature, Bitmap> bitmapMap = mapillaryDataLoadResult.bitmapHashMap;for (Map.Entry<Feature, Bitmap> featureBitmapEntry : bitmapMap.entrySet()) {Feature feature = featureBitmapEntry.getKey();String key = feature.getStringProperty(KEY_UNIQUE_FEATURE);map.getStyle().addImage(key, featureBitmapEntry.getValue());} GeoJsonSource mapillarySource = (GeoJsonSource) map.getStyle().getSource(ID_SOURCE);if (mapillarySource == null) {map.getStyle().addSource(new GeoJsonSource(ID_SOURCE, featureCollection, new GeoJsonOptions().withCluster(true).withClusterMaxZoom(17).withClusterRadius(IMAGE_SIZE / 3))); // unclusteredmap.getStyle().addLayerBelow(new SymbolLayer(ID_LAYER_UNCLUSTERED, ID_SOURCE).withProperties(iconImage(TOKEN_UNIQUE_FEATURE),iconAllowOverlap(true),iconSize(interpolate(exponential(1f), zoom(),stop(12, 0.0f),stop(15, 0.8f),stop(16, 1.1f),stop(17, 1.4f),stop(18, 1.7f)))), MAKI_LAYER_ID); // clusteredint[][] layers = new int[][] {new int[] {20, Color.RED},new int[] {10, Color.BLUE},new int[] {0, Color.GREEN}}; for (int i = 0; i < layers.length; i++) { Expression pointCount = toNumber(Expression.get("point_count")); //Add cluster circlesCircleLayer clusterLayer = new CircleLayer("cluster-" + i, ID_SOURCE);clusterLayer.setProperties(circleColor(layers[i][1]),circleRadius(interpolate(exponential(1f),zoom(),stop(12, 10f),stop(14, 16f),stop(15, 18f),stop(16, 20f))),circleOpacity(0.6f));clusterLayer.setMaxZoom(17f); // Add a filter to the cluster layer that hides the circles based on "point_count"clusterLayer.setFilter(i == 0? gte(pointCount, literal(layers[i][0])) :all(gte(pointCount, literal(layers[i][0])),lt(pointCount, literal(layers[i - 1][0]))));map.getStyle().addLayerBelow(clusterLayer, MAKI_LAYER_ID);} //Add the count labelsSymbolLayer count = new SymbolLayer("count", ID_SOURCE);count.setProperties(textField("{point_count}"),textSize(8f),textOffset(new Float[] {0.0f, 0.0f}),textColor(Color.WHITE),textIgnorePlacement(true));map.getStyle().addLayerBelow(count, MAKI_LAYER_ID);} else {mapillarySource.setGeoJson(featureCollection);}} static Bitmap getCroppedBitmap(Bitmap bitmap) {Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),bitmap.getHeight(), Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(output); final int color = 0xff424242;final Paint paint = new Paint();final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); paint.setAntiAlias(true);canvas.drawARGB(0, 0, 0, 0);paint.setColor(color);// canvas.drawRoundRect(rectF, roundPx, roundPx, paint);canvas.drawCircle((float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2,(float) bitmap.getWidth() / 2, paint);paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));canvas.drawBitmap(bitmap, rect, rect, paint);//Bitmap _bmp = Bitmap.createScaledBitmap(output, 60, 60, false);//return _bmp;return output;} private Runnable progressRunnable = new Runnable() {@Overridepublic void run() {if (isCancelled()) {setLoadingState(false, false);return;} if (loadingIncrease) {if (loadingProgress >= LOADING_PROGRESS_STEPS) {loadingIncrease = false;}} else {if (loadingProgress <= 0) {loadingIncrease = true;}} loadingProgress = loadingIncrease ? loadingProgress + 1 : loadingProgress - 1; feature.addNumberProperty(PROPERTY_LOADING_PROGRESS, loadingProgress);SymbolLayerMapillaryActivity activity = activityRef.get();if (activity != null) {activity.refreshSource();}progressHandler.postDelayed(this, LOADING_STEP_DURATION);}}; private void setLoadingState(boolean isLoading, boolean isSuccess) {progressHandler.removeCallbacksAndMessages(null);feature.addBooleanProperty(PROPERTY_LOADING, isLoading);SymbolLayerMapillaryActivity activity = activityRef.get();if (activity != null) {activity.refreshSource(); if (isLoading) { //zooming to a loading stateactivity.setActivityStep(STEP_LOADING);} else if (isSuccess) { //if success zooming to a ready state, otherwise do nothingactivity.setActivityStep(STEP_READY);}}}} private static class MapillaryDataLoadResult {private final HashMap<Feature, Bitmap> bitmapHashMap = new HashMap<>();private final FeatureCollection mapillaryFeatureCollection; MapillaryDataLoadResult(FeatureCollection mapillaryFeatureCollection) {this.mapillaryFeatureCollection = mapillaryFeatureCollection;} public void add(Feature feature, Bitmap bitmap) {bitmapHashMap.put(feature, bitmap);}} /*** 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;}} /*** Util class that creates a Source and a Layer based on Mapillary data.* https://www.mapillary.com/developer/tiles-documentation/*/private static class MapillaryTiles { static final String ID_SOURCE = "mapillary.source";static final String ID_LINE_LAYER = "mapillary.layer.line";static final String URL_TILESET = "https://d25uarhxywzl1j.cloudfront.net/v0.1/{z}/{x}/{y}.mvt"; static Source createSource() {TileSet mapillaryTileset = new TileSet("2.1.0", MapillaryTiles.URL_TILESET);mapillaryTileset.setMinZoom(0);mapillaryTileset.setMaxZoom(14);return new VectorSource(MapillaryTiles.ID_SOURCE, mapillaryTileset);} static Layer createLineLayer() {LineLayer lineLayer = new LineLayer(MapillaryTiles.ID_LINE_LAYER, MapillaryTiles.ID_SOURCE);lineLayer.setSourceLayer("mapillary-sequences");lineLayer.setProperties(lineCap(Property.LINE_CAP_ROUND),lineJoin(Property.LINE_JOIN_ROUND),lineOpacity(0.6f),lineWidth(2.0f),lineColor(Color.GREEN));return lineLayer;}} /*** RecyclerViewAdapter adapting features to cards.*/static class LocationRecyclerViewAdapter extendsRecyclerView.Adapter<SymbolLayerMapillaryActivity.LocationRecyclerViewAdapter.MyViewHolder> { private List<Feature> featureCollection;private SymbolLayerMapillaryActivity activity; LocationRecyclerViewAdapter(SymbolLayerMapillaryActivity activity, FeatureCollection featureCollection) {this.activity = activity;this.featureCollection = featureCollection.features();} @Overridepublic LocationRecyclerViewAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.cardview_symbol_layer, parent, false);return new LocationRecyclerViewAdapter.MyViewHolder(itemView);} @Overridepublic void onBindViewHolder(LocationRecyclerViewAdapter.MyViewHolder holder, int position) {Feature feature = featureCollection.get(position);holder.title.setText(feature.getStringProperty(PROPERTY_TITLE));holder.description.setText(feature.getStringProperty(PROPERTY_DESCRIPTION));holder.poi.setText(feature.getStringProperty(PROPERTY_POI));holder.style.setText(feature.getStringProperty(PROPERTY_STYLE));holder.setClickListener(new ItemClickListener() {@Overridepublic void onClick(View view, int position) {if (activity != null) {activity.toggleFavourite(position);}}});} @Overridepublic int getItemCount() {return featureCollection.size();} /*** ViewHolder for RecyclerView.*/static class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {TextView title;TextView poi;TextView style;TextView description;CardView singleCard;ItemClickListener clickListener; MyViewHolder(View view) {super(view);title = view.findViewById(R.id.textview_title);poi = view.findViewById(R.id.textview_poi);style = view.findViewById(R.id.textview_style);description = view.findViewById(R.id.textview_description);singleCard = view.findViewById(R.id.single_location_cardview);singleCard.setOnClickListener(this);} void setClickListener(ItemClickListener itemClickListener) {this.clickListener = itemClickListener;} @Overridepublic void onClick(View view) {clickListener.onClick(view, getLayoutPosition());}}} interface ItemClickListener {void onClick(View view, int position);}}