-
-
Save james-d/ce5ec1fd44ce6c64e81a to your computer and use it in GitHub Desktop.
import javafx.application.Application; | |
import javafx.beans.property.ObjectProperty; | |
import javafx.beans.property.SimpleObjectProperty; | |
import javafx.geometry.Insets; | |
import javafx.geometry.Point2D; | |
import javafx.geometry.Pos; | |
import javafx.geometry.Rectangle2D; | |
import javafx.scene.Scene; | |
import javafx.scene.control.Button; | |
import javafx.scene.control.Hyperlink; | |
import javafx.scene.control.Tooltip; | |
import javafx.scene.image.Image; | |
import javafx.scene.image.ImageView; | |
import javafx.scene.layout.HBox; | |
import javafx.scene.layout.Pane; | |
import javafx.scene.layout.Priority; | |
import javafx.scene.layout.VBox; | |
import javafx.stage.Stage; | |
public class PlutoExplorer extends Application { | |
private static final String IMAGE_CREDIT_URL = "http://www.nasa.gov/image-feature/global-mosaic-of-pluto-in-true-color"; | |
private static final String IMAGE_URL = "https://www.nasa.gov/sites/default/files/thumbnails/image/global-mosaic-of-pluto-in-true-color.jpg"; | |
private static final int MIN_PIXELS = 10; | |
@Override | |
public void start(Stage primaryStage) { | |
Image image = new Image(IMAGE_URL); | |
double width = image.getWidth(); | |
double height = image.getHeight(); | |
ImageView imageView = new ImageView(image); | |
imageView.setPreserveRatio(true); | |
reset(imageView, width / 2, height / 2); | |
ObjectProperty<Point2D> mouseDown = new SimpleObjectProperty<>(); | |
imageView.setOnMousePressed(e -> { | |
Point2D mousePress = imageViewToImage(imageView, new Point2D(e.getX(), e.getY())); | |
mouseDown.set(mousePress); | |
}); | |
imageView.setOnMouseDragged(e -> { | |
Point2D dragPoint = imageViewToImage(imageView, new Point2D(e.getX(), e.getY())); | |
shift(imageView, dragPoint.subtract(mouseDown.get())); | |
mouseDown.set(imageViewToImage(imageView, new Point2D(e.getX(), e.getY()))); | |
}); | |
imageView.setOnScroll(e -> { | |
double delta = e.getDeltaY(); | |
Rectangle2D viewport = imageView.getViewport(); | |
double scale = clamp(Math.pow(1.01, delta), | |
// don't scale so we're zoomed in to fewer than MIN_PIXELS in any direction: | |
Math.min(MIN_PIXELS / viewport.getWidth(), MIN_PIXELS / viewport.getHeight()), | |
// don't scale so that we're bigger than image dimensions: | |
Math.max(width / viewport.getWidth(), height / viewport.getHeight()) | |
); | |
Point2D mouse = imageViewToImage(imageView, new Point2D(e.getX(), e.getY())); | |
double newWidth = viewport.getWidth() * scale; | |
double newHeight = viewport.getHeight() * scale; | |
// To keep the visual point under the mouse from moving, we need | |
// (x - newViewportMinX) / (x - currentViewportMinX) = scale | |
// where x is the mouse X coordinate in the image | |
// solving this for newViewportMinX gives | |
// newViewportMinX = x - (x - currentViewportMinX) * scale | |
// we then clamp this value so the image never scrolls out | |
// of the imageview: | |
double newMinX = clamp(mouse.getX() - (mouse.getX() - viewport.getMinX()) * scale, | |
0, width - newWidth); | |
double newMinY = clamp(mouse.getY() - (mouse.getY() - viewport.getMinY()) * scale, | |
0, height - newHeight); | |
imageView.setViewport(new Rectangle2D(newMinX, newMinY, newWidth, newHeight)); | |
}); | |
imageView.setOnMouseClicked(e -> { | |
if (e.getClickCount() == 2) { | |
reset(imageView, width, height); | |
} | |
}); | |
Hyperlink link = new Hyperlink("Image Credit: NASA/JHUAPL/SwRI"); | |
link.setOnAction(e -> getHostServices().showDocument(IMAGE_CREDIT_URL)); | |
link.setPadding(new Insets(10)); | |
link.setTooltip(new Tooltip(IMAGE_CREDIT_URL)); | |
HBox buttons = createButtons(width, height, imageView); | |
Tooltip tooltip = new Tooltip("Scroll to zoom, drag to pan"); | |
Tooltip.install(buttons, tooltip); | |
Pane container = new Pane(imageView); | |
container.setPrefSize(800, 600); | |
imageView.fitWidthProperty().bind(container.widthProperty()); | |
imageView.fitHeightProperty().bind(container.heightProperty()); | |
VBox root = new VBox(link, container, buttons); | |
root.setFillWidth(true); | |
VBox.setVgrow(container, Priority.ALWAYS); | |
primaryStage.setScene(new Scene(root)); | |
primaryStage.setTitle("Pluto explorer"); | |
primaryStage.show(); | |
} | |
private HBox createButtons(double width, double height, ImageView imageView) { | |
Button reset = new Button("Reset"); | |
reset.setOnAction(e -> reset(imageView, width / 2, height / 2)); | |
Button full = new Button("Full view"); | |
full.setOnAction(e -> reset(imageView, width, height)); | |
HBox buttons = new HBox(10, reset, full); | |
buttons.setAlignment(Pos.CENTER); | |
buttons.setPadding(new Insets(10)); | |
return buttons; | |
} | |
// reset to the top left: | |
private void reset(ImageView imageView, double width, double height) { | |
imageView.setViewport(new Rectangle2D(0, 0, width, height)); | |
} | |
// shift the viewport of the imageView by the specified delta, clamping so | |
// the viewport does not move off the actual image: | |
private void shift(ImageView imageView, Point2D delta) { | |
Rectangle2D viewport = imageView.getViewport(); | |
double width = imageView.getImage().getWidth() ; | |
double height = imageView.getImage().getHeight() ; | |
double maxX = width - viewport.getWidth(); | |
double maxY = height - viewport.getHeight(); | |
double minX = clamp(viewport.getMinX() - delta.getX(), 0, maxX); | |
double minY = clamp(viewport.getMinY() - delta.getY(), 0, maxY); | |
imageView.setViewport(new Rectangle2D(minX, minY, viewport.getWidth(), viewport.getHeight())); | |
} | |
private double clamp(double value, double min, double max) { | |
if (value < min) | |
return min; | |
if (value > max) | |
return max; | |
return value; | |
} | |
// convert mouse coordinates in the imageView to coordinates in the actual image: | |
private Point2D imageViewToImage(ImageView imageView, Point2D imageViewCoordinates) { | |
double xProportion = imageViewCoordinates.getX() / imageView.getBoundsInLocal().getWidth(); | |
double yProportion = imageViewCoordinates.getY() / imageView.getBoundsInLocal().getHeight(); | |
Rectangle2D viewport = imageView.getViewport(); | |
return new Point2D( | |
viewport.getMinX() + xProportion * viewport.getWidth(), | |
viewport.getMinY() + yProportion * viewport.getHeight()); | |
} | |
public static void main(String[] args) { | |
launch(args); | |
} | |
} |
@clarkbean710 Using your solution, the imageview keeps jumping from side to side when dragging the image. Do you know how can I solve this problem?
Edit: I solved by adjusting MIN_PIXELS to the width of my window
Edit 2: I have a small correction for the shift method that avoid problems when dragging the imageview with the solution from @clarkbean710
private void shift(ImageView imageView, Point2D delta) {
Rectangle2D viewport = imageView.getViewport();
double width = imageView.getImage().getWidth();
double height = imageView.getImage().getHeight();
double maxX = width - viewport.getWidth();
double maxY = height - viewport.getHeight();
double minX = clamp(viewport.getMinX() - delta.getX(), 0, maxX);
double minY = clamp(viewport.getMinY() - delta.getY(), 0, maxY);
if (minX < 0.0) {
minX = 0.0;
}
if (minY < 0.0) {
minY = 0.0;
}
imageView.setViewport(new Rectangle2D(minX, minY, viewport.getWidth(), viewport.getHeight()));
}
Doesn't work for images that end up being larger than the window can accommodate. Seems silly to only be able to pan around images that are less than your screen's resolution.
It's due to people like you, that i'am learning a lot, thanks a lot sir
This example is work's like magic, thank you for sharing this.
However, I would like to use MVP architecture. Do anyone know how to move the scrollEvent, mousePressed, and mouseDragged to the presenter?
or zooming and panning the image is should on the view?
Hi there James-D,
I've reworked PlutoExplorer, added some more structure, loads of comments & Javadoc and corrected a small bug.
There are a number of detail improvements.
The bug was a superfluous line of code in the OnMouseDragged Event-Handler:
mouseDown.set(imageViewToImage(imageView, new Point2D(e.getX(), e.getY())));
It works fine without it. If its unclear why, contact me for a detailed explanation.
I've tried to separate the GUI from the Controller Logic a bit, so maybe this addresses the comment from 23.Sep.2020 of swardana.
Its still a way from being a fully fledged MVC structure, but it is only an example after all.
PlutoExplorerExtended.java can be found here.
All the best,
DanskerDave
If you change the event handler like this it will adjust the viewport proportions to match the ImageView size: