package com.mapbox.mapboxsdk.maps; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringDef; import android.util.DisplayMetrics; import android.util.Pair; import com.mapbox.mapboxsdk.constants.MapboxConstants; import com.mapbox.mapboxsdk.style.layers.Layer; import com.mapbox.mapboxsdk.style.layers.TransitionOptions; import com.mapbox.mapboxsdk.style.light.Light; import com.mapbox.mapboxsdk.style.sources.Source; import com.mapbox.mapboxsdk.utils.BitmapUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * The proxy object for current map style. *

* To create new instances of this object, create a new instance using a {@link Builder} and load the style with * {@link MapboxMap#setStyle(Builder)}. This object is returned from {@link MapboxMap#getStyle()} once the style * has been loaded by underlying map. *

*/ @SuppressWarnings("unchecked") public class Style { private final NativeMap nativeMap; private final HashMap sources = new HashMap<>(); private final HashMap layers = new HashMap<>(); private final HashMap images = new HashMap<>(); private final Builder builder; private boolean fullyLoaded; /** * Private constructor to build a style object. * * @param builder the builder used for creating this style * @param nativeMap the map object used to load this style */ private Style(@NonNull Builder builder, @NonNull NativeMap nativeMap) { this.builder = builder; this.nativeMap = nativeMap; } /** * Returns the current style url. * * @return the style url */ @NonNull public String getUrl() { validateState("getUrl"); return nativeMap.getStyleUrl(); } /** * Returns the current style json. * * @return the style json */ @NonNull public String getJson() { validateState("getJson"); return nativeMap.getStyleJson(); } // // Source // /** * Retrieve all the sources in the style * * @return all the sources in the current style */ @NonNull public List getSources() { validateState("getSources"); return nativeMap.getSources(); } /** * Adds the source to the map. The source must be newly created and not added to the map before * * @param source the source to add */ public void addSource(@NonNull Source source) { validateState("addSource"); sources.put(source.getId(), source); nativeMap.addSource(source); } /** * Retrieve a source by id * * @param id the source's id * @return the source if present in the current style */ @Nullable public Source getSource(String id) { validateState("getSource"); Source source = sources.get(id); if (source == null) { source = nativeMap.getSource(id); } return source; } /** * Tries to cast the Source to T, throws ClassCastException if it's another type. * * @param sourceId the id used to look up a layer * @param the generic type of a Source * @return the casted Source, null if another type */ @Nullable public T getSourceAs(@NonNull String sourceId) { validateState("getSourceAs"); // noinspection unchecked if (sources.containsKey(sourceId)) { return (T) sources.get(sourceId); } return (T) nativeMap.getSource(sourceId); } /** * Removes the source from the style. * * @param sourceId the source to remove * @return the source handle or null if the source was not present */ public boolean removeSource(@NonNull String sourceId) { validateState("removeSource"); sources.remove(sourceId); return nativeMap.removeSource(sourceId); } /** * Removes the source, preserving the reference for re-use * * @param source the source to remove * @return the source */ public boolean removeSource(@NonNull Source source) { validateState("removeSource"); sources.remove(source.getId()); return nativeMap.removeSource(source); } // // Layer // /** * Adds the layer to the map. The layer must be newly created and not added to the map before * * @param layer the layer to add */ public void addLayer(@NonNull Layer layer) { validateState("addLayer"); layers.put(layer.getId(), layer); nativeMap.addLayer(layer); } /** * Adds the layer to the map. The layer must be newly created and not added to the map before * * @param layer the layer to add * @param below the layer id to add this layer before */ public void addLayerBelow(@NonNull Layer layer, @NonNull String below) { validateState("addLayerBelow"); layers.put(layer.getId(), layer); nativeMap.addLayerBelow(layer, below); } /** * Adds the layer to the map. The layer must be newly created and not added to the map before * * @param layer the layer to add * @param above the layer id to add this layer above */ public void addLayerAbove(@NonNull Layer layer, @NonNull String above) { validateState("addLayerAbove"); layers.put(layer.getId(), layer); nativeMap.addLayerAbove(layer, above); } /** * Adds the layer to the map at the specified index. The layer must be newly * created and not added to the map before * * @param layer the layer to add * @param index the index to insert the layer at */ public void addLayerAt(@NonNull Layer layer, @IntRange(from = 0) int index) { validateState("addLayerAbove"); layers.put(layer.getId(), layer); nativeMap.addLayerAt(layer, index); } /** * Get the layer by id * * @param id the layer's id * @return the layer, if present in the style */ @Nullable public Layer getLayer(@NonNull String id) { validateState("getLayer"); Layer layer = layers.get(id); if (layer == null) { layer = nativeMap.getLayer(id); } return layer; } /** * Tries to cast the Layer to T, throws ClassCastException if it's another type. * * @param layerId the layer id used to look up a layer * @param the generic attribute of a Layer * @return the casted Layer, null if another type */ @Nullable public T getLayerAs(@NonNull String layerId) { validateState("getLayerAs"); // noinspection unchecked return (T) nativeMap.getLayer(layerId); } /** * Retrieve all the layers in the style * * @return all the layers in the current style */ @NonNull public List getLayers() { validateState("getLayers"); return nativeMap.getLayers(); } /** * Removes the layer. Any references to the layer become invalid and should not be used anymore * * @param layerId the layer to remove * @return the removed layer or null if not found */ public boolean removeLayer(@NonNull String layerId) { validateState("removeLayer"); layers.remove(layerId); return nativeMap.removeLayer(layerId); } /** * Removes the layer. The reference is re-usable after this and can be re-added * * @param layer the layer to remove * @return the layer */ public boolean removeLayer(@NonNull Layer layer) { validateState("removeLayer"); layers.remove(layer.getId()); return nativeMap.removeLayer(layer); } /** * Removes the layer. Any other references to the layer become invalid and should not be used anymore * * @param index the layer index * @return the removed layer or null if not found */ public boolean removeLayerAt(@IntRange(from = 0) int index) { validateState("removeLayerAt"); return nativeMap.removeLayerAt(index); } // // Image // /** * Adds an image to be used in the map's style * * @param name the name of the image * @param image the pre-multiplied Bitmap */ public void addImage(@NonNull String name, @NonNull Bitmap image) { addImage(name, image, false); } /** * Adds an drawable to be converted into a bitmap to be used in the map's style * * @param name the name of the image * @param drawable the drawable instance to convert */ public void addImage(@NonNull String name, @NonNull Drawable drawable) { Bitmap bitmap = BitmapUtils.getBitmapFromDrawable(drawable); if (bitmap == null) { throw new IllegalArgumentException("Provided drawable couldn't be converted to a Bitmap."); } addImage(name, bitmap, false); } /** * Adds an image to be used in the map's style * * @param name the name of the image * @param bitmap the pre-multiplied Bitmap * @param sdf the flag indicating image is an SDF or template image */ public void addImage(@NonNull final String name, @NonNull final Bitmap bitmap, boolean sdf) { validateState("addImage"); new BitmapImageConversionTask(nativeMap).execute(new Builder.ImageWrapper(name, bitmap, sdf)); } /** * Adds an images to be used in the map's style. * * @param images the map of images to add */ public void addImages(@NonNull HashMap images) { addImages(images, false); } /** * Adds an images to be used in the map's style. * * @param images the map of images to add * @param sdf the flag indicating image is an SDF or template image */ public void addImages(@NonNull HashMap images, boolean sdf) { validateState("addImages"); new BitmapImageConversionTask(nativeMap).execute(Builder.ImageWrapper.convertToImageArray(images, sdf)); } /** * Removes an image from the map's style. * * @param name the name of the image to remove */ public void removeImage(@NonNull String name) { validateState("removeImage"); nativeMap.removeImage(name); } /** * Get an image from the map's style using an id. * * @param id the id of the image * @return the image bitmap */ @Nullable public Bitmap getImage(@NonNull String id) { validateState("getImage"); return nativeMap.getImage(id); } // // Transition // /** *

* Set the transition options for style changes. *

* If not set, any changes take effect without animation, besides symbols, * which will fade in/out with a default duration after symbol collision detection. *

* To disable symbols fade in/out animation, * pass transition options with {@link TransitionOptions#enablePlacementTransitions} equal to false. *

* Both {@link TransitionOptions#duration} and {@link TransitionOptions#delay} * will also change the behavior of the symbols fade in/out animation if the placement transition is enabled. * * @param transitionOptions the transition options */ public void setTransition(@NonNull TransitionOptions transitionOptions) { validateState("setTransition"); nativeMap.setTransitionOptions(transitionOptions); } /** *

* Get the transition options for style changes. *

* By default, any changes take effect without animation, besides symbols, * which will fade in/out with a default duration after symbol collision detection. *

* To disable symbols fade in/out animation, * pass transition options with {@link TransitionOptions#enablePlacementTransitions} equal to false * into {@link #setTransition(TransitionOptions)}. *

* Both {@link TransitionOptions#duration} and {@link TransitionOptions#delay} * will also change the behavior of the symbols fade in/out animation if the placement transition is enabled. * * @return TransitionOptions the transition options */ @NonNull public TransitionOptions getTransition() { validateState("getTransition"); return nativeMap.getTransitionOptions(); } // // Light // /** * Get the light source used to change lighting conditions on extruded fill layers. * * @return the global light source */ @Nullable public Light getLight() { validateState("getLight"); return nativeMap.getLight(); } // // State // /** * Called when the underlying map will start loading a new style. This method will clean up this style * by setting the java sources and layers in a detached state and removing them from core. */ void onWillStartLoadingMap() { fullyLoaded = false; for (Source source : sources.values()) { if (source != null) { source.setDetached(); nativeMap.removeSource(source); } } for (Layer layer : layers.values()) { if (layer != null) { layer.setDetached(); nativeMap.removeLayer(layer); } } for (Map.Entry bitmapEntry : images.entrySet()) { nativeMap.removeImage(bitmapEntry.getKey()); bitmapEntry.getValue().recycle(); } sources.clear(); layers.clear(); images.clear(); } /** * Called when the underlying map has finished loading this style. * This method will add all components added to the builder that were defined with the 'with' prefix. */ void onDidFinishLoadingStyle() { if (!fullyLoaded) { fullyLoaded = true; for (Source source : builder.sources) { addSource(source); } for (Builder.LayerWrapper layerWrapper : builder.layers) { if (layerWrapper instanceof Builder.LayerAtWrapper) { addLayerAt(layerWrapper.layer, ((Builder.LayerAtWrapper) layerWrapper).index); } else if (layerWrapper instanceof Builder.LayerAboveWrapper) { addLayerAbove(layerWrapper.layer, ((Builder.LayerAboveWrapper) layerWrapper).aboveLayer); } else if (layerWrapper instanceof Builder.LayerBelowWrapper) { addLayerBelow(layerWrapper.layer, ((Builder.LayerBelowWrapper) layerWrapper).belowLayer); } else { // just add layer to map, but below annotations addLayerBelow(layerWrapper.layer, MapboxConstants.LAYER_ID_ANNOTATIONS); } } for (Builder.ImageWrapper image : builder.images) { addImage(image.id, image.bitmap, image.sdf); } if (builder.transitionOptions != null) { setTransition(builder.transitionOptions); } } } /** * Returns true if the style is fully loaded. Returns false if style hasn't been fully loaded or a new style is * underway of being loaded. * * @return True if fully loaded, false otherwise */ public boolean isFullyLoaded() { return fullyLoaded; } /** * Validates the style state, throw an IllegalArgumentException on invalid state. * * @param methodCall the calling method name */ private void validateState(String methodCall) { if (!fullyLoaded) { throw new IllegalStateException( String.format("Calling %s when a newer style is loading/has loaded.", methodCall) ); } } // // Builder // /** * Builder for composing a style object. */ public static class Builder { private final List sources = new ArrayList<>(); private final List layers = new ArrayList<>(); private final List images = new ArrayList<>(); private TransitionOptions transitionOptions; private String styleUrl; private String styleJson; /** *

* Will loads a new map style asynchronous from the specified URL. *

* {@code url} can take the following forms: *
    *
  • {@code Style#StyleUrl}: load one of the bundled styles in {@link Style}.
  • *
  • {@code mapbox://styles//