// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.turnlanes.gui; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.geom.Ellipse2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; import javax.swing.AbstractAction; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.KeyStroke; import org.openstreetmap.josm.plugins.turnlanes.model.UnexpectedDataException; class JunctionPane extends JComponent { private final class MouseInputProcessor extends MouseAdapter { private int originX; private int originY; private int button; @Override public void mousePressed(MouseEvent e) { JunctionPane.this.requestFocus(); button = e.getButton(); if (button == MouseEvent.BUTTON1) { final Point2D mouse = translateMouseCoords(e); for (InteractiveElement ie : interactives()) { if (ie.contains(mouse, state)) { setState(ie.activate(state)); repaint(); break; } } } originX = e.getX(); originY = e.getY(); } @Override public void mouseReleased(MouseEvent e) { if (dragging != null) { final Point2D mouse = translateMouseCoords(e); setState(dragging.drop(mouse.getX(), mouse.getY(), dropTarget(mouse), state)); } dragging = null; repaint(); } private InteractiveElement dropTarget(Point2D mouse) { for (InteractiveElement ie : interactives()) { if (ie.contains(mouse, state)) { return ie; } } return null; } @Override public void mouseClicked(MouseEvent e) { if (button == MouseEvent.BUTTON1) { final Point2D mouse = translateMouseCoords(e); for (InteractiveElement ie : interactives()) { if (ie.contains(mouse, state)) { setState(ie.click(state)); break; } } } } @Override public void mouseDragged(MouseEvent e) { if (button == MouseEvent.BUTTON1) { final Point2D mouse = translateMouseCoords(e); if (dragging == null) { final Point2D origin = translateCoords(originX, originY); for (InteractiveElement ie : interactives()) { if (ie.contains(origin, state)) { if (ie.beginDrag(origin.getX(), origin.getY())) { dragging = ie; } break; } } } if (dragging != null) { setState(dragging.drag(mouse.getX(), mouse.getY(), dropTarget(mouse), state)); } } else if (button == MouseEvent.BUTTON3) { translate(e.getX() - originX, e.getY() - originY); originX = e.getX(); originY = e.getY(); } } @Override public void mouseWheelMoved(MouseWheelEvent e) { scale(e.getX(), e.getY(), Math.pow(0.8, e.getWheelRotation())); } private Point2D translateMouseCoords(MouseEvent e) { return translateCoords(e.getX(), e.getY()); } private Point2D translateCoords(int x, int y) { final double c = Math.cos(-rotation); final double s = Math.sin(-rotation); final double x2 = -translationX + x / scale; final double y2 = -translationY + y / scale; return new Point2D.Double(x2 * c - y2 * s, x2 * s + y2 * c); } } private static final long serialVersionUID = 6917061040674799271L; private static final Color TRANSPARENT = new Color(0, 0, 0, 0); private final MouseInputProcessor mip = new MouseInputProcessor(); private GuiContainer container; private final JLabel error = new JLabel(""); private int width = 0; private int height = 0; private double rotation = 0; private double scale = 10; private double translationX = 0; private double translationY = 0; private boolean dirty = true; private BufferedImage passive; private BufferedImage interactive; private final NavigableMap<Integer, List<InteractiveElement>> interactives = new TreeMap<>(); private State state; private InteractiveElement dragging; JunctionPane(GuiContainer container) { setJunction(container); setLayout(new GridBagLayout()); error.setOpaque(false); add(error, new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(8, 8, 8, 8), 0, 0)); setFocusable(true); getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), "refresh"); getActionMap().put("refresh", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { setState(new State.Invalid(state)); } }); getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0), "zoomIn"); getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0), "zoomIn"); getActionMap().put("zoomIn", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { scale(Math.pow(0.8, -1)); } }); getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0), "zoomOut"); getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0), "zoomOut"); getActionMap().put("zoomOut", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { scale(Math.pow(0.8, 1)); } }); getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "center"); getActionMap().put("center", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { center(); } }); getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK), "toggleAllTurns"); getActionMap().put("toggleAllTurns", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { toggleAllTurns(); } }); getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_L, InputEvent.CTRL_DOWN_MASK), "rotateLeft"); getActionMap().put("rotateLeft", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { rotation -= Math.PI / 180; setState(new State.Dirty(state)); } }); getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK), "rotateRight"); getActionMap().put("rotateRight", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { rotation += Math.PI / 180; setState(new State.Dirty(state)); } }); getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete"); getActionMap().put("delete", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { setState(state.delete()); } }); } public void setJunction(GuiContainer container) { removeMouseListener(mip); removeMouseMotionListener(mip); removeMouseWheelListener(mip); this.interactives.clear(); this.dragging = null; this.container = container; center(); setState(new State.Invalid(new State.Default())); addMouseListener(mip); addMouseMotionListener(mip); addMouseWheelListener(mip); } private void center() { final Rectangle2D bounds = container.getBounds(); rotation = 0; scale = Math.min(getHeight() / 2 / bounds.getHeight(), getWidth() / 2 / bounds.getWidth()); translationX = -bounds.getCenterX(); translationY = -bounds.getCenterY(); translate(getWidth() / 2d, getHeight() / 2d); } private void toggleAllTurns() { if (state instanceof State.AllTurns) { setState(((State.AllTurns) state).unwrap()); } else { setState(new State.AllTurns(state)); } } private void setState(State state) { error.setText(""); if (state instanceof State.AllTurns) { dirty = true; this.state = state; } else if (state instanceof State.Invalid) { dirty = true; setState(((State.Invalid) state).unwrap()); try { final GuiContainer old = container; container = container.recalculate(); if (old.isEmpty() != container.isEmpty()) { center(); } } catch (UnexpectedDataException e) { displayError(e); } catch (RuntimeException e) { displayError(e); } } else if (state instanceof State.Dirty) { dirty = true; setState(((State.Dirty) state).unwrap()); } else { this.state = state.carryOver(container); } repaint(); } private void displayError(UnexpectedDataException e) { if (e.getKind() == UnexpectedDataException.Kind.MISSING_TAG && UnexpectedDataException.Kind.MISSING_TAG.format("lanes").equals(e.getMessage())) { error.setText(tr("<html>The number of lanes is not specified for one or more roads;" + " please add missing lanes tags.</html>")); } else { displayError((RuntimeException) e); } } private void displayError(RuntimeException e) { error.setText(tr("<html>An error occurred while constructing the model." + " Please run the validator to make sure the data is consistent." + "<br><br>Error: {0}</html>", e.getMessage())); } void scale(int x, int y, double scale) { this.scale *= scale; final double w = getWidth(); final double h = getHeight(); translationX -= (w * (scale - 1)) / (2 * this.scale); translationY -= (h * (scale - 1)) / (2 * this.scale); dirty = true; repaint(); } void scale(double scale) { scale(getWidth() / 2, getHeight() / 2, scale); } void translate(double x, double y) { translationX += x / scale; translationY += y / scale; dirty = true; repaint(); } @Override protected void paintComponent(Graphics g) { if (getWidth() != width || getHeight() != height) { translate((getWidth() - width) / 2d, (getHeight() - height) / 2d); width = getWidth(); height = getHeight(); // translate already set dirty flag passive = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); interactive = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); } if (dirty) { paintPassive((Graphics2D) passive.getGraphics()); dirty = false; } paintInteractive((Graphics2D) interactive.getGraphics()); final Graphics2D g2d = (Graphics2D) g; g2d.drawImage(passive, 0, 0, getWidth(), getHeight(), null); g2d.drawImage(interactive, 0, 0, getWidth(), getHeight(), null); paintChildren(g); } private void paintInteractive(Graphics2D g2d) { g2d.setBackground(TRANSPARENT); g2d.clearRect(0, 0, getWidth(), getHeight()); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); g2d.scale(scale, scale); g2d.translate(translationX, translationY); g2d.rotate(rotation); g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, 0.7f)); for (Map.Entry<Integer, List<InteractiveElement>> e : interactives.entrySet()) { for (InteractiveElement ie : e.getValue()) { ie.paintBackground(g2d, state); } for (InteractiveElement ie : e.getValue()) { ie.paint(g2d, state); } } } private List<InteractiveElement> interactives() { final List<InteractiveElement> result = new ArrayList<>(); for (List<InteractiveElement> ies : interactives.descendingMap().values()) { result.addAll(ies); } return result; } private void paintPassive(Graphics2D g2d) { interactives.clear(); g2d.setBackground(new Color(100, 160, 240)); g2d.clearRect(0, 0, getWidth(), getHeight()); g2d.scale(scale, scale); g2d.translate(translationX, translationY); g2d.rotate(rotation); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setColor(Color.GRAY); for (RoadGui r : container.getRoads()) { addAllInteractives(r.paint(g2d)); } for (JunctionGui j : container.getJunctions()) { addAllInteractives(j.paint(g2d)); dot(g2d, new Point2D.Double(j.x, j.y), container.getLaneWidth() / 5); } } private void addAllInteractives(List<InteractiveElement> ies) { for (InteractiveElement ie : ies) { final List<InteractiveElement> existing = interactives.get(ie.getZIndex()); final List<InteractiveElement> list; if (existing == null) { list = new ArrayList<>(); interactives.put(ie.getZIndex(), list); } else { list = existing; } list.add(ie); } } static void dot(Graphics2D g2d, Point2D p, double r, Color c) { final Color old = g2d.getColor(); g2d.setColor(c); g2d.fill(new Ellipse2D.Double(p.getX() - r, p.getY() - r, 2 * r, 2 * r)); g2d.setColor(old); } static void dot(Graphics2D g2d, Point2D p, double r) { dot(g2d, p, r, Color.RED); } void refresh() { setState(new State.Invalid(state)); } }