From dede97bc681591d703fe02b51fcbc03ddb7d6cef Mon Sep 17 00:00:00 2001 From: Holt59 Date: Sat, 3 Mar 2018 15:23:08 +0100 Subject: [PATCH] Add zoom controls to basic drawing. --- .../drawing/components/BasicDrawing.java | 38 +++ .../drawing/components/MapZoomControls.java | 173 ++++++++++++ .../components/ZoomAndPanListener.java | 251 ++++++++++-------- zoomIn.png | Bin 0 -> 3536 bytes zoomOut.png | Bin 0 -> 3273 bytes 5 files changed, 354 insertions(+), 108 deletions(-) create mode 100644 src/main/org/insa/graphics/drawing/components/MapZoomControls.java create mode 100644 zoomIn.png create mode 100644 zoomOut.png diff --git a/src/main/org/insa/graphics/drawing/components/BasicDrawing.java b/src/main/org/insa/graphics/drawing/components/BasicDrawing.java index 75a3ee8..cd0230f 100644 --- a/src/main/org/insa/graphics/drawing/components/BasicDrawing.java +++ b/src/main/org/insa/graphics/drawing/components/BasicDrawing.java @@ -5,12 +5,16 @@ import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; +import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; @@ -286,17 +290,42 @@ public class BasicDrawing extends JPanel implements Drawing { // Mapping DrawingClickListener -> MouseEventListener private Map listenerMapping = new IdentityHashMap<>(); + // Zoom controls + private MapZoomControls zoomControls; + /** * Create a new BasicDrawing. * */ public BasicDrawing() { this.zoomAndPanListener = new ZoomAndPanListener(this, ZoomAndPanListener.DEFAULT_MIN_ZOOM_LEVEL, 20, 1.2); + + // Try... + try { + this.zoomControls = new MapZoomControls(this, 0, ZoomAndPanListener.DEFAULT_MIN_ZOOM_LEVEL, 20); + this.zoomControls.addZoomInListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + zoomAndPanListener.zoomIn(); + } + }); + this.zoomControls.addZoomOutListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + zoomAndPanListener.zoomOut(); + } + }); + } + catch (IOException e) { + e.printStackTrace(); + } } @Override public void paintComponent(Graphics g1) { + super.paintComponent(g1); Graphics2D g = (Graphics2D) g1; + AffineTransform sTransform = g.getTransform(); g.clearRect(0, 0, getWidth(), getHeight()); g.setTransform(zoomAndPanListener.getCoordTransform()); @@ -311,6 +340,14 @@ public class BasicDrawing extends JPanel implements Drawing { overlay.draw(g); } } + + g.setTransform(sTransform); + if (this.zoomControls != null) { + this.zoomControls.setZoomLevel(this.zoomAndPanListener.getZoomLevel()); + this.zoomControls.draw(g, getWidth() - this.zoomControls.getWidth() - 20, + this.getHeight() - this.zoomControls.getHeight() - 20, this); + } + } /** @@ -513,6 +550,7 @@ public class BasicDrawing extends JPanel implements Drawing { (this.getHeight() - this.height * scale) / 2); this.zoomAndPanListener.getCoordTransform().scale(scale, scale); this.zoomAndPanListener.setZoomLevel(0); + this.zoomControls.setZoomLevel(0); // Repaint this.repaint(); diff --git a/src/main/org/insa/graphics/drawing/components/MapZoomControls.java b/src/main/org/insa/graphics/drawing/components/MapZoomControls.java new file mode 100644 index 0000000..0e18a33 --- /dev/null +++ b/src/main/org/insa/graphics/drawing/components/MapZoomControls.java @@ -0,0 +1,173 @@ +package org.insa.graphics.drawing.components; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.ImageObserver; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.imageio.ImageIO; + +public class MapZoomControls { + + // Default ID for action events + private static final int ZOOM_IN_ACTION_ID = 0x1; + private static final String ZOOM_IN_ACTION_NAME = "ZoomIn"; + + private static final int ZOOM_OUT_ACTION_ID = 0x2; + private static final String ZOOM_OUT_ACTION_NAME = "ZoomOut"; + + // Height + private static final int DEFAULT_HEIGHT = 18; + + // Default spacing between mark + private static final int DEFAULT_SPACING = 3; + + // Zoom ticks ratio from height (not the current one) + private static final double ZOOM_TICK_HEIGHT_RATIO = 0.3; + + // Zoom ticks color + private static final Color ZOOM_TICK_COLOR = Color.GRAY; + + // Current zoom ticks ratio from height + private static final double CURRENT_ZOOM_TICK_HEIGHT_RATIO = 0.8; + + // Zoom ticks color + private static final Color CURRENT_ZOOM_TICK_COLOR = new Color(25, 25, 25); + + // Use half mark or not + private boolean halfMark = true; + + private int currentLevel = 0; + private final int minLevel, maxLevel; + + // Zoom in/out image and their rectangles. + private final Image zoomIn, zoomOut; + private final Rectangle zoomInRect = new Rectangle(0, 0, 0, 0), zoomOutRect = new Rectangle(0, 0, 0, 0); + + // List of listeners + private final List zoomInListeners = new ArrayList<>(); + private final List zoomOutListeners = new ArrayList<>(); + + public MapZoomControls(Component component, final int defaultZoom, final int minZoom, final int maxZoom) + throws IOException { + + zoomIn = ImageIO.read(new File("zoomIn.png")).getScaledInstance(DEFAULT_HEIGHT, DEFAULT_HEIGHT, + Image.SCALE_SMOOTH); + zoomOut = ImageIO.read(new File("zoomOut.png")).getScaledInstance(DEFAULT_HEIGHT, DEFAULT_HEIGHT, + Image.SCALE_SMOOTH); + + this.currentLevel = defaultZoom; + this.minLevel = minZoom; + this.maxLevel = maxZoom; + + component.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (zoomInRect.contains(e.getPoint()) && currentLevel < maxLevel) { + currentLevel += 1; + for (ActionListener al: zoomInListeners) { + al.actionPerformed(new ActionEvent(this, ZOOM_IN_ACTION_ID, ZOOM_IN_ACTION_NAME)); + } + } + else if (zoomOutRect.contains(e.getPoint()) && currentLevel > minLevel) { + currentLevel -= 1; + for (ActionListener al: zoomOutListeners) { + al.actionPerformed(new ActionEvent(this, ZOOM_OUT_ACTION_ID, ZOOM_OUT_ACTION_NAME)); + } + } + component.repaint(); + } + }); + } + + /** + * Add a zoom-in listener. + * + * @param listener + */ + public void addZoomInListener(ActionListener listener) { + this.zoomInListeners.add(listener); + } + + /** + * Add a zoom-out listener. + * + * @param listener + */ + public void addZoomOutListener(ActionListener listener) { + this.zoomOutListeners.add(listener); + } + + /** + * @return the current zoom level. + */ + public int getZoomLevel() { + return this.currentLevel; + } + + /** + * Set the current zoom level. + * + * @param level + */ + public void setZoomLevel(int level) { + this.currentLevel = level; + } + + /** + * @return Height of this "component" when drawn. + */ + public int getHeight() { + return DEFAULT_HEIGHT; + } + + /** + * @return Width of this "component" when drawn. + */ + public int getWidth() { + return DEFAULT_HEIGHT + 2 + (this.maxLevel - this.minLevel) * DEFAULT_SPACING + 1 + 2 + DEFAULT_HEIGHT; + } + + protected void draw(Graphics2D g, int xoffset, int yoffset, ImageObserver observer) { + + int height = getHeight(); + + // Draw icon + g.drawImage(zoomOut, xoffset, yoffset, observer); + zoomOutRect.setBounds(xoffset, yoffset, DEFAULT_HEIGHT, DEFAULT_HEIGHT); + + // Draw ticks + xoffset += DEFAULT_HEIGHT + 2; + g.setColor(ZOOM_TICK_COLOR); + g.drawLine(xoffset, yoffset + height / 2, xoffset + (this.maxLevel - this.minLevel) * DEFAULT_SPACING + 1, + yoffset + height / 2); + for (int i = 0; i <= (this.maxLevel - this.minLevel); i += halfMark ? 2 : 1) { + g.drawLine(xoffset + i * DEFAULT_SPACING, yoffset + (int) (height * (1 - ZOOM_TICK_HEIGHT_RATIO) / 2), + xoffset + i * DEFAULT_SPACING, yoffset + (int) (height * (1 + ZOOM_TICK_HEIGHT_RATIO) / 2)); + } + + // Draw current ticks + g.setColor(CURRENT_ZOOM_TICK_COLOR); + g.drawLine(xoffset + (currentLevel - this.minLevel) * DEFAULT_SPACING, + yoffset + (int) (height * (1 - CURRENT_ZOOM_TICK_HEIGHT_RATIO) / 2), + xoffset + (currentLevel - this.minLevel) * DEFAULT_SPACING, + yoffset + (int) (height * (1 + CURRENT_ZOOM_TICK_HEIGHT_RATIO) / 2)); + + xoffset += (this.maxLevel - this.minLevel) * DEFAULT_SPACING + 1 + 2; + + g.drawImage(zoomIn, xoffset, yoffset, observer); + zoomInRect.setBounds(xoffset, yoffset, DEFAULT_HEIGHT, DEFAULT_HEIGHT); + + } + +} diff --git a/src/main/org/insa/graphics/drawing/components/ZoomAndPanListener.java b/src/main/org/insa/graphics/drawing/components/ZoomAndPanListener.java index c52f06c..764861d 100644 --- a/src/main/org/insa/graphics/drawing/components/ZoomAndPanListener.java +++ b/src/main/org/insa/graphics/drawing/components/ZoomAndPanListener.java @@ -1,141 +1,176 @@ package org.insa.graphics.drawing.components; -import java.awt.*; -import java.awt.event.*; +import java.awt.Component; +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; public class ZoomAndPanListener implements MouseListener, MouseMotionListener, MouseWheelListener { - public static final int DEFAULT_MIN_ZOOM_LEVEL = -20; - public static final int DEFAULT_MAX_ZOOM_LEVEL = 10; - public static final double DEFAULT_ZOOM_MULTIPLICATION_FACTOR = 1.2; + public static final int DEFAULT_MIN_ZOOM_LEVEL = -20; + public static final int DEFAULT_MAX_ZOOM_LEVEL = 10; + public static final double DEFAULT_ZOOM_MULTIPLICATION_FACTOR = 1.2; - private Component targetComponent; + private Component targetComponent; - private int zoomLevel = 0; - private int minZoomLevel = DEFAULT_MIN_ZOOM_LEVEL; - private int maxZoomLevel = DEFAULT_MAX_ZOOM_LEVEL; - private double zoomMultiplicationFactor = DEFAULT_ZOOM_MULTIPLICATION_FACTOR; + private int zoomLevel = 0; + private int minZoomLevel = DEFAULT_MIN_ZOOM_LEVEL; + private int maxZoomLevel = DEFAULT_MAX_ZOOM_LEVEL; + private double zoomMultiplicationFactor = DEFAULT_ZOOM_MULTIPLICATION_FACTOR; - private Point dragStartScreen; - private Point dragEndScreen; - private AffineTransform coordTransform = new AffineTransform(); + private Point dragStartScreen; + private Point dragEndScreen; + private AffineTransform coordTransform = new AffineTransform(); - public ZoomAndPanListener(Component targetComponent) { - this.targetComponent = targetComponent; - } + public ZoomAndPanListener(Component targetComponent) { + this.targetComponent = targetComponent; + } - public ZoomAndPanListener(Component targetComponent, int minZoomLevel, int maxZoomLevel, double zoomMultiplicationFactor) { - this.targetComponent = targetComponent; - this.minZoomLevel = minZoomLevel; - this.maxZoomLevel = maxZoomLevel; - this.zoomMultiplicationFactor = zoomMultiplicationFactor; - } - - public void translate(double dx, double dy) { - coordTransform.translate(dx, dy); - targetComponent.repaint(); - } + public ZoomAndPanListener(Component targetComponent, int minZoomLevel, int maxZoomLevel, + double zoomMultiplicationFactor) { + this.targetComponent = targetComponent; + this.minZoomLevel = minZoomLevel; + this.maxZoomLevel = maxZoomLevel; + this.zoomMultiplicationFactor = zoomMultiplicationFactor; + } + public void translate(double dx, double dy) { + coordTransform.translate(dx, dy); + targetComponent.repaint(); + } - public void mouseClicked(MouseEvent e) { - } + public void mouseClicked(MouseEvent e) { + } - public void mousePressed(MouseEvent e) { - dragStartScreen = e.getPoint(); - dragEndScreen = null; - } + public void mousePressed(MouseEvent e) { + dragStartScreen = e.getPoint(); + dragEndScreen = null; + } - public void mouseReleased(MouseEvent e) { -// moveCamera(e); - } + public void mouseReleased(MouseEvent e) { + } - public void mouseEntered(MouseEvent e) { - } + public void mouseEntered(MouseEvent e) { + } - public void mouseExited(MouseEvent e) { - } + public void mouseExited(MouseEvent e) { + } - public void mouseMoved(MouseEvent e) { - } + public void mouseMoved(MouseEvent e) { + } - public void mouseDragged(MouseEvent e) { - moveCamera(e); - } + public void mouseDragged(MouseEvent e) { + moveCamera(e); + } - public void mouseWheelMoved(MouseWheelEvent e) { - zoomCamera(e); - } + public void mouseWheelMoved(MouseWheelEvent e) { + zoomCamera(e); + } - private void moveCamera(MouseEvent e) { - try { - dragEndScreen = e.getPoint(); - Point2D.Float dragStart = transformPoint(dragStartScreen); - Point2D.Float dragEnd = transformPoint(dragEndScreen); - double dx = dragEnd.getX() - dragStart.getX(); - double dy = dragEnd.getY() - dragStart.getY(); - coordTransform.translate(dx, dy); - dragStartScreen = dragEndScreen; - dragEndScreen = null; - targetComponent.repaint(); - } catch (NoninvertibleTransformException ex) { - ex.printStackTrace(); - } - } + private void moveCamera(MouseEvent e) { + try { + dragEndScreen = e.getPoint(); + Point2D.Float dragStart = transformPoint(dragStartScreen); + Point2D.Float dragEnd = transformPoint(dragEndScreen); + double dx = dragEnd.getX() - dragStart.getX(); + double dy = dragEnd.getY() - dragStart.getY(); + coordTransform.translate(dx, dy); + dragStartScreen = dragEndScreen; + dragEndScreen = null; + targetComponent.repaint(); + } + catch (NoninvertibleTransformException ex) { + ex.printStackTrace(); + } + } - private void zoomCamera(MouseWheelEvent e) { - try { - int wheelRotation = e.getWheelRotation(); - Point p = e.getPoint(); - if (wheelRotation > 0) { - if (zoomLevel < maxZoomLevel) { - zoomLevel++; - Point2D p1 = transformPoint(p); - coordTransform.scale(1 / zoomMultiplicationFactor, 1 / zoomMultiplicationFactor); - Point2D p2 = transformPoint(p); - coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY()); - targetComponent.repaint(); - } - } else { - if (zoomLevel > minZoomLevel) { - zoomLevel--; - Point2D p1 = transformPoint(p); - coordTransform.scale(zoomMultiplicationFactor, zoomMultiplicationFactor); - Point2D p2 = transformPoint(p); - coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY()); - targetComponent.repaint(); - } - } - } catch (NoninvertibleTransformException ex) { - ex.printStackTrace(); - } - } + private void zoomCamera(MouseWheelEvent e) { + try { + int wheelRotation = e.getWheelRotation(); + Point p = e.getPoint(); + if (wheelRotation < 0) { + if (zoomLevel < maxZoomLevel) { + zoomLevel++; + Point2D p1 = transformPoint(p); + coordTransform.scale(zoomMultiplicationFactor, zoomMultiplicationFactor); + Point2D p2 = transformPoint(p); + coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY()); + targetComponent.repaint(); + } + } + else { + if (zoomLevel > minZoomLevel) { + zoomLevel--; + Point2D p1 = transformPoint(p); + coordTransform.scale(1 / zoomMultiplicationFactor, 1 / zoomMultiplicationFactor); + Point2D p2 = transformPoint(p); + coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY()); + targetComponent.repaint(); + } + } + } + catch (NoninvertibleTransformException ex) { + ex.printStackTrace(); + } + } - private Point2D.Float transformPoint(Point p1) throws NoninvertibleTransformException { + private Point2D.Float transformPoint(Point p1) throws NoninvertibleTransformException { - AffineTransform inverse = coordTransform.createInverse(); + AffineTransform inverse = coordTransform.createInverse(); - Point2D.Float p2 = new Point2D.Float(); - inverse.transform(p1, p2); - return p2; - } + Point2D.Float p2 = new Point2D.Float(); + inverse.transform(p1, p2); + return p2; + } - public int getZoomLevel() { - return zoomLevel; - } + public int getZoomLevel() { + return zoomLevel; + } + public void setZoomLevel(int zoomLevel) { + this.zoomLevel = zoomLevel; + } - public void setZoomLevel(int zoomLevel) { - this.zoomLevel = zoomLevel; - } + public void zoomIn() { + try { + Point p = new Point(targetComponent.getWidth() / 2, targetComponent.getHeight() / 2); + zoomLevel++; + Point2D p1 = transformPoint(p); + coordTransform.scale(zoomMultiplicationFactor, zoomMultiplicationFactor); + Point2D p2 = transformPoint(p); + coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY()); + targetComponent.repaint(); + } + catch (NoninvertibleTransformException ex) { + ex.printStackTrace(); + } + } + public void zoomOut() { + try { + Point p = new Point(targetComponent.getWidth() / 2, targetComponent.getHeight() / 2); + zoomLevel--; + Point2D p1 = transformPoint(p); + coordTransform.scale(1 / zoomMultiplicationFactor, 1 / zoomMultiplicationFactor); + Point2D p2 = transformPoint(p); + coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY()); + targetComponent.repaint(); + } + catch (NoninvertibleTransformException ex) { + ex.printStackTrace(); + } + } - public AffineTransform getCoordTransform() { - return coordTransform; - } + public AffineTransform getCoordTransform() { + return coordTransform; + } - public void setCoordTransform(AffineTransform coordTransform) { - this.coordTransform = coordTransform; - } + public void setCoordTransform(AffineTransform coordTransform) { + this.coordTransform = coordTransform; + } } \ No newline at end of file diff --git a/zoomIn.png b/zoomIn.png new file mode 100644 index 0000000000000000000000000000000000000000..52e80bd51f3722274b4eb34eba924c25b5c12177 GIT binary patch literal 3536 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 Hu0mjf*OWk2 literal 0 HcmV?d00001