/*******************************************************************************
* Copyright (c) 2015, 2017 itemis AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Alexander Nyßen (itemis AG) - initial API and implementation
* Matthias Wienand (itemis AG) - contribution for Bugzillas #476507, #510419
*
*******************************************************************************/
package org.eclipse.gef.mvc.fx.policies;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import org.eclipse.gef.fx.nodes.InfiniteCanvas;
import org.eclipse.gef.geometry.convert.fx.FX2Geometry;
import org.eclipse.gef.geometry.planar.AffineTransform;
import org.eclipse.gef.mvc.fx.operations.ChangeViewportOperation;
import org.eclipse.gef.mvc.fx.operations.ITransactionalOperation;
import org.eclipse.gef.mvc.fx.viewer.IViewer;
import javafx.geometry.Point2D;
/**
* An {@link IPolicy} to change the viewport of an {@link IViewer} via its
* {@link InfiniteCanvas}.
*
* @author anyssen
* @author mwienand
*
*/
public class ViewportPolicy extends AbstractPolicy {
private static final double DEFAULT_ZOOM_MIN = 0.0625;
private static final double DEFAULT_ZOOM_MAX = 16d;
@Override
protected ITransactionalOperation createOperation() {
InfiniteCanvas canvas = (InfiniteCanvas) getHost().getRoot().getViewer()
.getCanvas();
return new ChangeViewportOperation(canvas,
FX2Geometry.toAffineTransform(canvas.getContentTransform()));
}
/**
* Centers the contents within the viewport and zooms the viewport so that
* the contents are fully visible.
*/
public void fitToSize() {
fitToSize(DEFAULT_ZOOM_MIN, DEFAULT_ZOOM_MAX);
}
/**
* Centers the contents within the viewport and zooms the viewport so that
* the contents are fully visible (if possible given the specified zoom
* factor range).
*
* @param zoomMin
* The minimum zoom factor.
*/
public void fitToSize(double zoomMin) {
fitToSize(zoomMin, DEFAULT_ZOOM_MAX);
}
/**
* Centers the contents within the viewport and zooms the viewport so that
* the contents are fully visible (if possible given the specified zoom
* factor range).
*
* @param zoomMin
* The minimum zoom factor.
* @param zoomMax
* The maximum zoom factor.
*/
public void fitToSize(double zoomMin, double zoomMax) {
ChangeViewportOperation viewportOperation = getChangeViewportOperation();
InfiniteCanvas canvas = viewportOperation.getInfiniteCanvas();
// fit-to-size only works if we have contents
if (canvas.getContentBounds().isEmpty()) {
return;
}
canvas.fitToSize(zoomMin, zoomMax);
viewportOperation.setNewContentTransform(
FX2Geometry.toAffineTransform(canvas.getContentTransform()));
viewportOperation.setNewHorizontalScrollOffset(
canvas.getHorizontalScrollOffset());
viewportOperation
.setNewVerticalScrollOffset(canvas.getVerticalScrollOffset());
}
/**
* Returns an {@link ChangeViewportOperation} that is extracted from the
* operation created by {@link #createOperation()}.
*
* @return An {@link ChangeViewportOperation} that is extracted from the
* operation created by {@link #createOperation()}.
*/
protected ChangeViewportOperation getChangeViewportOperation() {
return (ChangeViewportOperation) super.getOperation();
}
/**
* Advances the viewport transformation by the given translation values.
*
* @param concatenate
* <code>true</code> to concatenate the specified zoom to the
* current transformation, <code>false</code> to not concatenate
* the specified scroll to the current but to the initial
* transformation.
* @param deltaTranslateX
* The horizontal translation delta.
* @param deltaTranslateY
* The vertical translation delta.
*/
// TODO: add discretize option
public void scroll(boolean concatenate, double deltaTranslateX,
double deltaTranslateY) {
checkInitialized();
ChangeViewportOperation operation = getChangeViewportOperation();
operation.setNewHorizontalScrollOffset(
(concatenate ? operation.getNewHorizontalScrollOffset()
: operation.getInitialHorizontalScrollOffset())
+ deltaTranslateX);
operation.setNewVerticalScrollOffset(
(concatenate ? operation.getNewVerticalScrollOffset()
: operation.getInitialVerticalScrollOffset())
+ deltaTranslateY);
locallyExecuteOperation();
}
/**
* Sets the x and y translation of the viewport to the given values. Does
* not alter scaling.
*
* @param tx
* The new x translation.
* @param ty
* The new y translation.
*/
public void setScroll(double tx, double ty) {
checkInitialized();
// query current transformation
AffineTransform newTransform = getChangeViewportOperation()
.getNewContentTransform();
// set zoom level
getChangeViewportOperation().setNewContentTransform(new AffineTransform(
newTransform.getM00(), newTransform.getM10(),
newTransform.getM01(), newTransform.getM11(), tx, ty));
locallyExecuteOperation();
}
/**
* Sets the x and y scaling of the viewport to the given zoom factor. Does
* not alter translation/scroll-offset.
*
* @param zoom
* The new x and y scaling for the viewport.
*/
public void setZoom(double zoom) {
checkInitialized();
// query current transformation
AffineTransform newTransform = getChangeViewportOperation()
.getNewContentTransform();
// set zoom level
getChangeViewportOperation().setNewContentTransform(new AffineTransform(
zoom, newTransform.getM10(), newTransform.getM01(), zoom,
newTransform.getTranslateX(), newTransform.getTranslateY()));
locallyExecuteOperation();
}
/**
* Concatenates a scaling transformation to the current viewport
* transformation.
*
* @param concatenate
* <code>true</code> to concatenate the specified zoom to the
* current transformation, <code>false</code> to not concatenate
* the specified zoom to the current but to the initial
* transformation.
* @param discretize
* <code>true</code> to discretize the resulting zoom level, i.e.
* round it to 6 decimal places and do not skip integer zoom
* levels, <code>false</code> to not change the resulting zoom
* level.
* @param relativeZoom
* The scale factor.
* @param sceneX
* The pivot x-coordinate.
* @param sceneY
* The pivot y-coordinate.
*/
public void zoom(boolean concatenate, boolean discretize,
double relativeZoom, double sceneX, double sceneY) {
checkInitialized();
double discreteZoomLevel = 0d;
if (discretize) {
// TODO: Improve discretization algorithm and/or make it
// configurable.
// query current zoom level
double oldZoomLevel = getChangeViewportOperation()
.getNewContentTransform().getScaleX();
// compute next zoom level
discreteZoomLevel = oldZoomLevel * relativeZoom;
// round to 6 decimal places
DecimalFormat df = new DecimalFormat("#.######");
df.setDecimalFormatSymbols(
DecimalFormatSymbols.getInstance(Locale.ENGLISH));
df.setRoundingMode(RoundingMode.HALF_EVEN);
discreteZoomLevel = Double
.parseDouble(df.format(discreteZoomLevel));
// ensure integer zoom levels are not skipped
int ozli = (int) oldZoomLevel;
int nzli = (int) discreteZoomLevel;
if (ozli != nzli && nzli != discreteZoomLevel
&& ozli != oldZoomLevel) {
discreteZoomLevel = ozli < nzli ? nzli : ozli;
}
}
// transform pivot to local coordinates
Point2D contentGroupPivot = ((InfiniteCanvas) getHost().getRoot()
.getViewer().getCanvas()).getContentGroup().sceneToLocal(sceneX,
sceneY);
// compute zoom transform
AffineTransform zoomTx = new AffineTransform()
.translate(contentGroupPivot.getX(), contentGroupPivot.getY())
.scale(relativeZoom, relativeZoom).translate(
-contentGroupPivot.getX(), -contentGroupPivot.getY());
if (concatenate) {
// apply relative zooming
getChangeViewportOperation()
.concatenateToNewContentTransform(zoomTx);
} else {
// concatenate to original transformation
AffineTransform newTx = getChangeViewportOperation()
.getInitialContentTransform().getCopy().concatenate(zoomTx);
getChangeViewportOperation().setNewContentTransform(newTx);
}
if (discretize) {
double newZoomLevel = getChangeViewportOperation()
.getNewContentTransform().getScaleX();
if (Math.abs(newZoomLevel - discreteZoomLevel) < 0.01) {
// // counter-act floating point errors
setZoom(discreteZoomLevel);
}
}
locallyExecuteOperation();
}
}