/* Copyright (C) 2002 J. M. Spivey */ import java.awt.*; import java.awt.event.*; import java.util.*; /** Display a map, allowing towns and roads to be coloured, and * detecting mouse clicks on towns. * * Also display 'tooltips' giving the name of the town under the * mouse. The smooth way to do this is to make the tip be a window in * its own right -- but that doesn't work for applets, where all * windows must be tagged as applet windows for security. So we do it * by using a panel instead of a canves, and creating a label within * the panel instead. We assume the map layout is such that the label * does not overlap the edge of the panel; otherwise it will be * clipped. */ class MapViewer extends Panel implements Observer { private static final float scale = 0.8f; private static final int offX = -10; private static final int offY = -10; private static final int width = 350; private static final int height = 500; private Map map; private Polygon outlines[]; private SelectionListener selectionListener; private Image buffer; private Map.Town tipTown = null; private Label tipLabel = new TipLabel(); public MapViewer() { this.setLayout(null); this.add(tipLabel); tipLabel.setVisible(false); // At least on Windows, mouseClicked is not called if the mouse // moves between the mouse button being pressed and released. // So we handle the press and release separately. this.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { mouseClick(e, true); } public void mouseReleased(MouseEvent e) { mouseClick(e, false); } }); // Track mouse motion for tooltips this.addMouseMotionListener(new MouseMotionAdapter() { public void mouseMoved(MouseEvent e) { MapViewer.this.mouseMoved(e); } }); } public void setMap(Map map) { this.map = map; // The map's coatlines use coordinates in miles. // Here we transform them into polygons in screen coordinates, // to save doing it every time we draw them. outlines = new Polygon[map.numOutlines()]; int i = 0; for (Enumeration z = map.getOutlines(); z.hasMoreElements();) { Map.Outline o = (Map.Outline) z.nextElement(); int n = o.length(); int x[] = new int[n]; int y[] = new int[n]; for (int j = 0; j < n; j++) { Point p = transform(o.pointAt(j)); x[j] = p.x; y[j] = p.y; } outlines[i++] = new Polygon(x, y, n); } } /** Paint the map on a given graphics context */ public void update(Graphics g) { // Allocate a buffer if necessary. We can't do this until the // native counterpart of the component exists. if (buffer == null) buffer = createImage(width, height); // Use double buffering to avoid flicker during animations // Paint the map into the buffer Graphics g1 = buffer.getGraphics(); paint(g1); // Transfer the buffer contents to the screen g.drawImage(buffer, 0, 0, this); } public void paint(Graphics g) { paintCoast(g); paintNetwork(g); // Paint any tooltip on top of the map super.paint(g); } /** Draw the coatline */ private void paintCoast(Graphics g) { if (map == null) return; for (int i = 0; i < outlines.length; i++) { g.setColor(Color.lightGray); g.fillPolygon(outlines[i]); g.setColor(Color.black); g.drawPolygon(outlines[i]); } } /** Draw the roads and towns */ private void paintNetwork(Graphics g) { if (map == null) return; // Draw the roads for (Enumeration z = map.getRoads(); z.hasMoreElements();) { Map.Road road = (Map.Road) z.nextElement(); Point p1 = transform(road.t1.getLocation()); Point p2 = transform(road.t2.getLocation()); // AWT only has 1-pixel lines. We simulate thicker lines // by drawing each road four times, each slightly offset. g.setColor(road.getColor()); g.drawLine(p1.x, p1.y, p2.x, p2.y); g.drawLine(p1.x+1, p1.y, p2.x+1, p2.y); g.drawLine(p1.x, p1.y+1, p2.x, p2.y+1); g.drawLine(p1.x+1, p1.y+1, p2.x+1, p2.y+1); } // Draw the towns for (Enumeration e = map.getTowns(); e.hasMoreElements();) { Map.Town t = (Map.Town) e.nextElement(); Point p = transform(t.getLocation()); g.setColor(t.getColor()); g.fillRect(p.x-3, p.y-3, 6, 6); g.setColor(Color.black); g.drawRect(p.x-3, p.y-3, 6, 6); } } // Our response when either a town or a road has been re-coloured // is to schedule the whole map for repainting. // A suggestion for speeding things up would be to maintain a bounding // rectangle around the stuff that has changed. /** Respond to changes in the search state */ public void update(Observable pf, Object arg) { repaint(); } /** Register a selectionListener. * * For simplicity, only one listener at a time is supported. */ public void setSelectionListener(SelectionListener s) { selectionListener = s; } private Map.Town clickTown = null; /** Respond to a mouse click by selecting the town */ public void mouseClick(MouseEvent e, boolean down) { Map.Town t = findTown(e.getPoint()); if (down) { // The mouse button has gone down: record where we are clickTown = t; } else { // The mouse button has gone up: perform the action if // we are still over the same town if (selectionListener != null && t != null && t == clickTown) selectionListener.select(t); clickTown = null; } } /** Track mouse movement in order to show tooltips */ private void mouseMoved(MouseEvent e) { Point z = e.getPoint(); if (tipTown != null) { // Tool tip showing: remove if we've left the town if (! hit(z, tipTown)) { // System.out.println("Kill tip"); tipTown = null; tipLabel.setVisible(false); } return; } // No tip showing: find whether we've hit a new town Map.Town t = findTown(z); if (t != null) { // System.out.println("Show tip " + t.getName()); tipTown = t; tipLabel.setVisible(true); tipLabel.setText(t.getName()); tipLabel.setSize(tipLabel.getPreferredSize()); tipLabel.setLocation(z.x, z.y+20); repaint(); } } private static Point transform(Map.Coords p) { return new Point((int) (scale * p.getX() + offX), height - (int) (scale * p.getY() - offY)); } public Dimension getMinimumSize() { return new Dimension(width, height); } public Dimension getPreferredSize() { return getMinimumSize(); } /** Find the town that contains a given point, or return null */ private Map.Town findTown(Point p) { // A linear search of the list of towns. for (Enumeration z = map.getTowns(); z.hasMoreElements();) { Map.Town t = (Map.Town) z.nextElement(); if (hit(p, t)) return t; } return null; } /** Test if a point is within a certain town */ private boolean hit(Point p, Map.Town t) { Point q = transform(t.getLocation()); return (Math.abs(p.x - q.x) <= 3 && Math.abs(p.y - q.y) <= 3); } /** Like a Label, but with a preferred size barely big * enough for the text. */ class TipLabel extends Label { Font tipFont = new Font("SansSerif", Font.PLAIN, 10); public TipLabel() { setBackground(new Color(255, 255, 127)); setFont(tipFont); } /** Calculate the mimimum size */ public Dimension getMinimumSize() { FontMetrics fm = getFontMetrics(tipFont); String label = getText(); if (label == null) label = ""; // Add 4 pixels to the width for safety return new Dimension(fm.stringWidth(label)+4, fm.getHeight()); } /** Calculate the preferred size. */ public Dimension getPreferredSize() { return getMinimumSize(); } } }