/* This program is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package org.opentripplanner.gui;
import java.awt.Point;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import org.opentripplanner.gbannotation.GraphBuilderAnnotation;
import org.opentripplanner.routing.core.State;
import org.opentripplanner.routing.core.TraverseMode;
import org.opentripplanner.routing.edgetype.PatternEdge;
import org.opentripplanner.routing.edgetype.StreetEdge;
import org.opentripplanner.routing.edgetype.StreetTransitLink;
import org.opentripplanner.routing.edgetype.StreetTraversalPermission;
import org.opentripplanner.routing.edgetype.TransitBoardAlight;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.spt.GraphPath;
import org.opentripplanner.routing.vertextype.IntersectionVertex;
import org.opentripplanner.routing.vertextype.TransitStop;
import processing.core.PApplet;
import processing.core.PFont;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.index.strtree.STRtree;
/**
* Processing applet to show a map of the graph. The user can: - Use mouse wheel to zoom (or right drag, or ctrl-drag) - Left drag to pan around the
* map - Left click to send a list of nearby vertices to the associated VertexSelectionListener.
*/
public class ShowGraph extends PApplet implements MouseWheelListener {
private static final int FRAME_RATE = 30;
private static final long serialVersionUID = -8336165356756970127L;
private static final boolean VIDEO = false;
private static final String VIDEO_PATH = "/home/syncopate/pathimage/";
private int videoFrameNumber = 0;
Graph graph;
STRtree vertexIndex;
STRtree edgeIndex;
Envelope modelOuterBounds;
Envelope modelBounds = new Envelope();
VertexSelectionListener selector;
private ArrayList<VertexSelectionListener> selectors;
private List<Vertex> visibleVertices;
private List<Edge> visibleStreetEdges = new ArrayList<Edge>(1000);
private List<Edge> visibleTransitEdges = new ArrayList<Edge>(1000);
private List<Vertex> highlightedVertices = new ArrayList<Vertex>(1000);
private List<Edge> highlightedEdges = new ArrayList<Edge>(1000);
// these queues are filled by a search in another thread, so must be threadsafe
private Queue<Vertex> newHighlightedVertices = new LinkedBlockingQueue<Vertex>();
private Queue<Edge> newHighlightedEdges = new LinkedBlockingQueue<Edge>();
private Vertex highlightedVertex;
private Edge highlightedEdge;
private GraphPath highlightedGraphPath;
protected double mouseModelX;
protected double mouseModelY;
private Point startDrag = null;
private int dragX, dragY;
private boolean ctrlPressed = false;
boolean drawFast = false;
boolean drawStreetEdges = true;
boolean drawTransitEdges = true;
boolean drawLinkEdges = true;
boolean drawStreetVertices = false;
boolean drawTransitStopVertices = true;
private static double lastLabelY;
private static final DecimalFormat latFormatter = new DecimalFormat("00.0000°N ; 00.0000°S");
private static final DecimalFormat lonFormatter = new DecimalFormat("000.0000°E ; 000.0000°W");
private final SimpleDateFormat shortDateFormat = new SimpleDateFormat("HH:mm:ss z");
/* Layer constants */
static final int DRAW_MINIMAL = 0; // XY coordinates
static final int DRAW_VERTICES = 1;
static final int DRAW_TRANSIT = 2;
static final int DRAW_STREETS = 3;
static final int DRAW_ALL = 4;
static final int DRAW_PARTIAL = 6;
private int drawLevel = DRAW_ALL;
private int drawOffset = 0;
/*
* Constructor. Call processing constructor, and register the listener to notify when the user selects vertices.
*/
public ShowGraph(VertexSelectionListener selector, Graph graph) {
super();
this.graph = graph;
this.selector = selector;
this.selectors = new ArrayList<VertexSelectionListener>();
}
/*
* Setup Processing applet
*/
public void setup() {
size(getSize().width, getSize().height, P2D);
/* Build spatial index of vertices and edges */
buildSpatialIndex();
/* Set model bounds to encompass all vertices in the index, and then some */
modelBounds = (Envelope) (vertexIndex.getRoot().getBounds());
modelBounds.expandBy(0.02);
matchAspect();
/* save this zoom level to allow returning to default later */
modelOuterBounds = new Envelope(modelBounds);
/* find and set up the appropriate font */
String[] fonts = PFont.list();
String[] preferredFonts = { "Mono", "Courier" };
PFont font = null;
for (String preferredFontName : preferredFonts) {
for (String fontName : fonts) {
if (fontName.contains(preferredFontName)) {
font = createFont(fontName, 16);
break;
}
}
if (font != null) {
break;
}
}
textFont(font);
textMode(SCREEN);
addMouseWheelListener(this);
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
super.mouseMoved(e);
Point p = e.getPoint();
mouseModelX = toModelX(p.x);
mouseModelY = toModelY(p.y);
}
});
addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent e) {
matchAspect();
drawLevel = DRAW_PARTIAL;
}
});
frameRate(FRAME_RATE);
}
/*
* Zoom in/out proportional to the number of clicks of the mouse wheel.
*/
public void mouseWheelMoved(MouseWheelEvent e) {
double f = e.getWheelRotation() * 0.2;
zoom(f, e.getPoint());
}
/*
* Zoom in/out. Translate the viewing window such that the place under the mouse pointer is a fixed point. If p is null, zoom around the center of
* the viewport.
*/
void zoom(double f, Point p) {
double ex = modelBounds.getWidth() * f;
double ey = modelBounds.getHeight() * f;
modelBounds.expandBy(ex / 2, ey / 2);
if (p != null) {
// Note: Graphics Y coordinates increase down the screen, hence the opposite signs.
double tx = ex * -((p.getX() / this.width) - 0.5);
double ty = ey * +((p.getY() / this.height) - 0.5);
modelBounds.translate(tx, ty);
}
// update the display
drawLevel = DRAW_PARTIAL;
}
public void zoomToDefault() {
modelBounds = new Envelope(modelOuterBounds);
drawLevel = DRAW_ALL;
}
public void zoomOut() {
modelBounds.expandBy(modelBounds.getWidth(), modelBounds.getHeight());
drawLevel = DRAW_ALL;
}
public void zoomToLocation(Coordinate c) {
Envelope e = new Envelope();
e.expandToInclude(c);
e.expandBy(0.002);
modelBounds = e;
matchAspect();
drawLevel = DRAW_ALL;
}
public void zoomToVertex(Vertex v) {
Envelope e = new Envelope();
e.expandToInclude(v.getCoordinate());
e.expandBy(0.002);
modelBounds = e;
drawLevel = DRAW_ALL;
}
/**
* Zoom to an envelope. Used for annotation zoom.
*
* @author mattwigway
*/
public void zoomToEnvelope(Envelope e) {
modelBounds = e;
matchAspect();
drawLevel = DRAW_ALL;
}
void matchAspect() {
/* Basic sinusoidal projection of lat/lon data to square pixels */
double yCenter = modelBounds.centre().y;
float xScale = cos(radians((float) yCenter));
double newX = modelBounds.getHeight() * (1 / xScale)
* ((float) this.getWidth() / this.getHeight());
modelBounds.expandBy((newX - modelBounds.getWidth()) / 2f, 0);
}
/*
* Iterate through all vertices and their (outgoing) edges. If they are of 'interesting' types,
* add them to the corresponding spatial index.
*/
public synchronized void buildSpatialIndex() {
vertexIndex = new STRtree();
edgeIndex = new STRtree();
Envelope env;
// int xminx, xmax, ymin, ymax;
for (Vertex v : graph.getVertices()) {
Coordinate c = v.getCoordinate();
env = new Envelope(c);
vertexIndex.insert(env, v);
for (Edge e : v.getOutgoing()) {
if (e.getGeometry() == null)
continue;
if (e instanceof PatternEdge || e instanceof StreetTransitLink
|| e instanceof StreetEdge) {
env = e.getGeometry().getEnvelopeInternal();
edgeIndex.insert(env, e);
}
}
}
vertexIndex.build();
edgeIndex.build();
}
@SuppressWarnings("unchecked")
private synchronized void findVisibleElements() {
visibleVertices = (List<Vertex>) vertexIndex.query(modelBounds);
visibleStreetEdges.clear();
visibleTransitEdges.clear();
for (Edge de : (Iterable<Edge>) edgeIndex.query(modelBounds)) {
if (de instanceof PatternEdge || de instanceof StreetTransitLink) {
visibleTransitEdges.add(de);
} else if (de instanceof StreetEdge) {
visibleStreetEdges.add(de);
}
}
}
private int drawEdge(Edge e) {
if (e.getGeometry() == null)
return 0; // do not attempt to draw geometry-less edges
Coordinate[] coords = e.getGeometry().getCoordinates();
beginShape();
for (int i = 0; i < coords.length; i++)
vertex((float) toScreenX(coords[i].x), (float) toScreenY(coords[i].y));
endShape();
return coords.length; // should be used to count segments, not edges drawn
}
/* use endpoints instead of geometry for quick updating */
private void drawEdgeFast(Edge e) {
Coordinate[] coords = e.getGeometry().getCoordinates();
Coordinate c0 = coords[0];
Coordinate c1 = coords[coords.length - 1];
line((float) toScreenX(c0.x), (float) toScreenY(c0.y), (float) toScreenX(c1.x),
(float) toScreenY(c1.y));
}
private void drawGraphPath(GraphPath gp) {
// draw edges in different colors according to mode
for (State s : gp.states) {
TraverseMode mode = s.getBackMode();
Edge e = s.getBackEdge();
if (e == null)
continue;
if (mode != null && mode.isTransit()) {
stroke(200, 050, 000);
strokeWeight(6);
drawEdge(e);
}
if (e instanceof StreetEdge) {
StreetTraversalPermission stp = ((StreetEdge) e).getPermission();
if (stp == StreetTraversalPermission.PEDESTRIAN) {
stroke(000, 200, 000);
strokeWeight(6);
drawEdge(e);
} else if (stp == StreetTraversalPermission.BICYCLE) {
stroke(000, 000, 200);
strokeWeight(6);
drawEdge(e);
} else if (stp == StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE) {
stroke(000, 200, 200);
strokeWeight(6);
drawEdge(e);
} else if (stp == StreetTraversalPermission.ALL) {
stroke(200, 200, 200);
strokeWeight(6);
drawEdge(e);
} else {
stroke(64, 64, 64);
strokeWeight(6);
drawEdge(e);
}
}
}
// mark key vertices
lastLabelY = -999;
labelState(gp.states.getFirst(), "begin");
for (State s : gp.states) {
Edge e = s.getBackEdge();
if (e instanceof TransitBoardAlight) {
if (((TransitBoardAlight) e).isBoarding()) {
labelState(s, "board");
} else {
labelState(s, "alight");
}
}
}
labelState(gp.states.getLast(), "end");
if (VIDEO) {
// freeze on final path for a few frames
for (int i = 0; i < 10; i++)
saveVideoFrame();
resetVideoFrameNumber();
}
}
private void labelState(State s, String str) {
fill(240, 240, 240);
Vertex v = s.getVertex();
drawVertex(v, 8);
str += " " + shortDateFormat.format(new Date(s.getTimeSeconds() * 1000));
str += " [" + (int) s.getWeight() + "]";
double x = toScreenX(v.getX()) + 10;
double y = toScreenY(v.getY());
double dy = y - lastLabelY;
if (dy == 0) {
y = lastLabelY + 20;
} else if (Math.abs(dy) < 20) {
y = lastLabelY + Math.signum(dy) * 20;
}
text(str, (float) x, (float) y);
lastLabelY = y;
}
private void drawVertex(Vertex v, double r) {
noStroke();
ellipse(toScreenX(v.getX()), toScreenY(v.getY()), r, r);
}
public synchronized void draw() {
// how many edges to draw before checking whether we need to move on to the next frame
final int BLOCK_SIZE = 1000;
// how many edges to skip over (to ensure a sampling of edges throughout the visible area)
final long DECIMATE = 40;
// 800 instead of 1000 msec, leaving 20% of the time for work other than drawing.
final int FRAME_TIME = 800 / FRAME_RATE;
int startMillis = millis();
if (drawLevel == DRAW_PARTIAL) {
background(15);
stroke(30, 128, 30);
strokeWeight(1);
noFill();
// noSmooth();
int drawIndex = 0;
int drawStart = 0;
int drawCount = 0;
while (drawStart < DECIMATE && drawStart < visibleStreetEdges.size()) {
if (drawFast)
drawEdgeFast(visibleStreetEdges.get(drawIndex));
else
drawEdge(visibleStreetEdges.get(drawIndex));
drawIndex += DECIMATE;
drawCount += 1;
if (drawCount % BLOCK_SIZE == 0 && millis() - startMillis > FRAME_TIME) {
// ran out of time to draw this frame.
// enable fast-drawing when too few edges were drawn:
// drawFast = drawCount < visibleStreetEdges.size() / 10;
// leave edge drawing loop to let other work happen.
break;
}
if (drawIndex >= visibleStreetEdges.size()) {
// start over drawing every DECIMATEth edge, offset by 1
drawStart += 1;
drawIndex = drawStart;
}
}
} else if (drawLevel == DRAW_ALL) {
// } else if (drawLevel == DRAW_STREETS) {
// smooth();
if (drawOffset == 0) {
findVisibleElements();
background(15);
}
if (drawStreetEdges) {
stroke(30, 128, 30); // dark green
strokeWeight(1);
noFill();
// for (Edge e : visibleStreetEdges) drawEdge(e);
while (drawOffset < visibleStreetEdges.size()) {
drawEdge(visibleStreetEdges.get(drawOffset));
drawOffset += 1;
// if (drawOffset % FRAME_SIZE == 0) return;
if (drawOffset % BLOCK_SIZE == 0) {
if (millis() - startMillis > FRAME_TIME)
return;
}
}
}
} else if (drawLevel == DRAW_TRANSIT) {
if (drawTransitEdges) {
stroke(40, 40, 128, 30); // transparent blue
strokeWeight(4);
noFill();
// for (Edge e : visibleTransitEdges) {
while (drawOffset < visibleTransitEdges.size()) {
Edge e = visibleTransitEdges.get(drawOffset);
drawEdge(e);
drawOffset += 1;
if (drawOffset % BLOCK_SIZE == 0) {
if (millis() - startMillis > FRAME_TIME)
return;
}
}
}
} else if (drawLevel == DRAW_VERTICES) {
/* turn off vertex display when zoomed out */
final double METERS_PER_DEGREE_LAT = 111111.111111;
drawTransitStopVertices = (modelBounds.getHeight() * METERS_PER_DEGREE_LAT / this.width < 4);
/* Draw selected visible vertices */
fill(60, 60, 200);
for (Vertex v : visibleVertices) {
if (drawTransitStopVertices && v instanceof TransitStop) {
drawVertex(v, 5);
} else if (v instanceof IntersectionVertex) {
IntersectionVertex iv = (IntersectionVertex) v;
if (iv.isTrafficLight()) {
drawVertex(v, 7);
}
}
}
/* Draw highlighted edges in another color */
noFill();
stroke(200, 200, 000, 16); // yellow transparent edge highlight
strokeWeight(8);
if (highlightedEdges != null) {
for (Edge e : highlightedEdges) {
drawEdge(e);
}
}
/* Draw highlighted graph path in another color */
if (highlightedGraphPath != null) {
drawGraphPath(highlightedGraphPath);
}
/* Draw (single) highlighted edge in highlight color */
if (highlightedEdge != null && highlightedEdge.getGeometry() != null) {
stroke(200, 10, 10, 128);
strokeWeight(8);
drawEdge(highlightedEdge);
}
/* Draw highlighted vertices */
fill(255, 127, 0); // orange fill
noStroke();
if (highlightedVertices != null) {
for (Vertex v : highlightedVertices) {
drawVertex(v, 8);
}
}
/* Draw (single) highlighed vertex in a different color */
if (highlightedVertex != null) {
fill(255, 255, 30);
drawVertex(highlightedVertex, 7);
}
noFill();
} else if (drawLevel == DRAW_MINIMAL) {
if (!newHighlightedEdges.isEmpty())
handleNewHighlights();
// Black background box
fill(0, 0, 0);
stroke(30, 128, 30);
// noStroke();
strokeWeight(1);
rect(3, 3, 303, textAscent() + textDescent() + 6);
// Print lat & lon coordinates
fill(128, 128, 256);
// noStroke();
String output = lonFormatter.format(mouseModelX) + " "
+ latFormatter.format(mouseModelY);
textAlign(LEFT, TOP);
text(output, 6, 6);
}
drawOffset = 0;
if (drawLevel > DRAW_MINIMAL)
drawLevel -= 1; // move to next layer
}
private void handleNewHighlights() {
// fill(0, 0, 0, 1);
// rect(0,0,this.width, this.height);
desaturate();
noFill();
stroke(256, 0, 0, 128); // , 8);
strokeWeight(6);
while (!newHighlightedEdges.isEmpty()) {
Edge de = newHighlightedEdges.poll();
if (de != null) {
drawEdge(de);
highlightedEdges.add(de);
}
}
if (VIDEO)
saveVideoFrame();
}
private void saveVideoFrame() {
save(VIDEO_PATH + "/" + videoFrameNumber++ + ".bmp");
}
private void resetVideoFrameNumber() {
videoFrameNumber = 0;
}
private void desaturate() {
final float f = 8;
loadPixels();
for (int i = 0; i < width * height; i++) {
int c = pixels[i];
float r = red(c);
float g = green(c);
float b = blue(c);
float avg = (r + g + b) / 3;
r += (avg - r) / f;
g += (avg - g) / f;
b += (avg - b) / f;
pixels[i] = color(r, g, b);
}
updatePixels();
}
private double toScreenY(double y) {
return map(y, modelBounds.getMinY(), modelBounds.getMaxY(), getSize().height, 0);
}
private double toScreenX(double x) {
return map(x, modelBounds.getMinX(), modelBounds.getMaxX(), 0, getSize().width);
}
public void keyPressed() {
if (key == CODED && keyCode == CONTROL)
ctrlPressed = true;
}
public void keyReleased() {
if (key == CODED && keyCode == CONTROL)
ctrlPressed = false;
}
@SuppressWarnings("unchecked")
public void mouseClicked() {
Envelope screenEnv = new Envelope(new Coordinate(mouseX, mouseY));
screenEnv.expandBy(4, 4);
Envelope env = new Envelope(toModelX(screenEnv.getMinX()), toModelX(screenEnv.getMaxX()),
toModelY(screenEnv.getMinY()), toModelY(screenEnv.getMaxY()));
List<Vertex> nearby = (List<Vertex>) vertexIndex.query(env);
selector.verticesSelected(nearby);
drawLevel = DRAW_ALL;
}
public void mouseReleased(MouseEvent e) {
startDrag = null;
}
public void mouseDragged(MouseEvent e) {
Point c = e.getPoint();
if (startDrag == null) {
startDrag = c;
dragX = c.x;
dragY = c.y;
}
double dx = dragX - c.x;
double dy = c.y - dragY;
if (ctrlPressed || mouseButton == RIGHT) {
zoom(dy * 0.01, startDrag);
} else {
double tx = modelBounds.getWidth() * dx / getWidth();
double ty = modelBounds.getHeight() * dy / getHeight();
modelBounds.translate(tx, ty);
}
dragX = c.x;
dragY = c.y;
drawLevel = DRAW_PARTIAL;
}
private double toModelY(double y) {
return map(y, 0, getSize().height, modelBounds.getMaxY(), modelBounds.getMinY());
}
private double toModelX(double x) {
return map(x, 0, getSize().width, modelBounds.getMinX(), modelBounds.getMaxX());
}
/**
* A version of ellipse that takes double args, because apparently Java is too stupid to downgrade automatically.
*
* @param d
* @param e
* @param f
* @param g
*/
private void ellipse(double d, double e, double f, double g) {
ellipse((float) d, (float) e, (float) f, (float) g);
}
/**
* Set the Vertex selector to newSelector, and store the old selector on the stack of selectors
*
* @param newSelector
*/
public void pushSelector(VertexSelectionListener newSelector) {
selectors.add(selector);
selector = newSelector;
}
/**
* Restore the previous vertexSelector
*/
public void popSelector() {
selector = selectors.get(selectors.size() - 1);
selectors.remove(selectors.size() - 1);
}
public void highlightVertex(Vertex v) {
Coordinate c = v.getCoordinate();
double xd = 0, yd = 0;
while (!modelBounds.contains(c)) {
xd = modelBounds.getWidth() / 100;
yd = modelBounds.getHeight() / 100;
modelBounds.expandBy(xd, yd);
}
modelBounds.expandBy(xd, yd);
highlightedVertex = v;
drawLevel = DRAW_ALL;
}
public void enqueueHighlightedEdge(Edge de) {
newHighlightedEdges.add(de);
}
public void clearHighlights() {
highlightedEdges.clear();
highlightedVertices.clear();
drawLevel = DRAW_ALL;
}
public void highlightEdge(Edge selected) {
highlightedEdge = selected;
drawLevel = DRAW_ALL;
}
public void highlightGraphPath(GraphPath gp) {
highlightedGraphPath = gp;
// drawLevel = DRAW_ALL;
drawLevel = DRAW_TRANSIT; // leave streets in grey
}
public void setHighlightedVertices(Set<Vertex> vertices) {
highlightedVertices = new ArrayList<Vertex>(vertices);
drawLevel = DRAW_ALL;
}
public void setHighlightedVertices(List<Vertex> vertices) {
highlightedVertices = vertices;
drawLevel = DRAW_ALL;
}
public void setHighlightedEdges(List<Edge> edges) {
highlightedEdges = edges;
drawLevel = DRAW_ALL;
}
public void drawAnotation(GraphBuilderAnnotation anno) {
Envelope env = new Envelope();
Edge e = anno.getReferencedEdge();
if (e != null) {
this.enqueueHighlightedEdge(e);
env.expandToInclude(e.getFromVertex().getCoordinate());
env.expandToInclude(e.getToVertex().getCoordinate());
}
ArrayList<Vertex> vertices = new ArrayList<Vertex>();
Vertex v = anno.getReferencedVertex();
if (v != null) {
env.expandToInclude(v.getCoordinate());
vertices.add(v);
}
if (e == null && v == null)
return;
// make it a little bigger, especially needed for STOP_UNLINKED
env.expandBy(0.02);
// highlight relevant things
this.clearHighlights();
this.setHighlightedVertices(vertices);
// zoom the graph display
this.zoomToEnvelope(env);
// and draw
this.draw();
}
}