package com.mapbox.mapboxsdk.location; import android.graphics.Bitmap; import android.graphics.PointF; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.mapbox.geojson.Feature; import com.mapbox.geojson.Point; import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.location.modes.RenderMode; import com.mapbox.mapboxsdk.maps.MapboxMap; import com.mapbox.mapboxsdk.maps.Style; import com.mapbox.mapboxsdk.style.layers.Layer; import com.mapbox.mapboxsdk.style.layers.SymbolLayer; import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; import java.util.HashSet; import java.util.List; import java.util.Set; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.ACCURACY_LAYER; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_ICON; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_LAYER; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_STALE_ICON; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BEARING_ICON; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BEARING_LAYER; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.FOREGROUND_ICON; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.FOREGROUND_LAYER; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.FOREGROUND_STALE_ICON; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.LOCATION_SOURCE; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_ALPHA; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_COLOR; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_RADIUS; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BACKGROUND_ICON; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BACKGROUND_STALE_ICON; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BEARING_ICON; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_COMPASS_BEARING; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_ICON; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_ICON_OFFSET; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_STALE_ICON; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_GPS_BEARING; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_LOCATION_STALE; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_SHADOW_ICON_OFFSET; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_ICON; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_LAYER; import static com.mapbox.mapboxsdk.style.expressions.Expression.interpolate; import static com.mapbox.mapboxsdk.style.expressions.Expression.linear; import static com.mapbox.mapboxsdk.style.expressions.Expression.stop; import static com.mapbox.mapboxsdk.style.expressions.Expression.zoom; import static com.mapbox.mapboxsdk.style.layers.Property.NONE; import static com.mapbox.mapboxsdk.style.layers.Property.VISIBLE; import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize; import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility; import static com.mapbox.mapboxsdk.utils.ColorUtils.colorToRgbaString; final class LocationLayerController { @RenderMode.Mode private int renderMode; private final MapboxMap mapboxMap; private Style style; private final LayerSourceProvider layerSourceProvider; private final LayerBitmapProvider bitmapProvider; private LocationComponentOptions options; private final OnRenderModeChangedListener internalRenderModeChangedListener; @VisibleForTesting final Set layerSet = new HashSet<>(); private Feature locationFeature; private GeoJsonSource locationSource; private boolean isHidden = true; @Nullable private String layerBelow; LocationLayerController(MapboxMap mapboxMap, Style style, LayerSourceProvider layerSourceProvider, LayerFeatureProvider featureProvider, LayerBitmapProvider bitmapProvider, @NonNull LocationComponentOptions options, @NonNull OnRenderModeChangedListener internalRenderModeChangedListener) { this.mapboxMap = mapboxMap; this.style = style; this.layerSourceProvider = layerSourceProvider; this.bitmapProvider = bitmapProvider; this.locationFeature = featureProvider.generateLocationFeature(locationFeature, options); this.internalRenderModeChangedListener = internalRenderModeChangedListener; initializeComponents(style, options); } void initializeComponents(Style style, LocationComponentOptions options) { this.style = style; addLocationSource(); addLayers(options.layerBelow()); applyStyle(options); if (isHidden) { hide(); } else { show(); } } void applyStyle(@NonNull LocationComponentOptions options) { String newLayerBelowOption = options.layerBelow(); if ((layerBelow != null || newLayerBelowOption != null)) { if (layerBelow == null || !layerBelow.equals(newLayerBelowOption)) { removeLayers(); addLayers(newLayerBelowOption); if (isHidden) { for (String layerId : layerSet) { setLayerVisibility(layerId, false); } } setRenderMode(renderMode); } } this.options = options; float elevation = options.elevation(); // Only add icon elevation if the values greater than 0. if (elevation > 0) { styleShadow(options); } styleForeground(options); styleBackground(options); styleBearing(options); styleAccuracy(options.accuracyAlpha(), options.accuracyColor()); styleScaling(options); determineIconsSource(options); } void setRenderMode(@RenderMode.Mode int renderMode) { int previousMode = this.renderMode; this.renderMode = renderMode; if (!isHidden) { boolean isStale = locationFeature.getBooleanProperty(PROPERTY_LOCATION_STALE); switch (renderMode) { case RenderMode.NORMAL: styleForeground(options); setLayerVisibility(SHADOW_LAYER, true); setLayerVisibility(FOREGROUND_LAYER, true); setLayerVisibility(BACKGROUND_LAYER, true); setLayerVisibility(ACCURACY_LAYER, !isStale); setLayerVisibility(BEARING_LAYER, false); break; case RenderMode.COMPASS: styleForeground(options); setLayerVisibility(SHADOW_LAYER, true); setLayerVisibility(FOREGROUND_LAYER, true); setLayerVisibility(BACKGROUND_LAYER, true); setLayerVisibility(ACCURACY_LAYER, !isStale); setLayerVisibility(BEARING_LAYER, true); break; case RenderMode.GPS: styleForeground(options); setLayerVisibility(SHADOW_LAYER, false); setLayerVisibility(FOREGROUND_LAYER, true); setLayerVisibility(BACKGROUND_LAYER, true); setLayerVisibility(ACCURACY_LAYER, false); setLayerVisibility(BEARING_LAYER, false); break; default: break; } determineIconsSource(options); } if (previousMode != renderMode) { internalRenderModeChangedListener.onRenderModeChanged(renderMode); } } int getRenderMode() { return renderMode; } // // Layer action // void show() { isHidden = false; setRenderMode(renderMode); } void hide() { isHidden = true; for (String layerId : layerSet) { setLayerVisibility(layerId, false); } } void updateForegroundOffset(double tilt) { JsonArray foregroundJsonArray = new JsonArray(); foregroundJsonArray.add(0f); foregroundJsonArray.add((float) (-0.05 * tilt)); locationFeature.addProperty(PROPERTY_FOREGROUND_ICON_OFFSET, foregroundJsonArray); JsonArray backgroundJsonArray = new JsonArray(); backgroundJsonArray.add(0f); backgroundJsonArray.add((float) (0.05 * tilt)); locationFeature.addProperty(PROPERTY_SHADOW_ICON_OFFSET, backgroundJsonArray); refreshSource(); } void updateForegroundBearing(float bearing) { if (renderMode != RenderMode.GPS) { setBearingProperty(PROPERTY_GPS_BEARING, bearing); } } boolean isHidden() { return isHidden; } boolean isConsumingCompass() { return renderMode == RenderMode.COMPASS; } private void setLayerVisibility(@NonNull String layerId, boolean visible) { Layer layer = style.getLayer(layerId); if (layer != null) { String targetVisibility = visible ? VISIBLE : NONE; if (!layer.getVisibility().value.equals(targetVisibility)) { layer.setProperties(visibility(visible ? VISIBLE : NONE)); } } } private void addLayers(@NonNull String idBelowLayer) { layerBelow = idBelowLayer; addSymbolLayer(BEARING_LAYER, idBelowLayer); addSymbolLayer(FOREGROUND_LAYER, BEARING_LAYER); addSymbolLayer(BACKGROUND_LAYER, FOREGROUND_LAYER); addSymbolLayer(SHADOW_LAYER, BACKGROUND_LAYER); addAccuracyLayer(); } private void addSymbolLayer(@NonNull String layerId, @NonNull String beforeLayerId) { Layer layer = layerSourceProvider.generateLayer(layerId); addLayerToMap(layer, beforeLayerId); } private void addAccuracyLayer() { Layer accuracyLayer = layerSourceProvider.generateAccuracyLayer(); addLayerToMap(accuracyLayer, BACKGROUND_LAYER); } private void addLayerToMap(Layer layer, @NonNull String idBelowLayer) { style.addLayerBelow(layer, idBelowLayer); layerSet.add(layer.getId()); } private void removeLayers() { for (String layerId : layerSet) { style.removeLayer(layerId); } layerSet.clear(); } private void setBearingProperty(@NonNull String propertyId, float bearing) { locationFeature.addNumberProperty(propertyId, bearing); refreshSource(); } private void updateAccuracyRadius(float accuracy) { locationFeature.addNumberProperty(PROPERTY_ACCURACY_RADIUS, accuracy); refreshSource(); } // // Source actions // private void addLocationSource() { locationSource = layerSourceProvider.generateSource(locationFeature); style.addSource(locationSource); } private void refreshSource() { GeoJsonSource source = style.getSourceAs(LOCATION_SOURCE); if (source != null) { locationSource.setGeoJson(locationFeature); } } private void setLocationPoint(Point locationPoint) { JsonObject properties = locationFeature.properties(); if (properties != null) { locationFeature = Feature.fromGeometry(locationPoint, properties); refreshSource(); } } // // Styling // private void styleBackground(LocationComponentOptions options) { Bitmap backgroundBitmap = bitmapProvider.generateBitmap( options.backgroundDrawable(), options.backgroundTintColor() ); Bitmap backgroundStaleBitmap = bitmapProvider.generateBitmap( options.backgroundDrawableStale(), options.backgroundStaleTintColor() ); style.addImage(BACKGROUND_ICON, backgroundBitmap); style.addImage(BACKGROUND_STALE_ICON, backgroundStaleBitmap); } private void styleShadow(@NonNull LocationComponentOptions options) { style.addImage(SHADOW_ICON, bitmapProvider.generateShadowBitmap(options)); } private void styleBearing(LocationComponentOptions options) { Bitmap bearingBitmap = bitmapProvider.generateBitmap(options.bearingDrawable(), options.bearingTintColor()); style.addImage(BEARING_ICON, bearingBitmap); } private void styleAccuracy(float accuracyAlpha, @ColorInt int accuracyColor) { locationFeature.addNumberProperty(PROPERTY_ACCURACY_ALPHA, accuracyAlpha); locationFeature.addStringProperty(PROPERTY_ACCURACY_COLOR, colorToRgbaString(accuracyColor)); refreshSource(); } private void styleForeground(LocationComponentOptions options) { Bitmap foregroundBitmap = bitmapProvider.generateBitmap( options.foregroundDrawable(), options.foregroundTintColor() ); Bitmap foregroundBitmapStale = bitmapProvider.generateBitmap( options.foregroundDrawableStale(), options.foregroundStaleTintColor() ); if (renderMode == RenderMode.GPS) { foregroundBitmap = bitmapProvider.generateBitmap( options.gpsDrawable(), options.foregroundTintColor() ); foregroundBitmapStale = bitmapProvider.generateBitmap( options.gpsDrawable(), options.foregroundStaleTintColor() ); } style.addImage(FOREGROUND_ICON, foregroundBitmap); style.addImage(FOREGROUND_STALE_ICON, foregroundBitmapStale); } private void styleScaling(@NonNull LocationComponentOptions options) { for (String layerId : layerSet) { Layer layer = style.getLayer(layerId); if (layer instanceof SymbolLayer) { layer.setProperties( iconSize( interpolate(linear(), zoom(), stop(mapboxMap.getMinZoomLevel(), options.minZoomIconScale()), stop(mapboxMap.getMaxZoomLevel(), options.maxZoomIconScale()) ) ) ); } } } private void determineIconsSource(LocationComponentOptions options) { String foregroundIconString = buildIconString( renderMode == RenderMode.GPS ? options.gpsName() : options.foregroundName(), FOREGROUND_ICON); String foregroundStaleIconString = buildIconString(options.foregroundStaleName(), FOREGROUND_STALE_ICON); String backgroundIconString = buildIconString(options.backgroundName(), BACKGROUND_ICON); String backgroundStaleIconString = buildIconString(options.backgroundStaleName(), BACKGROUND_STALE_ICON); String bearingIconString = buildIconString(options.bearingName(), BEARING_ICON); locationFeature.addStringProperty(PROPERTY_FOREGROUND_ICON, foregroundIconString); locationFeature.addStringProperty(PROPERTY_BACKGROUND_ICON, backgroundIconString); locationFeature.addStringProperty(PROPERTY_FOREGROUND_STALE_ICON, foregroundStaleIconString); locationFeature.addStringProperty(PROPERTY_BACKGROUND_STALE_ICON, backgroundStaleIconString); locationFeature.addStringProperty(PROPERTY_BEARING_ICON, bearingIconString); refreshSource(); } @NonNull private String buildIconString(@Nullable String bitmapName, @NonNull String drawableName) { if (bitmapName != null) { return bitmapName; } return drawableName; } void setLocationsStale(boolean isStale) { locationFeature.addBooleanProperty(PROPERTY_LOCATION_STALE, isStale); refreshSource(); if (renderMode != RenderMode.GPS) { setLayerVisibility(ACCURACY_LAYER, !isStale); } } // // Map click event // boolean onMapClick(@NonNull LatLng point) { PointF screenLoc = mapboxMap.getProjection().toScreenLocation(point); List features = mapboxMap.queryRenderedFeatures(screenLoc, BACKGROUND_LAYER, FOREGROUND_LAYER, BEARING_LAYER ); return !features.isEmpty(); } private final MapboxAnimator.AnimationsValueChangeListener latLngValueListener = new MapboxAnimator.AnimationsValueChangeListener() { @Override public void onNewAnimationValue(LatLng value) { Point point = Point.fromLngLat(value.getLongitude(), value.getLatitude()); setLocationPoint(point); } }; private final MapboxAnimator.AnimationsValueChangeListener gpsBearingValueListener = new MapboxAnimator.AnimationsValueChangeListener() { @Override public void onNewAnimationValue(Float value) { setBearingProperty(PROPERTY_GPS_BEARING, value); } }; private final MapboxAnimator.AnimationsValueChangeListener compassBearingValueListener = new MapboxAnimator.AnimationsValueChangeListener() { @Override public void onNewAnimationValue(Float value) { setBearingProperty(PROPERTY_COMPASS_BEARING, value); } }; private final MapboxAnimator.AnimationsValueChangeListener accuracyValueListener = new MapboxAnimator.AnimationsValueChangeListener() { @Override public void onNewAnimationValue(Float value) { updateAccuracyRadius(value); } }; Set getAnimationListeners() { Set holders = new HashSet<>(); holders.add(new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_LAYER_LATLNG, latLngValueListener)); if (renderMode == RenderMode.GPS) { holders.add(new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_LAYER_GPS_BEARING, gpsBearingValueListener)); } else if (renderMode == RenderMode.COMPASS) { holders.add( new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_LAYER_COMPASS_BEARING, compassBearingValueListener)); } if (renderMode == RenderMode.COMPASS || renderMode == RenderMode.NORMAL) { holders.add(new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_LAYER_ACCURACY, accuracyValueListener)); } return holders; } }