Skip to main content

SymbolLayer icons

A newer version of the Maps SDK is available
This page uses v9.7.1 of the Mapbox Maps SDK. A newer version of the SDK is available. Learn about the latest version, v11.9.0, in the Maps SDK documentation.
Note

This example is a part of the Mapbox Android Demo app. You can find the values for all referenced resources in the res directory. For example, see res/values/activity_strings.xml for R.string.* references used in this example. The dependencies can be found here.The examples use View binding.See setup documention if necessary.

activity_symbol_layer_mapillary
<?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.MapView
android: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.RecyclerView
android: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 animation
private 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;

@ActivityStep
private 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);
}

@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.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 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.DARK, new Style.OnStyleLoaded() {
@Override
public 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);
}
});
}

@Override
public 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 layer
Feature 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 layer
return handleClickIcon(screenPoint);
}
return true;
}

public void setupData(final FeatureCollection collection) {
if (mapboxMap == null) {
return;
}
featureCollection = collection;
mapboxMap.getStyle(new Style.OnStyleLoaded() {
@Override
public 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() {
@Override
public 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 textView
Rect hitRectText = new Rect();
textContainer.getHitRect(hitRectText);

// move hitbox to location of symbol
hitRectText.offset((int) symbolScreenPoint.x, (int) symbolScreenPoint.y);

// offset vertically to match anchor behaviour
hitRectText.offset(0, -view.getMeasuredHeight());

// hit test if clicked point is in textview hitbox
if (hitRectText.contains((int) screenPoint.x, (int) screenPoint.y)) {
// user clicked on text
String callout = feature.getStringProperty("call-out");
Toast.makeText(this, callout, Toast.LENGTH_LONG).show();
} else {
// user clicked on icon
List<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() {
@Override
public 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() {
@Override
public 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;
}

@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();

if (loadMapillaryDataTask != null) {
loadMapillaryDataTask.cancel(true);
}
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);
}
mapView.onDestroy();
}

@Override
public 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() {
@Override
public 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() {
@Override
public 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() {
@Override
public 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() {
@Override
public 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();

@Override
public 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);
}

@Override
protected FeatureCollection doInBackground(Void... params) {
SymbolLayerMapillaryActivity activity = activityRef.get();

if (activity == null) {
return null;
}

String geoJson = loadGeoJsonFromAsset(activity, "sf_poi.geojson");
return FeatureCollection.fromJson(geoJson);
}

@Override
protected 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 folder
InputStream 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")
@Override
protected 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;
}
}

@Override
protected 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;
}

@Override
protected void onPreExecute() {
super.onPreExecute();
loadingProgress = 0;
setLoadingState(true, false);
}

@Override
protected 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 circular
bitmap = getCroppedBitmap(bitmap);

mapillaryDataLoadResult.add(feature, bitmap);
}
return mapillaryDataLoadResult;

} catch (Exception exception) {
Timber.e(exception);
}
return null;
}

@Override
protected 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)
));

// unclustered
map.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);

// clustered
int[][] 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 circles
CircleLayer 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 labels
SymbolLayer 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() {
@Override
public 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 state
activity.setActivityStep(STEP_LOADING);
} else if (isSuccess) { //if success zooming to a ready state, otherwise do nothing
activity.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 extends
RecyclerView.Adapter<SymbolLayerMapillaryActivity.LocationRecyclerViewAdapter.MyViewHolder> {

private List<Feature> featureCollection;
private SymbolLayerMapillaryActivity activity;

LocationRecyclerViewAdapter(SymbolLayerMapillaryActivity activity, FeatureCollection featureCollection) {
this.activity = activity;
this.featureCollection = featureCollection.features();
}

@Override
public 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);
}

@Override
public 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() {
@Override
public void onClick(View view, int position) {
if (activity != null) {
activity.toggleFavourite(position);
}
}
});
}

@Override
public 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;
}

@Override
public void onClick(View view) {
clickListener.onClick(view, getLayoutPosition());
}
}
}

interface ItemClickListener {
void onClick(View view, int position);
}
}