be-graphes/src/main/org/insa/graphics/drawing/components/BasicDrawing.java
2018-03-11 16:15:30 +01:00

680 lines
21 KiB
Java

package org.insa.graphics.drawing.components;
import java.awt.BasicStroke;
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.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.Iterator;
import java.util.List;
import javax.swing.JPanel;
import org.insa.graph.Arc;
import org.insa.graph.Graph;
import org.insa.graph.GraphStatistics.BoundingBox;
import org.insa.graph.Node;
import org.insa.graph.Path;
import org.insa.graph.Point;
import org.insa.graphics.drawing.BasicGraphPalette;
import org.insa.graphics.drawing.Drawing;
import org.insa.graphics.drawing.DrawingClickListener;
import org.insa.graphics.drawing.GraphPalette;
import org.insa.graphics.drawing.MercatorProjection;
import org.insa.graphics.drawing.overlays.MarkerOverlay;
import org.insa.graphics.drawing.overlays.MarkerUtils;
import org.insa.graphics.drawing.overlays.Overlay;
import org.insa.graphics.drawing.overlays.PathOverlay;
import org.insa.graphics.drawing.overlays.PointSetOverlay;
/**
* Cette implementation de la classe Dessin produit vraiment un affichage (au
* contraire de la classe DessinInvisible).
*/
public class BasicDrawing extends JPanel implements Drawing {
/**
*
*/
private static final long serialVersionUID = 96779785877771827L;
private abstract class BasicOverlay implements Overlay {
// Visible?
protected boolean visible;
// Color
protected Color color;
public BasicOverlay(Color color) {
this.visible = true;
this.color = color;
}
@Override
public void setColor(Color color) {
this.color = color;
}
@Override
public Color getColor() {
return this.color;
}
@Override
public void setVisible(boolean visible) {
this.visible = visible;
BasicDrawing.this.repaint();
}
@Override
public boolean isVisible() {
return this.visible;
}
@Override
public void delete() {
synchronized (overlays) {
BasicDrawing.this.overlays.remove(this);
}
BasicDrawing.this.repaint();
}
/**
* Draw the given overlay.
*/
public void draw(Graphics2D g) {
if (this.visible) {
drawImpl(g);
}
}
public abstract void drawImpl(Graphics2D g);
public void redraw() {
BasicDrawing.this.repaint();
}
};
private class BasicMarkerOverlay extends BasicOverlay implements MarkerOverlay {
// Marker width and height
public static final int MARKER_WIDTH = 30, MARKER_HEIGHT = 60;
// Point of the marker.
private Point point;
// Image to draw
private Image image;
public BasicMarkerOverlay(Point point, Color color) {
super(color);
this.point = point;
this.color = color;
this.image = MarkerUtils.getMarkerForColor(color);
}
@Override
public Point getPoint() {
return point;
}
@Override
public void setColor(Color color) {
super.setColor(color);
this.image = MarkerUtils.getMarkerForColor(color);
}
@Override
public void moveTo(Point point) {
this.point = point;
BasicDrawing.this.repaint();
}
@Override
public void drawImpl(Graphics2D graphics) {
int px = projection.longitudeToPixelX(getPoint().getLongitude());
int py = projection.latitudeToPixelY(getPoint().getLatitude());
graphics.drawImage(this.image, px - MARKER_WIDTH / 2, py - MARKER_HEIGHT, MARKER_WIDTH,
MARKER_HEIGHT, BasicDrawing.this);
}
};
private class BasicPathOverlay extends BasicOverlay implements PathOverlay {
// List of points
private final List<Point> points;
// Origin / Destination markers.
private BasicMarkerOverlay origin, destination;
public BasicPathOverlay(List<Point> points, Color color, BasicMarkerOverlay origin,
BasicMarkerOverlay destination) {
super(color);
this.points = points;
this.origin = origin;
this.destination = destination;
this.color = color;
}
@Override
public void setColor(Color color) {
super.setColor(color);
this.origin.setColor(color);
this.destination.setColor(color);
}
@Override
public void drawImpl(Graphics2D graphics) {
if (!points.isEmpty()) {
graphics.setStroke(new BasicStroke(2));
graphics.setColor(getColor());
Iterator<Point> itPoint = points.iterator();
Point prev = itPoint.next();
while (itPoint.hasNext()) {
Point curr = itPoint.next();
int x1 = projection.longitudeToPixelX(prev.getLongitude());
int x2 = projection.longitudeToPixelX(curr.getLongitude());
int y1 = projection.latitudeToPixelY(prev.getLatitude());
int y2 = projection.latitudeToPixelY(curr.getLatitude());
graphics.drawLine(x1, y1, x2, y2);
prev = curr;
}
}
if (this.origin != null) {
this.origin.draw(graphics);
}
if (this.destination != null) {
this.destination.draw(graphics);
}
}
};
private class BasicPointSetOverlay extends BasicOverlay implements PointSetOverlay {
// Default point width
private static final int DEFAULT_POINT_WIDTH = 5;
// Image for path / points
private final BufferedImage image;
private final Graphics2D graphics;
private int width = DEFAULT_POINT_WIDTH;
public BasicPointSetOverlay() {
super(Color.BLACK);
this.image = new BufferedImage(BasicDrawing.this.width, BasicDrawing.this.height,
BufferedImage.TYPE_4BYTE_ABGR);
this.graphics = image.createGraphics();
this.graphics.setBackground(new Color(0, 0, 0, 0));
}
@Override
public void setColor(Color color) {
super.setColor(color);
this.graphics.setColor(color);
}
@Override
public void setWidth(int width) {
this.width = Math.max(2, width);
}
@Override
public void setWidthAndColor(int width, Color color) {
setWidth(width);
setColor(color);
}
@Override
public void addPoint(Point point) {
int x = projection.longitudeToPixelX(point.getLongitude()) - this.width / 2;
int y = projection.latitudeToPixelY(point.getLatitude()) - this.width / 2;
this.graphics.fillOval(x, y, this.width, this.width);
BasicDrawing.this.repaint();
}
@Override
public void addPoint(Point point, int width) {
setWidth(width);
addPoint(point);
}
@Override
public void addPoint(Point point, Color color) {
setColor(color);
addPoint(point);
}
@Override
public void addPoint(Point point, int width, Color color) {
setWidth(width);
setColor(color);
addPoint(point);
}
@Override
public void drawImpl(Graphics2D g) {
g.drawImage(this.image, 0, 0, BasicDrawing.this);
}
}
// Default path color.
public static final Color DEFAULT_PATH_COLOR = new Color(66, 134, 244);
// Default palette.
public static final GraphPalette DEFAULT_PALETTE = new BasicGraphPalette();
// Maximum width for the drawing (in pixels).
private static final int MAXIMUM_DRAWING_WIDTH = 2000;
private MercatorProjection projection;
// Width and height of the image
private int width, height;
// Zoom controls
private MapZoomControls zoomControls;
private ZoomAndPanListener zoomAndPanListener;
//
private Image graphImage = null;
private Graphics2D graphGraphics = null;
// List of image for markers
private List<BasicOverlay> overlays = Collections
.synchronizedList(new ArrayList<BasicOverlay>());
// Mapping DrawingClickListener -> MouseEventListener
private List<DrawingClickListener> drawingClickListeners = new ArrayList<>();
/**
* Create a new BasicDrawing.
*
*/
public BasicDrawing() {
setLayout(null);
this.setBackground(new Color(240, 240, 240));
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();
}
this.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent evt) {
if (zoomControls.contains(evt.getPoint())) {
return;
}
Point lonlat = null;
try {
lonlat = getLongitudeLatitude(evt);
}
catch (NoninvertibleTransformException e) {
return;
}
for (DrawingClickListener listener: drawingClickListeners) {
listener.mouseClicked(lonlat);
}
}
});
}
@Override
public void paintComponent(Graphics g1) {
super.paintComponent(g1);
Graphics2D g = (Graphics2D) g1;
AffineTransform sTransform = g.getTransform();
g.setColor(this.getBackground());
g.fillRect(0, 0, getWidth(), getHeight());
g.setTransform(zoomAndPanListener.getCoordTransform());
if (graphImage != null) {
// Draw graph
g.drawImage(graphImage, 0, 0, this);
}
// Draw markers
synchronized (overlays) {
for (BasicOverlay overlay: overlays) {
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() - 10, this);
}
}
/*
* (non-Javadoc)
*
* @see org.insa.graphics.drawing.Drawing#clear()
*/
@Override
public void clear() {
if (this.graphGraphics != null) {
this.graphGraphics.clearRect(0, 0, this.width, this.height);
}
synchronized (overlays) {
this.overlays.clear();
}
this.repaint();
}
/*
* (non-Javadoc)
*
* @see org.insa.graphics.drawing.Drawing#clearOverlays()
*/
@Override
public void clearOverlays() {
synchronized (overlays) {
this.overlays.clear();
}
this.repaint();
}
/**
* @return The current ZoomAndPanListener associated with this drawing.
*/
public ZoomAndPanListener getZoomAndPanListener() {
return this.zoomAndPanListener;
}
/**
* Return the longitude and latitude corresponding to the given position of the
* MouseEvent.
*
* @param event MouseEvent from which longitude/latitude should be retrieved.
*
* @return Point representing the projection of the MouseEvent position in the
* graph/map.
*
* @throws NoninvertibleTransformException if the actual transformation is
* invalid.
*/
protected Point getLongitudeLatitude(MouseEvent event) throws NoninvertibleTransformException {
// Get the point using the inverse transform of the Zoom/Pan object, this gives
// us
// a point within the drawing box (between [0, 0] and [width, height]).
Point2D ptDst = this.zoomAndPanListener.getCoordTransform()
.inverseTransform(event.getPoint(), null);
// Inverse the "projection" on x/y to get longitude and latitude.
return new Point(projection.pixelXToLongitude(ptDst.getX()),
projection.pixelYToLatitude(ptDst.getY()));
}
/*
* (non-Javadoc)
*
* @see
* org.insa.graphics.drawing.Drawing#addDrawingClickListener(org.insa.graphics.
* drawing.DrawingClickListener)
*/
@Override
public void addDrawingClickListener(DrawingClickListener listener) {
this.drawingClickListeners.add(listener);
}
/*
* (non-Javadoc)
*
* @see org.insa.graphics.drawing.Drawing#removeDrawingClickListener(org.insa.
* graphics.drawing.DrawingClickListener)
*/
@Override
public void removeDrawingClickListener(DrawingClickListener listener) {
this.drawingClickListeners.remove(listener);
}
public BasicMarkerOverlay createMarker(Point point, Color color) {
return new BasicMarkerOverlay(point, color);
}
@Override
public MarkerOverlay drawMarker(Point point, Color color) {
BasicMarkerOverlay marker = createMarker(point, color);
synchronized (overlays) {
this.overlays.add(marker);
}
this.repaint();
return marker;
}
@Override
public PointSetOverlay createPointSetOverlay() {
BasicPointSetOverlay ps = new BasicPointSetOverlay();
synchronized (overlays) {
this.overlays.add(ps);
}
return ps;
}
@Override
public PointSetOverlay createPointSetOverlay(int width, Color color) {
PointSetOverlay ps = createPointSetOverlay();
ps.setWidthAndColor(width, color);
return ps;
}
/**
* Draw the given arc.
*
* @param arc Arc to draw.
* @param palette Palette to use to retrieve color and width for arc, or null to
* use current settings.
*/
protected void drawArc(Arc arc, GraphPalette palette, boolean repaint) {
List<Point> pts = arc.getPoints();
if (!pts.isEmpty()) {
if (palette != null) {
this.graphGraphics.setColor(palette.getColorForArc(arc));
this.graphGraphics.setStroke(new BasicStroke(palette.getWidthForArc(arc)));
}
Iterator<Point> it1 = pts.iterator();
Point prev = it1.next();
while (it1.hasNext()) {
Point curr = it1.next();
int x1 = projection.longitudeToPixelX(prev.getLongitude());
int x2 = projection.longitudeToPixelX(curr.getLongitude());
int y1 = projection.latitudeToPixelY(prev.getLatitude());
int y2 = projection.latitudeToPixelY(curr.getLatitude());
graphGraphics.drawLine(x1, y1, x2, y2);
prev = curr;
}
}
if (repaint) {
this.repaint();
}
}
/**
* Initialize the drawing for the given graph.
*
* @param graph
*/
protected void initialize(Graph graph) {
// Clear everything.
this.clear();
BoundingBox box = graph.getGraphInformation().getBoundingBox();
// Find minimum/maximum longitude and latitude.
float minLon = box.getTopLeftPoint().getLongitude(),
maxLon = box.getBottomRightPoint().getLongitude(),
minLat = box.getBottomRightPoint().getLatitude(),
maxLat = box.getTopLeftPoint().getLatitude();
// Add a little delta to avoid drawing on the edge...
float diffLon = maxLon - minLon, diffLat = maxLat - minLat;
float deltaLon = 0.01f * diffLon, deltaLat = 0.01f * diffLat;
// Create the projection and retrieve width and height for the box.
projection = new MercatorProjection(box.extend(deltaLon, deltaLat, deltaLon, deltaLat),
MAXIMUM_DRAWING_WIDTH);
this.width = (int) projection.getImageWidth();
this.height = (int) projection.getImageHeight();
// Create the image
BufferedImage img = new BufferedImage(this.width, this.height,
BufferedImage.TYPE_3BYTE_BGR);
this.graphImage = img;
this.graphGraphics = img.createGraphics();
this.graphGraphics.setBackground(this.getBackground());
this.graphGraphics.clearRect(0, 0, this.width, this.height);
// Set the zoom and pan listener
double scale = 1 / Math.max(this.width / (double) this.getWidth(),
this.height / (double) this.getHeight());
this.zoomAndPanListener.setCoordTransform(this.graphGraphics.getTransform());
this.zoomAndPanListener.getCoordTransform().translate(
(this.getWidth() - this.width * scale) / 2,
(this.getHeight() - this.height * scale) / 2);
this.zoomAndPanListener.getCoordTransform().scale(scale, scale);
this.zoomAndPanListener.setZoomLevel(0);
this.zoomControls.setZoomLevel(0);
// Repaint
this.repaint();
}
@Override
public void drawGraph(Graph graph, GraphPalette palette) {
int repaintModulo = graph.getNodes().size() / 100;
// Initialize the buffered image
this.initialize(graph);
// Remove zoom and pan listener
this.removeMouseListener(zoomAndPanListener);
this.removeMouseMotionListener(zoomAndPanListener);
this.removeMouseWheelListener(zoomAndPanListener);
for (Node node: graph.getNodes()) {
for (Arc arc: node.getSuccessors()) {
// Draw arcs only if there are one-way arcs or if origin is lower than
// destination, avoid drawing two-ways arc twice.
if (arc.getRoadInformation().isOneWay()
|| arc.getOrigin().compareTo(arc.getDestination()) < 0) {
drawArc(arc, palette, false);
}
}
if (node.getId() % repaintModulo == 0) {
this.repaint();
}
}
this.repaint();
// Re-add zoom and pan listener
this.addMouseListener(zoomAndPanListener);
this.addMouseMotionListener(zoomAndPanListener);
this.addMouseWheelListener(zoomAndPanListener);
}
@Override
public void drawGraph(Graph graph) {
drawGraph(graph, DEFAULT_PALETTE);
}
@Override
public PathOverlay drawPath(Path path, Color color, boolean markers) {
List<Point> points = new ArrayList<Point>();
if (!path.isEmpty()) {
points.add(path.getOrigin().getPoint());
for (Arc arc: path.getArcs()) {
Iterator<Point> itPoint = arc.getPoints().iterator();
// Discard origin each time
itPoint.next();
while (itPoint.hasNext()) {
points.add(itPoint.next());
}
}
}
BasicMarkerOverlay origin = null, destination = null;
if (markers && !path.isEmpty()) {
origin = createMarker(path.getOrigin().getPoint(), color);
destination = createMarker(path.getDestination().getPoint(), color);
}
BasicPathOverlay overlay = new BasicPathOverlay(points, color, origin, destination);
synchronized (overlays) {
this.overlays.add(overlay);
}
this.repaint();
return overlay;
}
@Override
public PathOverlay drawPath(Path path, Color color) {
return drawPath(path, color, true);
}
@Override
public PathOverlay drawPath(Path path) {
return drawPath(path, DEFAULT_PATH_COLOR);
}
@Override
public PathOverlay drawPath(Path path, boolean markers) {
return drawPath(path, DEFAULT_PATH_COLOR, markers);
}
}