package com.mapbox.mapboxsdk.maps;
import android.graphics.Bitmap;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringDef;
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 java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
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.
*
*/
public class Style {
private final NativeMapView nativeMapView;
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 nativeMapView the map object used to load this style
*/
private Style(@NonNull Builder builder, @NonNull NativeMapView nativeMapView) {
this.builder = builder;
this.nativeMapView = nativeMapView;
}
/**
* Returns the current style url.
*
* @return the style url
*/
@NonNull
public String getUrl() {
if (!fullyLoaded) {
// we are loading a new style
return "";
}
return nativeMapView.getStyleUrl();
}
/**
* Returns the current style json.
*
* @return the style json
*/
@NonNull
public String getJson() {
if (!fullyLoaded) {
// we are loading a new style
return "";
}
return nativeMapView.getStyleJson();
}
//
// Source
//
/**
* Retrieve all the sources in the style
*
* @return all the sources in the current style
*/
@NonNull
public List getSources() {
if (!fullyLoaded) {
// we are loading a new style
return new ArrayList<>();
}
return nativeMapView.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) {
if (!fullyLoaded) {
throw new IllegalStateException(
"Calling addSource on old Style instance, use the more recently loaded Style instead."
);
}
sources.put(source.getId(), source);
nativeMapView.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) {
if (!fullyLoaded) {
// we are loading a new style
return null;
}
Source source = sources.get(id);
if (source == null) {
source = nativeMapView.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) {
if (!fullyLoaded) {
// we are loading a new style
return null;
}
// noinspection unchecked
if (sources.containsKey(sourceId)) {
return (T) sources.get(sourceId);
}
return (T) nativeMapView.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) {
if (!fullyLoaded) {
// we are loading a new style
return false;
}
sources.remove(sourceId);
return nativeMapView.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) {
if (!fullyLoaded) {
// we are loading a new style
return false;
}
sources.remove(source.getId());
return nativeMapView.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) {
if (!fullyLoaded) {
throw new IllegalStateException(
"Calling addLayer on old Style instance, use the more recently loaded Style instead."
);
}
layers.put(layer.getId(), layer);
nativeMapView.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) {
if (!fullyLoaded) {
throw new IllegalStateException(
"Calling addLayerBelow on old Style instance, use the more recently loaded Style instead."
);
}
layers.put(layer.getId(), layer);
nativeMapView.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) {
if (!fullyLoaded) {
throw new IllegalStateException(
"Calling addLayerAbove on old Style instance, use the more recently loaded Style instead."
);
}
layers.put(layer.getId(), layer);
nativeMapView.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) {
if (!fullyLoaded) {
throw new IllegalStateException(
"Calling addLayerAt on old Style instance, use the more recently loaded Style instead."
);
}
layers.put(layer.getId(), layer);
nativeMapView.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) {
if (!fullyLoaded) {
// we are loading a new style
return null;
}
Layer layer = layers.get(id);
if (layer == null) {
layer = nativeMapView.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) {
if (!fullyLoaded) {
// we are loading a new style
return null;
}
// noinspection unchecked
return (T) nativeMapView.getLayer(layerId);
}
/**
* Retrieve all the layers in the style
*
* @return all the layers in the current style
*/
@NonNull
public List getLayers() {
if (!fullyLoaded) {
// we are loading a new style
return new ArrayList<>();
}
return nativeMapView.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) {
if (!fullyLoaded) {
// we are loading a new style
return false;
}
layers.remove(layerId);
return nativeMapView.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) {
if (!fullyLoaded) {
// we are loading a new style
return false;
}
layers.remove(layer.getId());
return nativeMapView.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) {
return nativeMapView.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 image to be used in the map's style
*
* @param name the name of the image
* @param image the pre-multiplied Bitmap
* @param sdf the flag indicating image is an SDF or template image
*/
public void addImage(@NonNull String name, @NonNull Bitmap image, boolean sdf) {
if (!fullyLoaded) {
throw new IllegalStateException(
"Calling addImage on old Style instance, use the more recently loaded Style instead."
);
}
nativeMapView.addImage(name, image, sdf);
}
/**
* Adds an images to be used in the map's style.
*/
public void addImages(@NonNull HashMap images) {
if (!fullyLoaded) {
throw new IllegalStateException(
"Calling addImages on old Style instance, use the more recently loaded Style instead."
);
}
nativeMapView.addImages(images);
}
/**
* Removes an image from the map's style.
*
* @param name the name of the image to remove
*/
public void removeImage(@NonNull String name) {
if (!fullyLoaded) {
// we are loading a new style
return;
}
nativeMapView.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) {
if (!fullyLoaded) {
// we are loading a new style
return null;
}
return nativeMapView.getImage(id);
}
//
// Transition
//
/**
*
* Set the transition duration for style changes.
*
* The default value for delay and duration is zero, so any changes take effect without animation.
*
* @param transitionOptions the transition options
*/
public void setTransition(@NonNull TransitionOptions transitionOptions) {
if (!fullyLoaded) {
throw new IllegalStateException(
"Calling setTransition on old Style instance, use the more recently loaded Style instead."
);
}
nativeMapView.setTransitionDuration(transitionOptions.getDuration());
nativeMapView.setTransitionDelay(transitionOptions.getDelay());
}
/**
*
* Get the transition for style changes.
*
* The default value for delay and transition is zero, so any changes take effect without animation.
*
* @return TransitionOptions the transition options
*/
@NonNull
public TransitionOptions getTransition() {
if (!fullyLoaded) {
// we are loading a new style
return new TransitionOptions(0, 0);
}
return new TransitionOptions(nativeMapView.getTransitionDuration(), nativeMapView.getTransitionDelay());
}
//
// Light
//
/**
* Get the light source used to change lighting conditions on extruded fill layers.
*
* @return the global light source
*/
@Nullable
public Light getLight() {
if (!fullyLoaded) {
// we are loading a new style
return null;
}
return nativeMapView.getLight();
}
/**
* 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() {
for (Source source : sources.values()) {
if (source != null) {
source.setDetached();
nativeMapView.removeSource(source);
}
}
for (Layer layer : layers.values()) {
if (layer != null) {
layer.setDetached();
nativeMapView.removeLayer(layer);
}
}
for (Map.Entry bitmapEntry : images.entrySet()) {
nativeMapView.removeImage(bitmapEntry.getKey());
bitmapEntry.getValue().recycle();
}
sources.clear();
layers.clear();
images.clear();
fullyLoaded = false;
}
/**
* 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;
}
//
// 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//