Package de.fhpotsdam.unfolding

Source Code of de.fhpotsdam.unfolding.UnfoldingMap

package de.fhpotsdam.unfolding;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

import org.apache.log4j.Logger;

import processing.core.PApplet;
import processing.core.PVector;
import de.fhpotsdam.unfolding.events.MapEvent;
import de.fhpotsdam.unfolding.events.MapEventListener;
import de.fhpotsdam.unfolding.geo.Location;
import de.fhpotsdam.unfolding.mapdisplay.AbstractMapDisplay;
import de.fhpotsdam.unfolding.mapdisplay.MapDisplayFactory;
import de.fhpotsdam.unfolding.marker.Marker;
import de.fhpotsdam.unfolding.marker.MarkerManager;
import de.fhpotsdam.unfolding.providers.AbstractMapProvider;
import de.fhpotsdam.unfolding.utils.GeoUtils;
import de.fhpotsdam.unfolding.utils.ScreenPosition;
import de.fhpotsdam.utils.Integrator;

/**
* An interactive map. Uses the MapDisplay, and handles hit test, active status, as well as all interactions such as
* panning, zooming, and rotating (with or without tweening).
*
* Acts as facade for the map interactions, e.g. using innerScale for zooming, and outerRotate for rotating.
*
* (c) 2014 Till Nagel, and others. See http://unfoldingmaps.org/contact.html for full credits.
* Licensed under MIT License.
*/
public class UnfoldingMap implements MapEventListener {

  public static final String GREETING_MESSAGE = "Unfolding Map v0.9.6";

  public static final float SCALE_DELTA_IN = 1.05f;
  public static final float SCALE_DELTA_OUT = 1 / 1.05f;

  /** The default tile width of 256px. */
  public static final int TILE_WIDTH = 256;
  /** The default tile height of 256px. */
  public static final int TILE_HEIGHT = 256;
  private static final float PAN_DEFAULT_DELTA = TILE_WIDTH / 2;

  /** UnfoldingMap do not use tweened animation by default. Use {@link #setTweening(boolean)} to switch it on. */
  public static final boolean DEFAULT_TWEENING = false;

  public static final Location PRIME_MERIDIAN_EQUATOR_LOCATION = new Location(0, 0);
  /** The default zoom level of an UnfoldingMap shows the whole world. */
  public static final int DEFAULT_ZOOM_LEVEL = 2;
  private static final float DEFAULT_MIN_SCALE = 1;
  private static final float DEFAULT_MAX_SCALE = 262144; // 2^18

  private static final String MAPCHANGED_METHOD_NAME = "mapChanged";
  private Method mapChangedMethod = null;

  /** The minimum scale. If set the map cannot be further zoomed in. Use {@link #setZoomRange(float, float)}. */
  public float minScale = DEFAULT_MIN_SCALE;
  /** The maximum scale. If set the map cannot be further zoomed out. Use {@link #setZoomRange(float, float)}. */
  public float maxScale = DEFAULT_MAX_SCALE;

  /** The center location of the restricted pan area. */
  protected Location restrictedPanLocation = null;
  /** The maximum distance to the center location of the restricted pan area. */
  protected float maxPanningDistance;

  public static Logger log = Logger.getLogger(UnfoldingMap.class);

  /** Whether Unfolding lib showed a greeting message, i.e. the library version. */
  private static boolean greetingMessageDisplayed = false;

  /** The Processing applet. */
  protected PApplet p;

  /** The display of the map. */
  public AbstractMapDisplay mapDisplay;

  /** The ID of this map. */
  protected String id;

  /** Indicates whether this map is currently active. May be used for non-direct interactions. */
  protected boolean active = true;

  /** Indicates whether to smoothly animate between mapDisplay states. */
  private boolean tweening = DEFAULT_TWEENING;

  /** Tweens the scale. */
  public Integrator scaleIntegrator = new Integrator(1);

  /** Tweens the position. */
  private Integrator txIntegrator = new Integrator(1);
  private Integrator tyIntegrator = new Integrator(1);

  /**
   * Creates a new full canvas map with the given ID.
   *
   * @param p
   *            The main applet.
   * @param id
   *            The ID of this map.
   */
  public UnfoldingMap(PApplet p, String id) {
    this(p, id, 0, 0, p.width, p.height, true, false, null, null);
  }

  /**
   * Creates a new full canvas map with a generated ID.
   *
   * @param p
   *            The main applet.
   */
  public UnfoldingMap(PApplet p) {
    this(p, generateId(), 0, 0, p.width, p.height, true, false, null, null);
  }

  /**
   * Creates a new full canvas map with tiles from the given provider.
   *
   * @param p
   *            The main applet.
   * @param provider
   *            The map tiles provider to use.
   */
  public UnfoldingMap(PApplet p, AbstractMapProvider provider) {
    this(p, generateId(), 0, 0, p.width, p.height, true, false, provider, null);
  }

  /**
   * Creates a new full canvas map with given ID, and with tiles from the given provider.
   *
   * @param p
   *            The main applet.
   * @param id
   *            The ID of this map.
   * @param provider
   *            The map tiles provider to use.
   */
  public UnfoldingMap(PApplet p, String id, AbstractMapProvider provider) {
    this(p, id, 0, 0, p.width, p.height, true, false, provider, null);
  }

  /**
   * Creates a new map with specific position and dimension.
   *
   * @param p
   *            The main applet.
   * @param x
   *            The x position of this map.
   * @param y
   *            The y position of this map.
   * @param width
   *            The width of this map.
   * @param height
   *            The height of this map.
   */
  public UnfoldingMap(PApplet p, float x, float y, float width, float height) {
    this(p, generateId(), x, y, width, height, true, false, null, null);
  }

  public UnfoldingMap(PApplet p, float x, float y, float width, float height, String renderer) {
    this(p, generateId(), x, y, width, height, true, false, null, renderer);
  }

  /**
   * Creates a new map with specific position and dimension.
   *
   * @param p
   *            The main applet.
   * @param id
   *            The ID of this map.
   * @param x
   *            The x position of this map.
   * @param y
   *            The y position of this map.
   * @param width
   *            The width of this map.
   * @param height
   *            The height of this map.
   */
  public UnfoldingMap(PApplet p, String id, float x, float y, float width, float height) {
    this(p, id, x, y, width, height, true, false, null, null);
  }

  public UnfoldingMap(PApplet p, String id, float x, float y, float width, float height, String renderer) {
    this(p, id, x, y, width, height, true, false, null, renderer);
  }

  public UnfoldingMap(PApplet p, String id, float x, float y, float width, float height, boolean useDistortion) {
    this(p, id, x, y, width, height, true, useDistortion, null, null);
  }

  public UnfoldingMap(PApplet p, String id, float x, float y, float width, float height, boolean useDistortion,
      String renderer) {
    this(p, id, x, y, width, height, true, useDistortion, null, renderer);
  }

  /**
   * Creates a new map with specific position and dimension.
   *
   * @param p
   *            The main applet.
   * @param x
   *            The x position of this map.
   * @param y
   *            The y position of this map.
   * @param width
   *            The width of this map.
   * @param height
   *            The height of this map.
   * @param provider
   *            The map tiles provider to use.
   */
  public UnfoldingMap(PApplet p, float x, float y, float width, float height, AbstractMapProvider provider) {
    this(p, generateId(), x, y, width, height, true, false, provider, null);
  }

  public UnfoldingMap(PApplet p, float x, float y, float width, float height, AbstractMapProvider provider,
      String renderer) {
    this(p, generateId(), x, y, width, height, true, false, provider, renderer);
  }

  public UnfoldingMap(PApplet p, String id, float x, float y, float width, float height, boolean useMask,
      boolean useDistortion, AbstractMapProvider provider) {
    this(p, generateId(), x, y, width, height, true, false, provider, null);
  }

  /**
   * Creates a new map with specific position and dimension.
   *
   * @param p
   *            The main applet.
   * @param id
   *            The ID of this map.
   * @param x
   *            The x position of this map.
   * @param y
   *            The y position of this map.
   * @param width
   *            The width of this map.
   * @param height
   *            The height of this map.
   * @param useMask
   *            Whether this map enables using masks (test)
   * @param useDistortion
   *            Whether this map enables using distortion (test)
   * @param provider
   *            The map tiles provider to use.
   */
  public UnfoldingMap(PApplet p, String id, float x, float y, float width, float height, boolean useMask,
      boolean useDistortion, AbstractMapProvider provider, String renderer) {
    this.p = p;
    this.id = id;

    if (!greetingMessageDisplayed) {
      PApplet.println(GREETING_MESSAGE);
      greetingMessageDisplayed = true;
    }

    this.mapDisplay = MapDisplayFactory.getMapDisplay(p, id, x, y, width, height, useMask, useDistortion, provider,
        this, renderer);

    // panCenterZoomTo(PRIME_MERIDIAN_EQUATOR_LOCATION, DEFAULT_ZOOM_LEVEL);

    prepareMapChangedMethod();
  }

  /**
   * Internal method to call listening methods in your application for map changes.
   */
  protected void prepareMapChangedMethod() {
    // Prepare mapChanged method via reflection
    Class<? extends PApplet> appletClass = p.getClass();
    try {
      // First searches for a method with MapEvent as param
      mapChangedMethod = appletClass.getMethod(MAPCHANGED_METHOD_NAME, MapEvent.class);
    } catch (SecurityException e) {
    } catch (NoSuchMethodException e) {
      try {
        // If that failed, searches for a method without
        mapChangedMethod = appletClass.getMethod(MAPCHANGED_METHOD_NAME);
      } catch (SecurityException e2) {
      } catch (NoSuchMethodException e2) {
      }
    }
  }

  /**
   * Check whether all currently visible tiles have been loaded.
   *
   * @return True if all tiles have been loaded, false otherwise.
   */
  public boolean allTilesLoaded() {
    return mapDisplay.allTilesLoaded();
  }

  protected static String generateId() {
    return UUID.randomUUID().toString();
  }

  /**
   * Checks whether the given screen coordinates are on this Map.
   *
   * @param checkX
   *            The vertical position to check.
   * @param checkY
   *            The horizontal position to check.
   * @return True if map is hit, false otherwise.
   */
  public boolean isHit(float checkX, float checkY) {
    float[] check = mapDisplay.getObjectFromScreenPosition(checkX, checkY);
    return (check[0] > 0 && check[0] < mapDisplay.getWidth() && check[1] > 0 && check[1] < mapDisplay.getHeight());
  }

  /**
   * Checks whether the given screen coordinates are on this Map.
   *
   * @param screenPosition
   *            The position to check.
   * @return True if map is hit, false otherwise.
   */
  public boolean isHit(ScreenPosition screenPosition) {
    return isHit(screenPosition.x, screenPosition.y);
  }

  /**
   * Indicates whether this map is currently active. May be used for non-direct interactions.
   *
   * @return True if this map is active, false otherwise.
   */
  public boolean isActive() {
    return active;
  }

  /**
   * Sets the active flag of this map.
   *
   * @param active
   *            True if this map is active, false otherwise.
   */
  public void setActive(boolean active) {
    this.active = active;
  }

  /**
   * Returns the ID of this map.
   *
   * @return A identifying string for this map.
   */
  public String getId() {
    return id;
  }

  /**
   * Updates and draws the map. The main method to display this UnfoldingMap.
   */
  public void draw() {
    updateMap();
    restrictMapToArea();
    mapDisplay.draw();
  }

  /**
   * Internal method to listens to mapDisplay events. This will call any listening methods in your application.
   */
  @Override
  public void onManipulation(MapEvent mapEvent) {
    mapEvent.executeManipulationFor(this);

    // Forward map event to application via reflection
    if (mapChangedMethod != null) {
      try {
        mapChangedMethod.invoke(p, mapEvent);
      } catch (IllegalArgumentException e) {
      } catch (IllegalAccessException e) {
      } catch (InvocationTargetException e) {
      }
    }

  }

  /**
   * Updates the integrators for tweening. Must be called before {@link AbstractMapDisplay#draw()} .
   */
  public void updateMap() {
    if (tweening) {
      scaleIntegrator.update();
      mapDisplay.innerScale = scaleIntegrator.value;

      txIntegrator.update();
      mapDisplay.innerOffsetX = txIntegrator.value;
      tyIntegrator.update();
      mapDisplay.innerOffsetY = tyIntegrator.value;

      mapDisplay.calculateInnerMatrix();
    }
  }

  // ----------------------------------------------------

  /**
   * Returns the top left corner of the visible map.
   *
   * @return The geographic location with latitude and longitude.
   */
  public Location getTopLeftBorder() {
    return mapDisplay.getLocationFromObjectPosition(0, 0);
  }

  /**
   * Returns the bottom right corner of the visible map.
   *
   * @return The geographic location with latitude and longitude.
   */
  public Location getBottomRightBorder() {
    return mapDisplay.getLocationFromObjectPosition(mapDisplay.getWidth(), mapDisplay.getHeight());
  }

  /**
   * Returns the center location of the visible map.
   *
   * @return The geographic location with latitude and longitude.
   */
  public Location getCenter() {
    return mapDisplay.getLocationFromObjectPosition(mapDisplay.getWidth() / 2, mapDisplay.getHeight() / 2);
  }

  // public Location getLocation(float x, float y) {
  // return mapDisplay.getLocationForScreenPosition(x, y);
  // }

  // @Override
  // public Location getCenterLocation() {
  // Location location = getLocationForObjectPosition(width / 2, height / 2);
  // return location;
  // }
  //
  @Deprecated
  public Location getLocationFromScreenPosition(float x, float y) {
    return getLocation(x, y);
  }

  /**
   * Converts a position on the screen to a geographic location.
   *
   * @param screenPosition
   *            The position in screen coordinates.
   * @return The geographic location with latitude and longitude.
   */
  public Location getLocation(ScreenPosition screenPosition) {
    return mapDisplay.getLocation(screenPosition);
  }

  /**
   * Converts a position on the screen to a geographic location.
   *
   * @param x
   *            The x position in screen coordinates.
   * @param y
   *            The y position in screen coordinates.
   * @return The geographic location with latitude and longitude.
   */
  public Location getLocation(float x, float y) {
    return mapDisplay.getLocation(new ScreenPosition(x, y));
  }

  @Deprecated
  public float[] getScreenPositionFromLocation(Location location) {
    return mapDisplay.getScreenPositionFromLocation(location);
  }

  /**
   * Converts the geographic location to a position on the screen.
   *
   * @param location
   *            The geographic location with latitude and longitude.
   * @return The screen position.
   */
  public ScreenPosition getScreenPosition(Location location) {
    return mapDisplay.getScreenPosition(location);
  }

  // public PVector getScreenPositionFromLocation(Location location) {
  // float[] xy = mapDisplay.getScreenPositionFromLocation(location);
  // return new PVector(xy[0], xy[1]);
  // }

  // Transformations ----------------------------------------------------

  /**
   * Rotates the map by given angle.
   *
   * <p>
   * Use {@link #outerRotate(float)} to rotate the map container. Check RotatableMapApp in examples for a comparison.
   * </p>
   *
   * @param angle
   *            The angle to rotate the map by.
   */
  public void rotate(float angle) {
    innerRotate(angle);
  }

  /**
   * Rotates the map to the given angle, that is it is rotated to the target angle.
   *
   * @param angle
   *            The angle to rotate the map to.
   */
  public void rotateTo(float angle) {
    setInnerRotate(angle);
  }

  /**
   * Zooms to the given level. Map tiles will be non-scaled.
   *
   * @param level
   *            The level to zoom to.
   */
  public void zoomToLevel(int level) {
    float scale = getScaleFromZoom(level);
    setInnerScale(scale);
  }

  public void zoomTo(float zoom) {
    float scale = getScaleFromZoom(zoom);
    setInnerScale(scale);
  }

  /**
   * Zooms in or out one or more levels. Map tiles will be non-scaled.
   *
   * @param levelDelta
   *            The number of levels to zoom in or out.
   */
  public void zoomLevel(int levelDelta) {
    int newLevel = getZoomLevelFromScale(mapDisplay.innerScale) + levelDelta;
    zoomToLevel(newLevel);
  }

  /**
   * @deprecated Replaced by {@link #zoomLevel(int)}
   * @param levelDelta
   *            The number of levels to zoom in or out.
   */
  @Deprecated
  public void zoom(int levelDelta) {
    zoomLevel(levelDelta);
  }

  /**
   * Zooms in a level.
   */
  public void zoomLevelIn() {
    zoom(1);
  }

  /**
   * Zooms out a level.
   */
  public void zoomLevelOut() {
    zoom(-1);
  }

  /**
   * Zooms in or out. Map tiles may be scaled.
   *
   * @param scaleDelta
   *            The scale to zoom by.
   */
  public void zoom(float scaleDelta) {
    innerScale(scaleDelta);
  }

  /**
   * Zooms into the map less than a full level. Map tiles will be scaled.
   */
  public void zoomIn() {
    innerScale(SCALE_DELTA_IN);
  }

  /**
   * Zooms out of the map less than a full level. Map tiles will be scaled.
   */
  public void zoomOut() {
    innerScale(SCALE_DELTA_OUT);
  }

  /**
   * Zooms in around position, and pans to it.
   *
   * After the pan the center still is at the same location. (As innerTransformationCenter is in object coordinates,
   * thus stays at same inner position.)
   *
   * @param x
   *            X position to zoom around and pan to (in screen coordinates).
   * @param y
   *            Y position to zoom around and pan to (in screen coordinates).
   * @param level
   *            Zoom level to zoom to.
   */
  public void zoomAndPanTo(float x, float y, int level) {
    // NB: Could not be deprecated as switching float/int parameters would be ambiguous!

    // Works only when first zoom around pos, then pan to pos
    mapDisplay.setInnerTransformationCenter(new PVector(x, y));
    zoomToLevel(level);
    panTo(x, y);
  }

  /**
   * Zooms in around position, and pans to it.
   *
   * @deprecated Use {@link #zoomAndPanTo(int, ScreenPosition)}.
   *
   * @param screenPosition
   *            ScreenPosition to zoom around and pan to.
   * @param level
   *            Zoom level to zoom to.
   */
  public void zoomAndPanTo(ScreenPosition screenPosition, int level) {
    zoomAndPanTo(level, screenPosition);
  }

  /**
   * Zooms in around position, and pans to it.
   *
   * After the pan the center still is at the same location. (As innerTransformationCenter is in object coordinates,
   * thus stays at same inner position.)
   *
   * @param level
   *            Zoom level to zoom to.
   * @param screenPosition
   *            ScreenPosition to zoom around and pan to.
   */
  public void zoomAndPanTo(int level, ScreenPosition screenPosition) {
    // Works only when first zoom around pos, then pan to pos
    mapDisplay.setInnerTransformationCenter(new PVector(screenPosition.x, screenPosition.y));
    zoomToLevel(level);
    panTo(screenPosition.x, screenPosition.y);
  }

  /**
   * Zooms in around position, and pans to it.
   *
   * @deprecated Use {@link #zoomAndPanTo(int, Location)}.
   *
   * @param location
   *            The Location to zoom around and pan to.
   * @param zoomLevel
   *            Zoom level to zoom to.
   */
  public void zoomAndPanTo(Location location, int zoomLevel) {
    zoomAndPanTo(zoomLevel, location);
  }

  /**
   * Zooms in around position, and pans to it.
   *
   * After the pan the center still is at the same location. (As innerTransformationCenter is in object coordinates,
   * thus stays at same inner position.)
   *
   * @param zoomLevel
   *            Zoom level to zoom to.
   * @param location
   *            The Location to zoom around and pan to.
   */
  public void zoomAndPanTo(int zoomLevel, Location location) {
    ScreenPosition pos = mapDisplay.getScreenPosition(location);
    mapDisplay.setInnerTransformationCenter(new PVector(pos.x, pos.y));
    zoomToLevel(zoomLevel);
    panTo(location);
  }

  /**
   * Pans to the given position. The position will be centered.
   *
   * @param x
   *            X of the position to pan to, in screen coordinates.
   * @param y
   *            Y of the position to pan to, in screen coordinates.
   */
  public void panTo(float x, float y) {
    float[] objectXY = mapDisplay.getObjectFromScreenPosition(x, y);
    panObjectPositionToObjectCenter(objectXY[0], objectXY[1]);
  }

  /**
   * Pans to the given screen position. The position will be centered.
   *
   * @param screenPosition
   *            the position to pan to.
   */
  public void panTo(ScreenPosition screenPosition) {
    float[] objectXY = mapDisplay.getObjectFromScreenPosition(screenPosition.x, screenPosition.y);
    panObjectPositionToObjectCenter(objectXY[0], objectXY[1]);
  }

  /**
   * Pans to the given Location. The position of the location will be centered.
   *
   * @param location
   *            The Location to pan to.
   */
  public void panTo(Location location) {
    float[] innerXY = mapDisplay.getInnerObjectFromLocation(location);
    float[] objectXY = mapDisplay.getObjectFromInnerObjectPosition(innerXY[0], innerXY[1]);
    panObjectPositionToObjectCenter(objectXY[0], objectXY[1]);
  }

  /**
   * Pans from point1 to point 2, given in screen coordinates.
   */
  public void pan(float x1, float y1, float x2, float y2) {
    float[] xy1 = mapDisplay.getObjectFromScreenPosition(x1, y1);
    float[] xy2 = mapDisplay.getObjectFromScreenPosition(x2, y2);

    float dx = xy2[0] - xy1[0];
    float dy = xy2[1] - xy1[1];

    addInnerOffset(dx, dy);
  }

  /**
   * Pans between two ScreenPosition.
   *
   * @param from
   *            ScreenPosition to start from.
   * @param to
   *            ScreenPosition to pan to.
   */
  public void pan(ScreenPosition from, ScreenPosition to) {
    float[] xy1 = mapDisplay.getObjectFromScreenPosition(from.x, from.y);
    float[] xy2 = mapDisplay.getObjectFromScreenPosition(to.x, to.y);

    float dx = xy2[0] - xy1[0];
    float dy = xy2[1] - xy1[1];

    addInnerOffset(dx, dy);
  }

  /**
   * Pans from one location to another one.
   *
   * @param fromLocation
   *            Origin location to pan from.
   * @param toLocation
   *            Destination location to pan to.
   */
  public void pan(Location fromLocation, Location toLocation) {
    float[] xy1 = mapDisplay.getObjectFromLocation(fromLocation);
    float[] xy2 = mapDisplay.getObjectFromLocation(toLocation);

    float dx = xy2[0] - xy1[0];
    float dy = xy2[1] - xy1[1];

    addInnerOffset(dx, dy);
  }

  /**
   * Pans by distance in screen coordinates.
   *
   * @param dx
   *            Horizontal distance in pixel.
   * @param dy
   *            Vertical distance in pixel.
   */
  public void panBy(float dx, float dy) {
    addInnerOffset(dx, dy);
  }

  /**
   * Pans one tile to the left.
   */
  public void panLeft() {
    addInnerOffset(PAN_DEFAULT_DELTA, 0);
  }

  /**
   * Pans one tile to the right.
   */
  public void panRight() {
    addInnerOffset(-PAN_DEFAULT_DELTA, 0);
  }

  /**
   * Pans one tile up.
   */
  public void panUp() {
    addInnerOffset(0, PAN_DEFAULT_DELTA);
  }

  /**
   * Pans one tile down.
   */
  public void panDown() {
    addInnerOffset(0, -PAN_DEFAULT_DELTA);
  }

  /**
   * Moves the map to the given position.
   *
   * <p>
   * The whole map container is moved. Use one of the panning methods to pan the geographical map.
   * </p>
   *
   * @param x
   *            X position in screen coordinates.
   * @param y
   *            Y position in screen coordinates.
   */
  public void move(float x, float y) {
    setOffset(x, y);
  }

  /**
   * Moves the map to the given ScreenPosition.
   *
   * @param screenPosition
   *            the ScreenPosition to move to.
   */
  public void move(ScreenPosition screenPosition) {
    setOffset(screenPosition.x, screenPosition.y);
  }

  /**
   * Moves the map by the given screen coordinates.
   *
   * @param dx
   *            The x distance to move by.
   * @param dy
   *            The y distance to move by.
   *
   */
  public void moveBy(float dx, float dy) {
    addOffset(dx, dy);
  }

  public void zoomAndPanToFit(List<Location> locations) {
    Location[] boundingBox = GeoUtils.getBoundingBox(locations);
    List<Location> boundingBoxLocations = Arrays.asList(boundingBox);
    Location centerLocation = GeoUtils.getEuclideanCentroid(boundingBoxLocations);
    ScreenPosition pos = mapDisplay.getScreenPosition(centerLocation);
    mapDisplay.setInnerTransformationCenter(new PVector(pos.x, pos.y));
    zoomToFit(boundingBox);
    panTo(centerLocation);
  }

  public void zoomToFit(List<Location> locations) {
    Location[] boundingBox = GeoUtils.getBoundingBox(locations);
    zoomToFit(boundingBox);
  }

  public void zoomToFit(Location[] boundingBox) {
    ScreenPosition nwPos = getScreenPosition(boundingBox[0]);
    ScreenPosition sePos = getScreenPosition(boundingBox[1]);
    float zoomScale = 0.9f / Math.max((sePos.x - nwPos.x) / getWidth(), (sePos.y - nwPos.y) / getHeight());
    innerScale(zoomScale);
  }

  // MarkerManagement -----------------------------------------------

  /**
   * Add a MarkerManager to the map. Replaces the default MarkerManager if the given one is the first, and the default
   * one is empty.
   *
   * <p>
   * Use this if you want to handle multiple independent marker groups. Otherwise, simply use UnfoldingMap's marker
   * methods directly (such as {@link #addMarker(Marker)}) which handles this internally with the default
   * MarkerManager.
   * </p>
   *
   * @param markerManager
   *            The MarkerManager to add.
   */
  public void addMarkerManager(MarkerManager<Marker> markerManager) {
    markerManager.setMap(this);
    mapDisplay.addMarkerManager(markerManager);
  }

  /**
   * Returns the lastly added MarkerManager.
   *
   * @return The last MarkerManager.
   */
  public MarkerManager<Marker> getLastMarkerManager() {
    return mapDisplay.getLastMarkerManager();
  }

  /**
   * Returns the default MarkerManager, i.e. the first MarkerManager. This is the one used internally by all
   * UnfoldingMap's marker methods.
   *
   * @return The default MarkerManager.
   */
  public MarkerManager<Marker> getDefaultMarkerManager() {
    return mapDisplay.getDefaultMarkerManager();
  }

  public List<MarkerManager<Marker>> getMarkerManagerList() {
    return mapDisplay.getMarkerManagerList();
  }

  public void removeMarkerManager(MarkerManager<Marker> markerManager) {
    mapDisplay.getMarkerManagerList().remove(markerManager);
  }

  public void removeMarkerManager(int i) {
    mapDisplay.getMarkerManagerList().remove(i);
  }

  /**
   * Returns the MarkerManager at the given index position.
   *
   * @param index
   *            The index to get.
   * @return The MarkerManager at the index, or null if not existing.
   */
  public MarkerManager<Marker> getMarkerManager(int index) {
    return mapDisplay.getMarkerManager(index);
  }

  /**
   * Adds one or multiple markers to the map.
   *
   * <p>
   * <em>Note</em>: Uses the default marker manager. If you have more than one marker manager, use
   * {@link MarkerManager#addMarker(Marker)} instead.
   * </p>
   *
   * @param marker
   *            The marker or markers to add.
   */
  public void addMarkers(Marker... marker) {
    for (Marker m : marker) {
      mapDisplay.addMarker(m);
    }
  }

  /**
   * Adds a marker to the map.
   *
   * <p>
   * <em>Note</em>: Uses the default marker manager. If you have more than one marker manager, use
   * {@link MarkerManager#addMarker(Marker)} instead.
   * </p>
   *
   * @param marker
   *            The marker to add.
   */
  public void addMarker(Marker marker) {
    addMarkers(marker);
  }

  /**
   * Adds multiple markers to the map.
   *
   * <p>
   * <em>Note</em>: Uses the default marker manager. If you have more than one marker manager, use
   * {@link MarkerManager#addMarkers(List)} instead.
   * </p>
   *
   * @param markers
   *            The markers to add.
   */
  public void addMarkers(List<Marker> markers) {
    mapDisplay.addMarkers(markers);
  }

  /**
   * Gets markers of the map.
   *
   * <p>
   * <em>Note</em>: Returns only the markers of the default marker manager. If you have more than one marker manager,
   * use {@link MarkerManager#addMarkers(List)} instead.
   * </p>
   *
   * @return The markers.
   */
  public List<Marker> getMarkers() {
    return mapDisplay.getDefaultMarkerManager().getMarkers();
  }

  /**
   * Checks whether a marker got hit with the given screen coordinates. Can be used for interactive selection, etc.
   * This only returns the first found marker. Use {@link #getHitMarker(float, float)} to get all hit markers.
   *
   * <p>
   * <em>Note</em>: Returns only the markers of the default marker manager. If you have more than one marker manager,
   * use {@link MarkerManager#getFirstHitMarker(float, float)} instead.
   * </p>
   *
   * @return The hit marker, or null if none was hit.
   */
  public Marker getFirstHitMarker(float checkX, float checkY) {
    return mapDisplay.getDefaultMarkerManager().getFirstHitMarker(checkX, checkY);
  }

  /**
   * Checks whether multiple markers got hit with the given screen coordinates. Can be used for interactive selection,
   * etc. This returns all found markers. Use {@link #getFirstHitMarker(float, float)} to get only one marker.
   *
   * <p>
   * <em>Note</em>: Returns only the markers of the default marker manager. If you have more than one marker manager,
   * use {@link MarkerManager#getHitMarkers(float, float)} instead.
   * </p>
   *
   * @return All hit markers, or an empty list if none were hit.
   */
  public List<Marker> getHitMarkers(float checkX, float checkY) {
    return mapDisplay.getDefaultMarkerManager().getHitMarkers(checkX, checkY);
  }

  /**
   * @deprecated Use {@link #getHitMarkers(float, float)} instead.
   */
  public List<Marker> getHitMarker(float checkX, float checkY) {
    return getHitMarkers(checkX, checkY);
  }

  // Transformations ------------------------------------

  protected void setOuterRotate(float angle) {
    mapDisplay.angle = angle;
    mapDisplay.calculateMatrix();
  }

  /**
   * Rotates the map container.
   */
  public void outerRotate(float angle) {
    mapDisplay.angle += angle;
    mapDisplay.calculateMatrix();
  }

  protected void setInnerRotate(float angle) {
    mapDisplay.innerAngle = angle;
    mapDisplay.calculateInnerMatrix();
  }

  /**
   * @deprecated Use {@link #rotate(float)} instead.
   *
   * @param angle
   *            The angle to rotate by.
   */
  public void innerRotate(float angle) {
    mapDisplay.innerAngle += angle;
    mapDisplay.calculateInnerMatrix();
  }

  protected void outerScale(float scale) {
    mapDisplay.scale *= scale;
    mapDisplay.calculateMatrix();
  }

  protected void setOuterScale(float scale) {
    mapDisplay.scale = scale;
    mapDisplay.calculateMatrix();
  }

  public void innerScale(float scale) {
    // TODO Check max,min scale in TileProvider, not here in UnfoldingMap
    scale = PApplet.constrain(mapDisplay.innerScale * scale, minScale, maxScale);
    if (tweening) {
      scaleIntegrator.target(scale);
    } else {
      mapDisplay.innerScale = scale;
      mapDisplay.calculateInnerMatrix();
    }
  }

  public void setInnerScale(float scale) {
    scale = PApplet.constrain(scale, minScale, maxScale);
    if (tweening) {
      scaleIntegrator.target(scale);
    } else {
      mapDisplay.innerScale = scale;
      mapDisplay.calculateInnerMatrix();
    }
  }

  /**
   * Gets the current zoom level of the map. Typically ranges from 0 to 18 or higher, depending on the MapProvider.
   *
   * @return The zoom level.
   */
  public int getZoomLevel() {
    return getZoomLevelFromScale(mapDisplay.innerScale);
  }

  /**
   * Gets the current zoom of the map. Can be used to scale the size of markers.
   *
   * This is the actual scale of the map (not simply a floating zoom level value). It ranges from 0^2 to 18^2 or
   * higher. Use {@link #getZoomFromScale(double)} to convert to floating zoom level.
   *
   * @return The zoom value.
   */
  public float getZoom() {
    return mapDisplay.innerScale;
  }

  /**
   * Sets the range of map scale factors.
   */
  public void setScaleRange(float minScale, float maxScale) {
    this.minScale = minScale;
    this.maxScale = maxScale;
  }

  /**
   * Sets the range of map zoom levels. All zoom interactions will be restricted to these levels.
   *
   * @param minZoomLevel
   *            The lower zoom level boundary.
   * @param maxZoomLevel
   *            The upper zoom level boundary.
   */
  public void setZoomRange(float minZoomLevel, float maxZoomLevel) {
    this.minScale = getScaleFromZoom(minZoomLevel);
    this.maxScale = getScaleFromZoom(maxZoomLevel);
  }

  // /**
  // * Sets a bounding box as panning range of the map.
  // *
  // * @param location
  // * topLeft corner of the box.
  // * @param location2
  // * bottomRight corner of the box.
  // */
  // TODO Implement setPanningBOund
  // public void setPanningRange(Location topLeftLocation, Location bottomRightLocation) {
  // }

  /**
   * Restricts the area this map can pan to in a radial fashion.
   *
   * @param location
   *            The center location of the circular restriction area.
   * @param maxPanningDistance
   *            The radius of the circular restriction area in kilometers.
   */
  public void setPanningRestriction(Location location, float maxPanningDistance) {
    this.restrictedPanLocation = location;
    this.maxPanningDistance = maxPanningDistance;
  }

  /**
   * Frees this map from any panning restriction.
   */
  public void resetPanningRestriction() {
    this.restrictedPanLocation = null;
  }

  /**
   * Method to perform any current map panning restriction.
   */
  private void restrictMapToArea() {
    if (restrictedPanLocation == null) {
      return;
    }

    Location mapCenter = getCenter();
    double dist = GeoUtils.getDistance(restrictedPanLocation, mapCenter);
    if (dist > maxPanningDistance) {
      float angle = PApplet.degrees((float) GeoUtils.getAngleBetween(restrictedPanLocation, mapCenter));
      float backDist = maxPanningDistance - (float) dist;
      // Pan back, with same angle but negative distance
      Location newLocation = GeoUtils.getDestinationLocation(mapCenter, angle, backDist);
      panTo(newLocation);
    }
  }

  // Outer and inner offset ---------------------------------------

  /**
   * Pans object center to given object position.
   *
   * @param x
   *            X in object coordinates.
   * @param y
   *            Y in object coordinates.
   */
  protected void panObjectPositionToObjectCenter(float x, float y) {
    float dx = mapDisplay.getWidth() / 2 - x;
    float dy = mapDisplay.getHeight() / 2 - y;
    addInnerOffset(dx, dy);
  }

  protected void addInnerOffset(float dx, float dy) {
    if (tweening) {
      txIntegrator.target(txIntegrator.target + dx);
      tyIntegrator.target(tyIntegrator.target + dy);
    } else {
      mapDisplay.innerOffsetX += dx;
      mapDisplay.innerOffsetY += dy;
      mapDisplay.calculateInnerMatrix();
    }
  }

  protected void setInnerOffset(float x, float y) {
    if (tweening) {
      txIntegrator.target(x);
      tyIntegrator.target(y);
    } else {
      mapDisplay.innerOffsetX = x;
      mapDisplay.innerOffsetY = y;
      mapDisplay.calculateInnerMatrix();
    }
  }

  /**
   * Sets the offset of the map. Can be used to re-position the map container (not the map content itself).
   *
   * @param x
   *            The x of the screen position.
   * @param y
   *            The y of the screen position.
   */
  protected void setOffset(float x, float y) {
    mapDisplay.offsetX = x;
    mapDisplay.offsetY = y;
    mapDisplay.calculateMatrix();
  }

  /**
   * Moves the offset of the map. Can be used to re-position the map container (not the map content itself).
   *
   * @param dx
   *            The x distance in screen coordinates.
   * @param dy
   *            The y distance in screen coordinates.
   */
  protected void addOffset(float dx, float dy) {
    mapDisplay.offsetX += dx;
    mapDisplay.offsetY += dy;
    mapDisplay.calculateMatrix();
  }

  // --------------------------------------------------------------

  /**
   * Converts zoom to scale.
   *
   * @param zoom
   *            The zoom value (between 0 and typically 20+).
   * @return The scale value (between 0 and 2<sup>zoom</sup>).
   */
  public static float getScaleFromZoom(float zoom) {
    return (float) Math.pow(2.0f, zoom);
  }

  /**
   * Converts scale to zoom.
   *
   * @param scale
   *            The scale value (between 0 and 2<sup>zoom</sup>).
   * @return The zoom value (between 0 and typically 20+).
   */
  public static float getZoomFromScale(double scale) {
    return (float) Math.log(scale) / (float) Math.log(2);
  }

  /**
   * Converts scale to zoom level. Same as {@link #getZoomFromScale(double)} but with integer values.
   */
  public static int getZoomLevelFromScale(double scale) {
    return Math.round(getZoomFromScale(scale));
  }

  /**
   * Switches the tweening flag.
   */
  public void switchTweening() {
    // Use setter method to ensure proper setting of integrators.
    setTweening(!tweening);
  }

  /**
   * Indicates whether the map currently animates between different states.
   *
   * @return True if tweening is on, false otherwise.
   */
  public boolean isTweening() {
    return tweening;
  }

  /**
   * Sets the tweening flag, i.e. whether the map shall animate between different states.
   *
   * @param tweening
   *            Whether or not the map shall animate.
   */
  public void setTweening(boolean tweening) {
    if (tweening == this.tweening)
      return;

    this.tweening = tweening;

    if (tweening) {
      // Set current (animated) and target (final) values to same, so there is no unnecessary animation starting
      // after switching to tweening.

      scaleIntegrator.set(mapDisplay.innerScale);
      scaleIntegrator.target(mapDisplay.innerScale);

      txIntegrator.set(mapDisplay.innerOffsetX);
      txIntegrator.target(mapDisplay.innerOffsetX);
      tyIntegrator.set(mapDisplay.innerOffsetY);
      tyIntegrator.target(mapDisplay.innerOffsetY);

    } else {
      // Set zoom and pan directly, even if still in transition animation

      mapDisplay.innerScale = scaleIntegrator.target;

      mapDisplay.innerOffsetX = txIntegrator.target;
      mapDisplay.innerOffsetY = tyIntegrator.target;
    }
  }

  /**
   * Sets the background color of this map.
   *
   * @param bgColor
   *            The color in the current colorMode.
   */
  public void setBackgroundColor(Integer bgColor) {
    this.mapDisplay.setBackgroundColor(bgColor);
  }

  /**
   * Returns the width of this map.
   *
   * @return The width in pixels.
   */
  public float getWidth() {
    return mapDisplay.getWidth();
  }

  /**
   * Returns the height of this map.
   *
   * @return The height in pixels.
   */
  public float getHeight() {
    return mapDisplay.getHeight();
  }

}
TOP

Related Classes of de.fhpotsdam.unfolding.UnfoldingMap

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.