/*
* This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
*
* Copyright 2008-2015 Geosparc nv, http://www.geosparc.com/, Belgium.
*
* The program is available in open source according to the GNU Affero
* General Public License. All contributions in this program are covered
* by the Geomajas Contributors License Agreement. For full licensing
* details, see LICENSE.txt in the project root.
*/
package org.geomajas.gwt2.client.controller;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;
import org.geomajas.annotation.Api;
import org.geomajas.geometry.Coordinate;
import org.geomajas.geometry.service.MathService;
import org.geomajas.gwt.client.map.RenderSpace;
import org.geomajas.gwt2.client.animation.KineticTrajectory;
import org.geomajas.gwt2.client.animation.NavigationAnimationFactory;
import org.geomajas.gwt2.client.animation.NavigationAnimationImpl;
import org.geomajas.gwt2.client.map.MapPresenter;
import org.geomajas.gwt2.client.map.View;
import org.geomajas.gwt2.client.map.ViewPort;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.DoubleClickEvent;
import com.google.gwt.event.dom.client.HumanInputEvent;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseWheelEvent;
/**
* Generic navigation map controller. This controller allows for panning and zooming on the map in many different ways.
* Options are the following:
* <ul>
* <li><b>Panning</b>: Drag the map to pan.</li>
* <li><b>Double click</b>: Double clicking on some location will see the map zoom in to that location.</li>
* <li><b>Zoom to rectangle</b>: By holding shift or ctrl and dragging at the same time, a rectangle will appear on the
* map. On mouse up, the map will than zoom to that rectangle.</li>
* <li></li>
* </ul>
* For zooming using the mouse wheel there are 2 options, defined by the {@link ScrollZoomType} enum. These options are
* the following:
* <ul>
* <li><b>ScrollZoomType.ZOOM_CENTER</b>: Zoom in/out so that the current center of the map remains.</li>
* <li><b>ScrollZoomType.ZOOM_POSITION</b>: Zoom in/out so that the mouse world position remains the same. Can be great
* for many subsequent double clicks, to make sure you keep zooming to the same location (wherever the mouse points to).
* </li>
* </ul>
*
* @author Pieter De Graef
* @since 2.0.0
*/
@Api(allMethods = true)
public class NavigationController extends AbstractMapController {
private static Logger logger = Logger.getLogger("MapPresenterImpl");
/** Zooming types on mouse wheel scroll. */
public static enum ScrollZoomType {
/** When scroll zooming, retain the center of the map position. */
ZOOM_CENTER,
/** When scroll zooming, retain the mouse position. */
ZOOM_POSITION
}
private final ZoomToRectangleController zoomToRectangleController;
protected Coordinate dragOrigin;
protected List<Coordinate> dragPositions = new ArrayList<Coordinate>();
protected List<Date> dragTimes = new ArrayList<Date>();
protected Coordinate lastClickPosition;
protected boolean zooming;
protected boolean dragging;
private ScrollZoomType scrollZoomType = ScrollZoomType.ZOOM_POSITION;
private long lastMillis;
private static final long MAX_KINETIC_DELAY = 30;
// ------------------------------------------------------------------------
// Constructors:
// ------------------------------------------------------------------------
/** Create a NavigationController instance. */
public NavigationController() {
super(false);
zoomToRectangleController = new ZoomToRectangleController();
}
// ------------------------------------------------------------------------
// MapController implementation:
// ------------------------------------------------------------------------
@Override
public void onActivate(MapPresenter mapPresenter) {
super.onActivate(mapPresenter);
zoomToRectangleController.onActivate(mapPresenter);
}
@Override
public void onMouseDown(MouseDownEvent event) {
super.onMouseDown(event);
if (event.isControlKeyDown() || event.isShiftKeyDown()) {
// Trigger the dragging on the zoomToRectangleController:
zoomToRectangleController.onMouseDown(event);
}
}
@Override
public void onDown(HumanInputEvent<?> event) {
if (event.isControlKeyDown() || event.isShiftKeyDown()) {
zooming = true;
} else if (!isRightMouseButton(event)) {
dragging = true;
dragOrigin = getLocation(event, RenderSpace.SCREEN);
dragPositions.clear();
dragTimes.clear();
mapPresenter.setCursor("move");
}
lastClickPosition = getLocation(event, RenderSpace.WORLD);
}
@Override
public void onUp(HumanInputEvent<?> event) {
if (zooming) {
logger.info("zooming");
Coordinate coordinate = getLocation(event, RenderSpace.WORLD);
if (!coordinate.equals(lastClickPosition)) {
zoomToRectangleController.onUp(event);
}
zooming = false;
} else if (dragging) {
stopPanning(event);
}
}
@Override
public void onMouseMove(MouseMoveEvent event) {
if (zooming) {
zoomToRectangleController.onMouseMove(event);
} else if (dragging) {
super.onMouseMove(event);
}
}
@Override
public void onDrag(HumanInputEvent<?> event) {
dragPositions.add(getLocation(event, RenderSpace.SCREEN));
Date time = new Date();
dragTimes.add(time);
if (time.getTime() - this.dragTimes.get(0).getTime() > 200) {
this.dragTimes.remove(0);
this.dragPositions.remove(0);
}
updateView(event);
}
@Override
public void onMouseOut(MouseOutEvent event) {
if (zooming) {
zoomToRectangleController.onMouseOut(event);
} else {
stopPanning(null);
}
}
@Override
public void onDoubleClick(DoubleClickEvent event) {
mapPresenter.getViewPort().registerAnimation(
NavigationAnimationFactory.createZoomIn(mapPresenter, calculatePosition(true, lastClickPosition)));
}
@Override
public void onMouseWheel(MouseWheelEvent event) {
final boolean isNorth;
if (event.getDeltaY() == 0) {
isNorth = (getWheelDelta(event.getNativeEvent()) < 0);
} else {
isNorth = event.isNorth();
}
Coordinate location = getLocation(event, RenderSpace.WORLD);
scrollZoomTo(isNorth, location);
}
// Getters and setters:
// ------------------------------------------------------------------------
/**
* Get the scroll zoom type of this controller.
*
* @return the scroll zoom type.
*/
public ScrollZoomType getScrollZoomType() {
return scrollZoomType;
}
/**
* Set the scroll zoom type of this controller.
*
* @param scrollZoomType the scroll zoom type.
*/
public void setScrollZoomType(ScrollZoomType scrollZoomType) {
this.scrollZoomType = scrollZoomType;
}
// ------------------------------------------------------------------------
// Private methods:
// ------------------------------------------------------------------------
protected void stopPanning(HumanInputEvent<?> event) {
dragging = false;
mapPresenter.setCursor("default");
if (null != event && dragPositions.size() >= 2) {
logger.info("kinetics started");
// start kinetic animation
Coordinate dragStart = toWorld(dragPositions.get(0));
Coordinate dragStop = toWorld(dragPositions.get(dragPositions.size() - 1));
long timeStart = dragTimes.get(0).getTime();
long timeStop = dragTimes.get(dragTimes.size() - 1).getTime();
int delta = (int) (timeStop - timeStart);
long delay = new Date().getTime() - timeStop;
if (delta > 0 && delay < MAX_KINETIC_DELAY) {
// map moves in the inverse direction !
Coordinate direction = subtract(dragStart, dragStop);
double distance = abs(direction);
Coordinate current = mapPresenter.getViewPort().getPosition();
double resolution = mapPresenter.getViewPort().getResolution();
KineticTrajectory trajectory = new KineticTrajectory(new View(current, resolution), direction, distance
/ delta);
mapPresenter.getViewPort().registerAnimation(
new NavigationAnimationImpl(mapPresenter.getViewPort(), mapPresenter.getEventBus(), trajectory,
(int) trajectory.getDuration()));
} else {
mapPresenter.getViewPort().stopInteraction();
}
}
}
private double abs(Coordinate c) {
return MathService.distance(c, new Coordinate());
}
private Coordinate toWorld(Coordinate coordinate) {
return mapPresenter.getViewPort().getTransformationService()
.transform(coordinate, RenderSpace.SCREEN, RenderSpace.WORLD);
}
private Coordinate subtract(Coordinate c1, Coordinate c2) {
return new Coordinate(c1.getX() - c2.getX(), c1.getY() - c2.getY());
}
private Coordinate add(Coordinate c1, Coordinate c2) {
return new Coordinate(c2.getX() + c1.getX(), c2.getY() + c1.getY());
}
protected void updateView(HumanInputEvent<?> event) {
if (dragging) {
Coordinate end = getLocation(event, RenderSpace.SCREEN);
Coordinate beginWorld = mapPresenter.getViewPort().getTransformationService()
.transform(dragOrigin, RenderSpace.SCREEN, RenderSpace.WORLD);
Coordinate endWorld = mapPresenter.getViewPort().getTransformationService()
.transform(end, RenderSpace.SCREEN, RenderSpace.WORLD);
double x = mapPresenter.getViewPort().getPosition().getX() + beginWorld.getX() - endWorld.getX();
double y = mapPresenter.getViewPort().getPosition().getY() + beginWorld.getY() - endWorld.getY();
View view = new View(new Coordinate(x, y), mapPresenter.getViewPort().getResolution());
view.setDragging(true);
mapPresenter.getViewPort().applyView(view);
dragOrigin = end;
}
}
protected native int getWheelDelta(NativeEvent evt) /*-{
return Math.round(-evt.wheelDelta) || 0;
}-*/;
protected void scrollZoomTo(boolean isNorth, Coordinate location) {
ViewPort viewPort = mapPresenter.getViewPort();
int index = viewPort.getResolutionIndex(viewPort.getResolution());
if (isNorth) {
if (index < viewPort.getResolutionCount() - 1) {
if (scrollZoomType == ScrollZoomType.ZOOM_POSITION) {
Coordinate position = calculatePosition(true, location);
viewPort.registerAnimation(NavigationAnimationFactory.createZoomIn(mapPresenter, position));
} else {
viewPort.registerAnimation(NavigationAnimationFactory.createZoomIn(mapPresenter));
}
}
} else {
if (index > 0) {
if (scrollZoomType == ScrollZoomType.ZOOM_POSITION) {
Coordinate position = calculatePosition(false, location);
viewPort.registerAnimation(NavigationAnimationFactory.createZoomOut(mapPresenter, position));
} else {
viewPort.registerAnimation(NavigationAnimationFactory.createZoomOut(mapPresenter));
}
}
}
}
/**
* Calculate the target position should there be a rescale point. The idea is that after zooming in or out, the
* mouse cursor would still lie at the same position in world space.
*/
protected Coordinate calculatePosition(boolean zoomIn, Coordinate rescalePoint) {
ViewPort viewPort = mapPresenter.getViewPort();
Coordinate position = viewPort.getPosition();
int index = viewPort.getResolutionIndex(viewPort.getResolution());
double resolution = viewPort.getResolution();
if (zoomIn && index < viewPort.getResolutionCount() - 1) {
resolution = viewPort.getResolution(index + 1);
} else if (!zoomIn && index > 0) {
resolution = viewPort.getResolution(index - 1);
}
double factor = viewPort.getResolution() / resolution;
double dX = (rescalePoint.getX() - position.getX()) * (1 - 1 / factor);
double dY = (rescalePoint.getY() - position.getY()) * (1 - 1 / factor);
return new Coordinate(position.getX() + dX, position.getY() + dY);
}
}