package com.qozix.mapview;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.view.View;
import com.qozix.geom.Coordinate;
import com.qozix.layouts.ZoomPanLayout;
import com.qozix.mapview.geom.ManagedGeolocator;
import com.qozix.mapview.hotspots.HotSpotManager;
import com.qozix.mapview.markers.CalloutManager;
import com.qozix.mapview.markers.MarkerManager;
import com.qozix.mapview.paths.PathManager;
import com.qozix.mapview.tiles.MapTileDecoder;
import com.qozix.mapview.tiles.MapTileEnhancer;
import com.qozix.mapview.tiles.TileManager;
import com.qozix.mapview.tiles.TileRenderListener;
import com.qozix.mapview.viewmanagers.DownsampleManager;
import com.qozix.mapview.viewmanagers.ViewCurator;
import com.qozix.mapview.zoom.ZoomLevel;
import com.qozix.mapview.zoom.ZoomListener;
import com.qozix.mapview.zoom.ZoomManager;
/**
* The MapView widget is a subclass of ViewGroup that provides a mechanism to asynchronously display tile-based images,
* with additional functionality for 2D dragging, flinging, pinch or double-tap to zoom, adding overlaying Views (markers),
* multiple levels of detail, and support for faux-geolocation (by specifying top-left and bottom-right coordinates).
*
* <p>It might be best described as a hybrid of com.google.android.maps.MapView and iOS's CATiledLayer, and is appropriate for a variety of uses
* but was intended for map-type applications, especially high-detail or custom implementations (e.g., inside a building).</p>
*
* <p>A minimal implementation might look like this:</p>
*
* <pre>{@code
* MapView mapView = new MapView(this);
* mapView.addZoomLevel(3090, 2536, "path/to/tiles/%col%-%row%.jpg");
* }</pre>
*
* A more advanced implementation might look like this:
* <pre>{@code
* MapView mapView = new MapView(this);
* mapView.addMapEventListener(someMapEventListener);
* mapView.registerGeolocator(42.379676, -71.094919, 42.346550, -71.040280);
* mapView.addZoomLevel(6180, 5072, "tiles/boston-1000-%col%_%row%.jpg", 256, 256);
* mapView.addZoomLevel(3090, 2536, "tiles/boston-500-%col%_%row%.jpg", 256, 256);
* mapView.addZoomLevel(1540, 1268, "tiles/boston-250-%col%_%row%.jpg", 256, 256);
* mapView.addZoomLevel(770, 634, "tiles/boston-125-%col%_%row%.jpg", 128, 128);
* mapView.addMarker(someView, 42.35848, -71.063736);
* mapView.addMarker(anotherView, 42.3665, -71.05224);
* }</pre>
*
* <p>Licensed under <a href="http://creativecommons.org/licenses/by/3.0/legalcode" target="_blank">Creative Commons</a></p>
*/
public class MapView extends ZoomPanLayout {
private HashSet<MapEventListener> mapEventListeners = new HashSet<MapEventListener>();
private ZoomManager zoomManager = new ZoomManager();
private HotSpotManager hotSpotManager = new HotSpotManager();
private DownsampleManager downsampleManager = new DownsampleManager();
private TileManager tileManager;
private MarkerManager markerManager;
private PathManager pathManager;
private CalloutManager calloutManager;
private ManagedGeolocator geolocator;
private boolean isUsingGeolocation;
/**
* Constructor to use when creating a MapView from code. Inflating from XML is not currently supported.
* @param context (Context) The Context the MapView is running in, through which it can access the current theme, resources, etc.
*/
public MapView( Context context ) {
super( context );
tileManager = new TileManager( context, zoomManager );
super.addChild( tileManager );
pathManager = new PathManager( context, zoomManager );
super.addChild( pathManager );
markerManager = new MarkerManager( context, zoomManager );
super.addChild( markerManager );
calloutManager = new CalloutManager( context, zoomManager );
super.addChild( calloutManager );
updateClipFromCurrentZoom();
zoomManager.addZoomListener( zoomListener );
tileManager.setTileRenderListener( renderListener );
addZoomPanListener( zoomPanListener );
addGestureListener( gestureListener );
}
//------------------------------------------------------------------------------------
// PUBLIC API
//------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------
// Getters/Setters
//------------------------------------------------------------------------------------
/**
* Get the current zoom level (index) used by the MapView
* @return (int) current zoom level used by the MapView
*/
public int getZoom(){
return zoomManager.getZoom();
}
/**
* Changes the zoom level used by the MapView.
* The value is an index and represent the range 0 to n,
* where n = the total number of zoom levels added
* @param zoom (int) zoom index to use
*/
public void setZoom( int zoom ) {
zoomManager.setZoom( zoom );
setScale( zoomManager.getScale() );
}
/**
* Register a map event listener callback object for this MapView.
* @param listener (MapEventListener) an implementation of the MapEventListener interface
*/
public void addMapEventListener( MapEventListener listener ) {
mapEventListeners.add( listener );
}
/**
* Removes a MapEventListener object from those listening to this MapView.
* @param listener (MapEventListener) an implementation of the MapEventListener interface
*/
public void removeMapEventListener( MapEventListener listener ) {
mapEventListeners.remove( listener );
}
//------------------------------------------------------------------------------------
// Rendering API
//------------------------------------------------------------------------------------
/**
* Request that the current tile set is re-examined and re-drawn.
* The request is added to a queue and is not guaranteed to be processed at any particular
* time, and will never be handled immediately.
*/
public void requestRender(){
tileManager.requestRender();
}
/**
* Notify the map view that it may stop rendering tiles. The rendering thread will be
* attempt to be interrupted, but no guarantee is provided when.
*/
public void cancelRender() {
tileManager.cancelRender();
}
/**
* Enables or disables map-tile image caching (in-memory and on-disk)
* @param shouldCache (boolean) true to enable caching, false to disable it (default)
*/
public void setCacheEnabled( boolean shouldCache ) {
tileManager.setCacheEnabled( shouldCache );
}
/**
* Sets a custom class to perform the decode operation when tile bitmaps are requested.
* By default, a MapTileDecoder implementation is provided that renders bitmaps from the context's Assets,
* but alternative implementations could be used that fetch images via HTTP, or from the SD card, or resources, SVG, etc.
* {@link MapTileDecoderHttp} is an example of such an implementation.
* @param decoder (MapTileDecoder) A class instance that implements MapTileDecoder, and must define a decode method, which accepts a String file name and a Context object, and returns a Bitmap
*/
public void setTileDecoder( MapTileDecoder decoder ) {
tileManager.setDecoder( decoder );
}
public void setTileEnhancer( MapTileEnhancer enhancer ) {
tileManager.setEnhancer( enhancer );
}
//------------------------------------------------------------------------------------
// Zoom Management API
//------------------------------------------------------------------------------------
/**
* Register a tile set to be used for a particular zoom level.
* Each tile set to be used must be registered using this method,
* and at least one zoom level must be registered for the MapView to render any tiles.
* Zoom levels will be ordered from smallest to largest, based on the area (width * height)
*
* @param wide (int) total width of the tile set
* @param tall (int) total height of the tile set
* @param pattern (String) string path to the location of the map tiles (in the assets directory), with %col% and %row% to be replaced by their respective integer positions. E.g., "folder/tile-%col%-%row%.jpg"
*/
public void addZoomLevel( int wide, int tall, String pattern ) {
zoomManager.addZoomLevel(wide, tall, pattern );
}
/**
* Register a tile set to be used for a particular zoom level.
* Each tile set to be used must be registered using this method,
* and at least one zoom level must be registered for the MapView to render any tiles.
* Zoom levels will be ordered from smallest to largest, based on the area (width * height)
*
* @param wide (int) total width of the tile set
* @param tall (int) total height of the tile set
* @param pattern (String) string path to the location of the map tiles (in the assets directory), with %col% and %row% to be replaced by their respective integer positions. E.g., "folder/tile-%col%-%row%.jpg"
* @param downsample (String) string path to the location of an optional non-tiled image to serve as the background for a ZoomLevel. It's recommended that this image is low-res and small in size.
*/
public void addZoomLevel( int wide, int tall, String pattern, String downsample ) {
zoomManager.addZoomLevel(wide, tall, pattern, downsample );
}
/**
* Register a tile set to be used for a particular zoom level.
* Each tile set to be used must be registered using this method,
* and at least one zoom level must be registered for the MapView to render any tiles.
* Zoom levels will be ordered from smallest to largest, based on the area (width * height)
*
* @param wide (int) total width of the tile set
* @param tall (int) total height of the tile set
* @param pattern (String) string path to the location of the map tiles (in the assets directory), with %col% and %row% to be replaced by their respective integer positions. E.g., "folder/tile-%col%-%row%.jpg"
* @param tileWidth (int) size of each tiled column
* @param tileHeight (int) size of each tiled row
*/
public void addZoomLevel( int wide, int tall, String pattern, int tileWidth, int tileHeight ){
zoomManager.addZoomLevel( wide, tall, pattern, tileWidth, tileHeight );
}
/**
* Register a tile set to be used for a particular zoom level.
* Each tile set to be used must be registered using this method,
* and at least one zoom level must be registered for the MapView to render any tiles.
* Zoom levels will be ordered from smallest to largest, based on the area (width * height)
*
* @param wide (int) total width of the tile set
* @param tall (int) total height of the tile set
* @param pattern (String) string path to the location of the map tiles (in the assets directory), with %col% and %row% to be replaced by their respective integer positions. E.g., "folder/tile-%col%-%row%.jpg"
* @param downsample (String) string path to the location of an optional non-tiled image to serve as the background for a ZoomLevel. It's recommended that this image is low-res and small in size.
* @param tileWidth (int) size of each tiled column
* @param tileHeight (int) size of each tiled row
*/
public void addZoomLevel( int wide, int tall, String pattern, String downsample, int tileWidth, int tileHeight ){
zoomManager.addZoomLevel( wide, tall, pattern, downsample, tileWidth, tileHeight );
}
/**
* Clear all previously registered zoom levels. This method is experimental.
*/
public void resetZoomLevels(){
zoomManager.resetZoomLevels();
}
/**
* While zoom is locked (after this method is invoked, and before unlockZoom is invoked),
* the ZoomLevel will not change, and the current ZoomLevel will be scaled beyond the normal
* bounds. Normally, once a ZoomLevel reach 0.5 or lower scale, the manager searches for the
* next smallest ZoomLevel and activates it, changing the tiles set et al. While zoom is locked,
* this does not occur
*/
public void lockZoom(){
zoomManager.lockZoom();
}
/**
* Unlocks a ZoomLevel locked with lockZoom
*/
public void unlockZoom(){
zoomManager.unlockZoom();
}
//------------------------------------------------------------------------------------
// Geolocation API
//------------------------------------------------------------------------------------
/**
* Register a set of offset points to use when calculating position within the MapView.
* This can be used for lat/lng coordinates instead of pixel-based positioning.
* If a geolocation rectangle is specified, many positioning methods will be able to translate coordinates into
* relative pixel values.
* To remove this process, use unregisterGeolocator
* @param left (double) the left edge of the rectangle used when calculating position (longitude of the bottom-right coordinate)
* @param top (double) the top edge of the rectangle used when calculating position (latitude of the top-left coordinate)
* @param right (double) the right edge of the rectangle used when calculating position (longitude of the top-left coordinate)
* @param bottom (double) the bottom edge of the rectangle used when calculating position (latitude of the bottom-right coordinate)
*/
public void registerGeolocator(double left, double top, double right, double bottom ){
isUsingGeolocation = true;
if ( geolocator == null ) {
geolocator = new ManagedGeolocator( zoomManager );
isUsingGeolocation = true;
}
geolocator.setCoordinates( left, top, right, bottom );
}
/**
* Removes the geolocation rectangle from this MapView's registry. Positioning methods will use pixels values.
*/
public void unregisterGeolocator(){
isUsingGeolocation = false;
geolocator = null;
}
/**
* Converts pixels values to coordinates using the geolocation rectangle provided by @link #registerGeolocator(double, double, double, double)
* @throws UnsupportedOperationException if no geolocation rectangle has been previously registered.
* @param x (double) pixel value of the x-axis to be translated
* @param y (double) pixel value of the y-axis to be translated
* @return (double[]) a 2-element array of coordinates, with latitude at position 0 and longitude at position 1
*/
public double[] pixelsToLatLng( double x, double y ){
if( !isUsingGeolocation ) {
throw new UnsupportedOperationException("MapView requires that a geolocation rectangle is registered before it can translate pixels to Coordinates");
}
double[] position = new double[2];
Point point = new Point( (int) x, (int) y );
Coordinate coordinate = geolocator.translate( point );
position[0] = coordinate.latitude;
position[1] = coordinate.longitude;
return position;
}
/**
* Converts pixels values to coordinates using the geolocation rectangle provided by @link #registerGeolocator(double, double, double, double)
* @throws UnsupportedOperationException if no geolocation rectangle has been previously registered.
* @param points (double[]) 2-element array of pixel values. The x-value should be at position 0, and the y-value at position 1
* @return (double[]) a 2-element array of coordinates, with latitude at position 0 and longitude at position 1
*/
public double[] pixelsToLatLng( double[] points ) {
return pixelsToLatLng( points[0], points[1] );
}
/**
* Converts coordinates values to pixels using the geolocation rectangle provided by @link #registerGeolocator(double, double, double, double)
* @throws UnsupportedOperationException if no geolocation rectangle has been previously registered.
* @param lat (double) latitude of the value to be translated to y-axis pixel
* @param lng (double) longitude of the value to be translated to x-axis pixel
* @return (double[]) a 2-element array of pixel positions, with x at position 0 and y at position 1
*/
public double[] latLngToPixels( double lat, double lng ){
if( !isUsingGeolocation ) {
throw new UnsupportedOperationException("MapView requires that a geolocation rectangle is registered before it can translate coordinates to pixels");
}
double[] position = new double[2];
Coordinate coordinate = new Coordinate( lat, lng );
Point point = geolocator.translate( coordinate );
position[0] = point.x;
position[1] = point.y;
return position;
}
/**
* Converts coordinates values to pixels using the geolocation rectangle provided by @link #registerGeolocator(double, double, double, double)
* @throws UnsupportedOperationException if no geolocation rectangle has been previously registered.
* @param points (double[]) 2-element array of pixel values. Latitude should be at position 0, and longitude at position 1
* @return (double[]) a 2-element array of pixel positions, with x at position 0 and y at position 1
*/
public double[] latLngToPixels( double[] coordinates ) {
return latLngToPixels( coordinates[0], coordinates[1] );
}
//------------------------------------------------------------------------------------
// Positioning API
//------------------------------------------------------------------------------------
/**
* Scroll the MapView to the x and y positions provided.
* This signature will use pixel values if no geolocator is registered, otherwise
* it will compute the coordinates based on the rectangle provided to the
* geolocation registration. Equivalent to moveTo( x, y, false)
* @param x (double) x position the MapView should focus to
* @param y (double) y position the MapView should focus to
*/
public void moveTo( double x, double y ){
moveTo( x, y, false );
}
/**
* Scroll the MapView to the x and y positions provided.
* This signature will use pixel values if no geolocator is registered or if @param
* absolute is false, otherwise it will compute the coordinates based on the
* rectangle provided to the geolocation registration.
* @param x (double) x position the MapView should focus to
* @param y (double) y position the MapView should focus to
* @param absolute (boolean) true to always use pixel values and omit geolocation translation
*/
public void moveTo( double x, double y, boolean absolute ) {
int[] position = getPosition( x, y, absolute );
Point point = new Point( position[0], position[1] );
point.x *= getScale();
point.y *= getScale();
scrollToPoint( point );
}
/**
* Scroll the MapView to the x and y positions provided and center to the screen.
* This signature will use pixel values if no geolocator is registered, otherwise
* it will compute the coordinates based on the rectangle provided to the
* geolocation registration. Equivalent to moveToAndCenter( x, y, false)
* @param x (double) x position the MapView should focus to
* @param y (double) y position the MapView should focus to
*/
public void moveToAndCenter( double x, double y ) {
moveToAndCenter( x, y, false );
}
/**
* Scroll the MapView to the x and y positions provided and center to the screen.
* This signature will use pixel values if no geolocator is registered or if @param
* absolute is false, otherwise it will compute the coordinates based on the
* rectangle provided to the geolocation registration.
* @param x (double) x position the MapView should focus to
* @param y (double) y position the MapView should focus to
* @param absolute (boolean) true to always use pixel values and omit geolocation translation
*/
public void moveToAndCenter( double x, double y, boolean absolute ) {
int[] position = getPosition( x, y, absolute );
Point point = new Point( position[0], position[1] );
point.x *= getScale();
point.y *= getScale();
scrollToAndCenter( point );
requestRender();
}
/**
* Scroll the MapView to the x and y positions provided, using scrolling animation.
* This signature will use pixel values if no geolocator is registered, otherwise
* it will compute the coordinates based on the rectangle provided to the
* geolocation registration. Equivalent to slideTo( x, y, false)
* @param x (double) x position the MapView should focus to
* @param y (double) y position the MapView should focus to
*/
public void slideTo( double x, double y ){
slideTo( x, y, false );
}
/**
* Scroll the MapView to the x and y positions provided, using scrolling animation.
* This signature will use pixel values if no geolocator is registered or if @param
* absolute is false, otherwise it will compute the coordinates based on the rectangle
* provided to the geolocation registration.
* @param x (double) x position the MapView should focus to
* @param y (double) y position the MapView should focus to
* @param absolute (boolean) true to always use pixel values and omit geolocation translation
*/
public void slideTo( double x, double y, boolean absolute ){
int[] position = getPosition( x, y, absolute );
Point point = new Point( position[0], position[1] );
point.x *= getScale();
point.y *= getScale();
slideToPoint( point );
}
/**
* Scroll the MapView to the x and y positions provide and center to the screen,
* using scrolling animation. This signature will use pixel values if no geolocator
* is registered otherwise it will compute the coordinates based on the rectangle
* provided to the geolocation registration.
* @param x (double) x position the MapView should focus to
* @param y (double) y position the MapView should focus to
*/
public void slideToAndCenter( double x, double y ) {
slideToAndCenter( x, y, false );
}
/**
* Scroll the MapView to the x and y positions provide and center to the screen,
* using scrolling animation. This signature will use pixel values if no geolocator
* is registered or if @param absolute is false, otherwise it will compute the
* coordinates based on the rectangle provided to the geolocation registration.
* @param x (double) x position the MapView should focus to
* @param y (double) y position the MapView should focus to
* @param absolute (boolean) true to always use pixel values and omit geolocation translation
*/
public void slideToAndCenter( double x, double y, boolean absolute ) {
int[] position = getPosition( x, y, absolute );
Point point = new Point( position[0], position[1] );
point.x *= getScale();
point.y *= getScale();
slideToAndCenter( point );
}
//------------------------------------------------------------------------------------
// Marker, Callout and HotSpot API
//------------------------------------------------------------------------------------
/**
* Markers added to this MapView will have anchor logic applied on the values provided here.
* E.g., setMarkerAnchorPoints(0.5f, 1.0f) will have markers centered horizontally, positioned
* vertically to a value equal to 0 - height.
* Note that individual markers can be assigned specific anchors - this method applies a default
* value to all markers added without specifying anchor values.
* @param anchorX (float) the x-axis position of a marker will be offset by a number equal to the negative width of the marker multiplied by this value
* @param anchorY (float) the y-axis position of a marker will be offset by a number equal to the negative height of the marker multiplied by this value
*/
public void setMarkerAnchorPoints( float anchorX, float anchorY ) {
markerManager.setAnchors( anchorX, anchorY );
}
/**
* Add a marker to the the MapView. The marker can be any View.
* No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
* @param view (View) View instance to be added to the MapView
* @param x (double) x position the View instance should be positioned at
* @param y (double) y position the View instance should be positioned at
* @return (View) the View instance added to the MapView
*/
public View addMarker( View view, double x, double y ){
return addMarker( view, x, y, false );
}
/**
* Add a marker to the the MapView. The marker can be any View.
* No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
* @param view (View) View instance to be added to the MapView
* @param x (double) x position the View instance should be positioned at
* @param y (double) y position the View instance should be positioned at
* @param absolute (boolean) true to always use pixel values and omit geolocation translation
* @return (View) the View instance added to the MapView
*/
public View addMarker( View view, double x, double y, boolean absolute ){
int[] position = getPosition( x, y, absolute );
markerManager.addMarker( view, position[0], position[1] );
return view;
}
/**
* Add a marker to the the MapView. The marker can be any View.
* No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
* @param view (View) View instance to be added to the MapView
* @param x (double) x position the View instance should be positioned at
* @param y (double) y position the View instance should be positioned at
* @param aX (float) the x-axis position of a marker will be offset by a number equal to the negative width of the marker multiplied by this value
* @param aY (float) the y-axis position of a marker will be offset by a number equal to the negative height of the marker multiplied by this value
* @return (View) the View instance added to the MapView
*/
public View addMarker( View view, double x, double y, float aX, float aY ){
return addMarker( view, x, y, aX, aY, false );
}
/**
* Add a marker to the the MapView. The marker can be any View.
* No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
* @param view (View) View instance to be added to the MapView
* @param x (double) x position the View instance should be positioned at
* @param y (double) y position the View instance should be positioned at
* @param aX (float) the x-axis position of a marker will be offset by a number equal to the negative width of the marker multiplied by this value
* @param aY (float) the y-axis position of a marker will be offset by a number equal to the negative height of the marker multiplied by this value
* @param absolute (boolean) true to always use pixel values and omit geolocation translation
* @return (View) the View instance added to the MapView
*/
public View addMarker( View view, double x, double y, float aX, float aY, boolean absolute ){
int[] position = getPosition( x, y, absolute );
markerManager.addMarker( view, position[0], position[1], aX, aY );
return view;
}
/**
* Add a marker to the the MapView. The marker can be any View.
* No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
* This signature requires an additional parameter that represents the zoom level (int) that this marker should be shown at.
* At any other zoom level, the marker View will have it's visibility set to View.NONE
* @param view (View) View instance to be added to the MapView
* @param x (double) x position the View instance should be positioned at
* @param y (double) y position the View instance should be positioned at
* @param zoom (int) the zoom level (index) that this view should be shown at
* @return (View) the View instance added to the MapView
*/
public View addMarkerAtZoom( View view, double x, double y, int zoom ){
return addMarkerAtZoom( view, x, y, zoom, false );
}
/**
* Add a marker to the the MapView. The marker can be any View.
* No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
* This signature requires an additional parameter that represents the zoom level (int) that this marker should be shown at.
* At any other zoom level, the marker View will have it's visibility set to View.NONE
* @param view (View) View instance to be added to the MapView
* @param x (double) x position the View instance should be positioned at
* @param y (double) y position the View instance should be positioned at
* @param zoom (int) the zoom level (index) that this view should be shown at
* @param absolute (boolean) true to always use pixel values and omit geolocation translation
* @return (View) the View instance added to the MapView
*/
public View addMarkerAtZoom( View view, double x, double y, int zoom, boolean absolute ){
int[] position = getPosition( x, y, absolute );
markerManager.addMarkerAtZoom( view, position[0], position[1], zoom );
return view;
}
/**
* Add a marker to the the MapView. The marker can be any View.
* No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
* This signature requires an additional parameter that represents the zoom level (int) that this marker should be shown at.
* At any other zoom level, the marker View will have it's visibility set to View.NONE
* @param view (View) View instance to be added to the MapView
* @param x (double) x position the View instance should be positioned at
* @param y (double) y position the View instance should be positioned at
* @param aX (float) the x-axis position of a marker will be offset by a number equal to the negative width of the marker multiplied by this value
* @param aY (float) the y-axis position of a marker will be offset by a number equal to the negative height of the marker multiplied by this value
* @param zoom (int) the zoom level (index) that this view should be shown at
* @return (View) the View instance added to the MapView
*/
public View addMarkerAtZoom( View view, double x, double y, float aX, float aY, int zoom ){
return addMarkerAtZoom( view, x, y, aX, aY, zoom, false );
}
/**
* Add a marker to the the MapView. The marker can be any View.
* No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
* This signature requires an additional parameter that represents the zoom level (int) that this marker should be shown at.
* At any other zoom level, the marker View will have it's visibility set to View.NONE
* @param view (View) View instance to be added to the MapView
* @param x (double) x position the View instance should be positioned at
* @param y (double) y position the View instance should be positioned at
* @param aX (float) the x-axis position of a marker will be offset by a number equal to the negative width of the marker multiplied by this value
* @param aY (float) the y-axis position of a marker will be offset by a number equal to the negative height of the marker multiplied by this value
* @param zoom (int) the zoom level (index) that this view should be shown at
* @param absolute (boolean) true to always use pixel values and omit geolocation translation
* @return (View) the View instance added to the MapView
*/
public View addMarkerAtZoom( View view, double x, double y, float aX, float aY, int zoom, boolean absolute ){
int[] position = getPosition( x, y, absolute );
markerManager.addMarkerAtZoom( view, position[0], position[1], aX, aY, zoom );
return view;
}
/**
* Removes a marker View from the MapView's view tree.
* @param view The marker View to be removed.
* @return (boolean) true if the view was in the view tree and was removed, false if it was not in the view tree
*/
public boolean removeMarker( View view ) {
if( markerManager.indexOfChild( view ) > -1 ) {
markerManager.removeView( view );
return true;
}
return false;
}
public void removeAllMarkers() {
markerManager.removeAllViews();
markerManager.invalidate();
}
public void updateMarker(Object tag, double x, double y) {
for (int i = 0; i < markerManager.getChildCount(); i++) {
if (markerManager.getChildAt(i).getTag() == tag) {
markerManager.getChildAt(i).setX((float)x);
markerManager.getChildAt(i).setY((float)y);
}
}
}
/**
* Add a callout to the the MapView. The callout can be any View.
* No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
* Callout views will always be positioned at the top of the view tree (at the highest z-index), and will always be removed during any touch event
* that is not consumed by the callout View.
* @param view (View) View instance to be added to the MapView
* @param x (double) x position the View instance should be positioned at
* @param y (double) y position the View instance should be positioned at
* @return (View) the View instance added to the MapView
*/
public View addCallout( View view, double x, double y ){
return addCallout( view, x, y, false );
}
/**
* Add a callout to the the MapView. The callout can be any View.
* No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
* Callout views will always be positioned at the top of the view tree (at the highest z-index), and will always be removed during any touch event
* that is not consumed by the callout View.
* @param view (View) View instance to be added to the MapView
* @param x (double) x position the View instance should be positioned at
* @param y (double) y position the View instance should be positioned at
* @param absolute (boolean) true to always use pixel values and omit geolocation translation
* @return (View) the View instance added to the MapView
*/
public View addCallout( View view, double x, double y, boolean absolute ){
int[] position = getPosition( x, y, absolute );
calloutManager.addMarker( view, position[0], position[1] );
return view;
}
/**
* Add a callout to the the MapView. The callout can be any View.
* No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
* Callout views will always be positioned at the top of the view tree (at the highest z-index), and will always be removed during any touch event
* that is not consumed by the callout View.
* @param view (View) View instance to be added to the MapView
* @param x (double) x position the View instance should be positioned at
* @param y (double) y position the View instance should be positioned at
* @param aX (float) the x-axis position of a callout view will be offset by a number equal to the negative width of the callout view multiplied by this value
* @param aY (float) the y-axis position of a callout view will be offset by a number equal to the negative height of the callout view multiplied by this value
* @return (View) the View instance added to the MapView
*/
public View addCallout( View view, double x, double y, float aX, float aY ){
return addCallout( view, x, y, aX, aY, false );
}
/**
* Add a callout to the the MapView. The callout can be any View.
* No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters
* Callout views will always be positioned at the top of the view tree (at the highest z-index), and will always be removed during any touch event
* that is not consumed by the callout View.
* @param view (View) View instance to be added to the MapView
* @param x (double) x position the View instance should be positioned at
* @param y (double) y position the View instance should be positioned at
* @param aX (float) the x-axis position of a callout view will be offset by a number equal to the negative width of the callout view multiplied by this value
* @param aY (float) the y-axis position of a callout view will be offset by a number equal to the negative height of the callout view multiplied by this value
* @param absolute (boolean) true to always use pixel values and omit geolocation translation
* @return (View) the View instance added to the MapView
*/
public View addCallout( View view, double x, double y, float aX, float aY, boolean absolute ){
int[] position = getPosition( x, y, absolute );
calloutManager.addMarker( view, position[0], position[1], aX, aY );
return view;
}
/**
* Removes a callout View from the MapView's view tree.
* @param view The callout View to be removed.
* @return (boolean) true if the view was in the view tree and was removed, false if it was not in the view tree
*/
public boolean removeCallout( View view ) {
if( calloutManager.indexOfChild( view ) > -1 ) {
calloutManager.removeView( view );
return true;
}
return false;
}
/**
* Register a rectangle that should fire an OnClickListener when a touch event occurs that intersects that rectangle.
* The rectangle moves and scales with the map view.
* Note that while the second parameter takes a View.OnClickListener instance, the View instance passed to the OnClick method
* will be null, since the HotSpot is not an actual View but an abstract representation of one.
* @param rectangle (Rect) the rectangle that is tested against touch events that occur on the MapView
* @param listener (View.OnClickListener) the OnClickListener instance that is fired if the rectangle intersects a touch event
*/
public void addHotSpot( Rect rectangle, View.OnClickListener listener ){
hotSpotManager.addHotSpot( rectangle, listener );
}
/**
* Remove a hotspot registered with addHotSpot
* @param rectangle (Rect) the rectangle to test for
* @param listener (View.OnClickListener) the listener that was registered to this hotspot
* @return (boolean) true if a hotspot was removed, false if not
*/
public void removeHotSpot( Rect rectangle, View.OnClickListener listener ) {
hotSpotManager.removeHotSpot( rectangle, listener );
}
//------------------------------------------------------------------------------------
// Marker, Callout and HotSpot API
//------------------------------------------------------------------------------------
/**
* Draw a path (line) on the MapView that will be rendered beneath any Marker or Callout views.
* @param positions (List<double[]>) List of 2-element double arrays. Each element represents a position; each position represents a point.
* @return (View) the PathView instance that represents the line drawn. The PathView instance has methods that allow manipulation of the
* path width, color, shadow, and corner effect, but is not documented at this time.
*/
public View drawPath( List<double[]> positions ) {
LinkedList<Point> points = getPath( positions );
return pathManager.drawPath( points );
}
/**
* Draw a path (line) on the MapView that will be rendered beneath any Marker or Callout views.
* @param positions (double[]...) array (or var-args) of 2-element double arrays. Each element or argument represents a position; each position represents a point.
* @return (View) the PathView instance that represents the line drawn. The PathView instance has methods that allow manipulation of the
* path width, color, shadow, and corner effect, but is not documented at this time.
*/
public View drawPath(double[]... positions ) {
return drawPath( Arrays.asList( positions ));
}
/**
* Draw a path (line) on the MapView that will be rendered beneath any Marker or Callout views.
* This signature requires an additional parameter that represents the zoom level (int) that this path should be shown at.
* At any other zoom level, the path will not be displayed.
* @param zoom (int) the zoom level (index) that this view should be shown at
* @param positions (List<double[]>) List of 2-element double arrays. Each element represents a position; each position represents a point.
* @return (View) the PathView instance that represents the line drawn. The PathView instance has methods that allow manipulation of the
* path width, color, shadow, and corner effect, but is not documented at this time.
*/
public View drawPathAtZoom( int zoom, List<double[]> positions) {
LinkedList<Point> points = getPath( positions );
return pathManager.drawPathAtZoom( points, zoom );
}
/**
* Draw a path (line) on the MapView that will be rendered beneath any Marker or Callout views.
* This signature requires an additional parameter that represents the zoom level (int) that this path should be shown at.
* At any other zoom level, the path will not be displayed.
* @param zoom (int) the zoom level (index) that this view should be shown at
* @param positions (double[]...) array (or var-args) of 2-element double arrays. Each element or argument represents a position; each position represents a point.
* @return (View) the PathView instance that represents the line drawn. The PathView instance has methods that allow manipulation of the
* path width, color, shadow, and corner effect, but is not documented at this time.
*/
public View drawPathAtZoom(int zoom, double[]... positions ){
return drawPathAtZoom( zoom, Arrays.asList( positions ) );
}
/**
* Removes a path View from the MapView's view tree.
* @param view The path View to be removed.
* @return (boolean) true if the view was in the view tree and was removed, false if it was not in the view tree
*/
public boolean removePath( View view ) {
if( pathManager.indexOfChild( view ) > -1 ) {
pathManager.removeView( view );
return true;
}
return false;
}
//------------------------------------------------------------------------------------
// Memory Management API
//------------------------------------------------------------------------------------
/**
* Clear bitmap tiles, appropriate for onPause.
* (Currently not clearing downsample - might be worthwhile)
*/
public void clear() {
tileManager.clear();
}
/**
* Clear bitmap tiles and remove all views, appropriate for onDestroy
* References to MapView should be set to null following invocations of this method.
*/
public void destroy() {
tileManager.clear();
ViewCurator.clear( this );
}
//------------------------------------------------------------------------------------
// PRIVATE API
//------------------------------------------------------------------------------------
/**
* @throws UnsupportedOperationException MapView does not allow direct insertion of child views.
*/
@Override
public void addChild( View view ) {
throw new UnsupportedOperationException( "MapView does not allow direct insertion of child views." );
}
// make sure we keep the viewport UTD, and if layout changes we'll need to recompute what tiles to show
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout( changed, l, t, r, b );
if ( changed ) {
updateViewport();
requestRender();
}
}
// set clipping bounds to the scaled size of the current zoom level
private void updateClipFromCurrentZoom(){
int w = zoomManager.getComputedCurrentWidth();
int h = zoomManager.getComputedCurrentHeight();
setSize( w, h );
}
// show downsampled background image for the current zoom level
private void updateDownsample() {
ZoomLevel zoomLevel = zoomManager.getCurrentZoomLevel();
String downsample = zoomLevel.getDownsample();
downsampleManager.setDownsample( getClip(), downsample );
}
// let the zoom manager know what tiles to show based on our position and dimensions
private void updateViewport(){
int left = getScrollX();
int top = getScrollY();
int right = left + getWidth();
int bottom = top + getHeight();
zoomManager.updateViewport( left, top, right, bottom );
}
// tell the tile renderer to not start any more tasks, but it can continue with any that are already running
private void suppressRender() {
tileManager.suppressRender();
}
// private geolocation helper
private int[] getPosition( double x, double y, boolean absolute ){
int[] position = new int[2];
if ( !absolute && isUsingGeolocation ){
Coordinate c = new Coordinate( x, y );
Point p = geolocator.translate( c );
x = p.x;
y = p.y;
}
position[0] = (int) x;
position[1] = (int) y;
return position;
}
// private geolocation helper
private LinkedList<Point> getPath( List<double[]> positions ) {
LinkedList<Point> points = new LinkedList<Point>();
if( isUsingGeolocation ) {
for( double[] coordinates : positions ) {
Coordinate coordinate = new Coordinate( coordinates[0], coordinates[1] );
Point point = geolocator.translate( coordinate );
points.add( point );
}
} else {
for( double[] pixels : positions ) {
Point point = new Point( (int) pixels[0], (int) pixels[1] );
points.add( point );
}
}
return points;
}
//------------------------------------------------------------------------------------
// Private Listeners
//------------------------------------------------------------------------------------
private ZoomPanListener zoomPanListener = new ZoomPanListener() {
@Override
public void onZoomPanEvent(){
}
@Override
public void onScrollChanged( int x, int y ) {
updateViewport();
for ( MapEventListener listener : mapEventListeners ) {
listener.onScrollChanged( x, y );
}
}
@Override
public void onScaleChanged( double scale ) {
zoomManager.setScale( scale );
for ( MapEventListener listener : mapEventListeners ) {
listener.onScaleChanged( scale );
}
}
@Override
public void onZoomStart( double scale ) {
zoomManager.lockZoom();
zoomManager.setScale( scale );
for ( MapEventListener listener : mapEventListeners ) {
listener.onZoomStart( scale );
}
}
@Override
public void onZoomComplete( double scale ) {
zoomManager.unlockZoom();
zoomManager.setScale( scale );
requestRender(); // put this here instead of gesture listener so we catch animations and pinches
for ( MapEventListener listener : mapEventListeners ) {
listener.onZoomComplete( scale );
}
}
};
private ZoomListener zoomListener = new ZoomListener(){
@Override
public void onZoomLevelChanged( int oldZoom, int currentZoom ) {
updateClipFromCurrentZoom();
updateDownsample();
requestRender();
for ( MapEventListener listener : mapEventListeners ) {
listener.onZoomLevelChanged( oldZoom, currentZoom );
}
}
/*
* do *not* update scale in response to changes in the zoom manager
* transactions are one-way - set scale on MapView (ZoomPanLayout)
* and pass those to ZoomManager, which then distributes, manages
* and notifies all other interested parties.
*/
@Override
public void onZoomScaleChanged( double scale ) {
}
};
private GestureListener gestureListener = new GestureListener(){
@Override
public void onDoubleTap( Point point ) {
for ( MapEventListener listener : mapEventListeners ) {
listener.onDoubleTap( point.x, point.y );
}
}
@Override
public void onDrag( Point point ) {
suppressRender();
for ( MapEventListener listener : mapEventListeners ) {
listener.onDrag( point.x, point.y );
}
}
@Override
public void onFingerDown( Point point ) {
suppressRender();
for ( MapEventListener listener : mapEventListeners ) {
listener.onFingerDown( point.x, point.y );
}
}
@Override
public void onFingerUp( Point point ) {
if ( !isFlinging() ) {
requestRender();
}
for ( MapEventListener listener : mapEventListeners ) {
listener.onFingerUp( point.x, point.y );
}
}
@Override
public void onFling( Point startPoint, Point finalPoint ) {
suppressRender();
for ( MapEventListener listener : mapEventListeners ) {
listener.onFling( startPoint.x, startPoint.y, finalPoint.x, finalPoint.y );
}
}
@Override
public void onFlingComplete( Point point ) {
requestRender();
for ( MapEventListener listener : mapEventListeners ) {
listener.onFlingComplete( point.x, point.y );
}
}
@Override
public void onPinch( Point point ) {
suppressRender();
for ( MapEventListener listener : mapEventListeners ) {
listener.onPinch( point.x, point.y );
}
}
@Override
public void onPinchComplete( Point point ) {
requestRender();
for ( MapEventListener listener : mapEventListeners ) {
listener.onPinchComplete( point.x, point.y );
}
}
@Override
public void onPinchStart( Point point ) {
suppressRender();
for ( MapEventListener listener : mapEventListeners ) {
listener.onPinchStart( point.x, point.y );
}
}
@Override
public void onTap( Point point ) {
Point scaledPoint = new Point();
scaledPoint.x = (int) ( point.x / getScale() );
scaledPoint.y = (int) ( point.y / getScale() );
hotSpotManager.processHit( scaledPoint );
for ( MapEventListener listener : mapEventListeners ) {
listener.onTap( point.x, point.y );
}
}
@Override
public void onScrollComplete( Point point ) {
requestRender();
for ( MapEventListener listener : mapEventListeners ) {
listener.onScrollChanged( point.x, point.y );
}
}
};
private TileRenderListener renderListener = new TileRenderListener(){
@Override
public void onRenderCancelled() {
}
@Override
public void onRenderComplete() {
for ( MapEventListener listener : mapEventListeners ) {
listener.onRenderComplete();
}
}
@Override
public void onRenderStart() {
for ( MapEventListener listener : mapEventListeners ) {
listener.onRenderStart();
}
}
};
//------------------------------------------------------------------------------------
// Public static interfaces and classes
//------------------------------------------------------------------------------------
/**
* Interface for implementations to recieve MapView events. This interface consolidates several disparate
* listeners (Gestures, ZoomPan Events, MapView events) into a single unit for ease of use.
*/
public static interface MapEventListener {
/**
* Fires when a ACTION_DOWN event is raised from the MapView
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onFingerDown( int x, int y );
/**
* Fires when a ACTION_UP event is raised from the MapView
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onFingerUp( int x, int y );
/**
* Fires while the MapView is being dragged
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onDrag( int x, int y );
/**
* Fires when a user double-taps the MapView
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onDoubleTap( int x, int y );
/**
* Fires when a user taps the MapView
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onTap( int x, int y );
/**
* Fires while a user is pinching the MapView
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onPinch( int x, int y );
/**
* Fires when a user starts a pinch action
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onPinchStart( int x, int y );
/**
* Fires when a user completes a pinch action
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onPinchComplete( int x, int y );
/**
* Fires when a user initiates a fling action
* @param sx (int) the x position of the start of the fling
* @param sy (int) the y position of the start of the fling
* @param dx (int) the x position of the end of the fling
* @param dy (int) the y position of the end of the fling
*/
public void onFling( int sx, int sy, int dx, int dy );
/**
* Fires when a fling action has completed
* @param x (int) the final x scroll position of the MapView after the fling
* @param y (int) the final y scroll position of the MapView after the fling
*/
public void onFlingComplete( int x, int y );
/**
* Fires when the MapView's scale has updated
* @param scale (double) the new scale of the MapView (0-1)
*/
public void onScaleChanged( double scale );
/**
* Fires when the MapView's scroll position has updated
* @param x (int) the new x scroll position of the MapView
* @param y (int) the new y scroll position of the MapView
*/
public void onScrollChanged( int x, int y );
/**
* Fires when a zoom action starts (typically through a pinch of double-tap action,
* or by programmatic animated zoom methods.
* @param scale (double) the new scale of the MapView (0-1)
*/
public void onZoomStart( double scale );
/**
* Fires when a zoom action ends (typically through a pinch of double-tap action,
* or by programmatic animated zoom methods.
* @param scale (double) the new scale of the MapView (0-1)
*/
public void onZoomComplete( double scale );
/**
* Fires when the MapView should start using a new ZoomLevel
* @param oldZoom (int) the zoom level the MapView was using before the change
* @param currentZoom (int) the zoom level the MapView has changed to
*/
public void onZoomLevelChanged( int oldZoom, int currentZoom );
/**
* Fires when the rendering thread has started to update the visible tiles.
*/
public void onRenderStart();
/**
* Fires when the rendering thread has completed updating the visible tiles, but before cleanup
*/
public void onRenderComplete();
}
/**
* Convenience class that implements {@MapEventListener}
*/
public static class MapEventListenerImplementation implements MapEventListener {
/**
* Fires when a ACTION_DOWN event is raised from the MapView
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onFingerDown( int x, int y ) {
}
/**
* Fires when a ACTION_UP event is raised from the MapView
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onFingerUp( int x, int y ) {
}
/**
* Fires while the MapView is being dragged
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onDrag( int x, int y ) {
}
/**
* Fires when a user double-taps the MapView
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onDoubleTap( int x, int y ) {
}
/**
* Fires when a user taps the MapView
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onTap( int x, int y ) {
}
/**
* Fires while a user is pinching the MapView
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onPinch( int x, int y ) {
}
/**
* Fires when a user starts a pinch action
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onPinchStart( int x, int y ) {
}
/**
* Fires when a user completes a pinch action
* @param x (int) the x position of the event
* @param y (int) the y position of the event
*/
public void onPinchComplete( int x, int y ) {
}
/**
* Fires when a user initiates a fling action
* @param sx (int) the x position of the start of the fling
* @param sy (int) the y position of the start of the fling
* @param dx (int) the x position of the end of the fling
* @param dy (int) the y position of the end of the fling
*/
public void onFling( int sx, int sy, int dx, int dy ) {
}
/**
* Fires when a fling action has completed
* @param x (int) the final x scroll position of the MapView after the fling
* @param y (int) the final y scroll position of the MapView after the fling
*/
public void onFlingComplete( int x, int y ) {
}
/**
* Fires when the MapView's scale has updated
* @param scale (double) the new scale of the MapView (0-1)
*/
public void onScaleChanged( double scale ) {
}
/**
* Fires when the MapView's scroll position has updated
* @param x (int) the new x scroll position of the MapView
* @param y (int) the new y scroll position of the MapView
*/
public void onScrollChanged( int x, int y ) {
}
/**
* Fires when a zoom action starts (typically through a pinch of double-tap action,
* or by programmatic animated zoom methods.
* @param scale (double) the new scale of the MapView (0-1)
*/
public void onZoomStart( double scale ) {
}
/**
* Fires when a zoom action ends (typically through a pinch of double-tap action,
* or by programmatic animated zoom methods.
* @param scale (double) the new scale of the MapView (0-1)
*/
public void onZoomComplete( double scale ) {
}
/**
* Fires when the MapView should start using a new ZoomLevel
* @param oldZoom (int) the zoom level the MapView was using before the change
* @param currentZoom (int) the zoom level the MapView has changed to
*/
public void onZoomLevelChanged( int oldZoom, int currentZoom ) {
}
/**
* Fires when the rendering thread has started to update the visible tiles.
*/
public void onRenderStart() {
}
/**
* Fires when the rendering thread has completed updating the visible tiles, but before cleanup
*/
public void onRenderComplete() {
}
}
}