/*
* Licensed to GraphHopper GmbH under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*
* GraphHopper GmbH licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.graphhopper.ui;
import com.carrotsearch.hppc.IntIndexedContainer;
import com.graphhopper.GraphHopper;
import com.graphhopper.coll.GHBitSet;
import com.graphhopper.coll.GHTBitSet;
import com.graphhopper.reader.osm.GraphHopperOSM;
import com.graphhopper.routing.*;
import com.graphhopper.routing.ch.PreparationWeighting;
import com.graphhopper.routing.ch.PrepareContractionHierarchies;
import com.graphhopper.routing.util.*;
import com.graphhopper.routing.weighting.Weighting;
import com.graphhopper.storage.CHGraph;
import com.graphhopper.storage.Graph;
import com.graphhopper.storage.NodeAccess;
import com.graphhopper.storage.SPTEntry;
import com.graphhopper.storage.index.LocationIndexTree;
import com.graphhopper.storage.index.QueryResult;
import com.graphhopper.util.*;
import com.graphhopper.util.Parameters.Algorithms;
import com.graphhopper.util.shapes.BBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.util.Random;
/**
* A rough graphical user interface for visualizing the OSM graph. Mainly for debugging algorithms
* and spatial data structures. See e.g. this blog post:
* https://graphhopper.com/blog/2016/01/19/alternative-roads-to-rome/
* <p>
* Use the web module for a better/faster/userfriendly/... alternative!
* <p>
*
* @author Peter Karich
*/
public class MiniGraphUI {
// private final Graph graph;
private final Graph routingGraph;
private final NodeAccess na;
private final MapLayer pathLayer;
private final Weighting weighting;
private final FlagEncoder encoder;
private final RoutingAlgorithmFactory algoFactory;
private final AlgorithmOptions algoOpts;
// for moving
int currentPosX;
int currentPosY;
private Logger logger = LoggerFactory.getLogger(getClass());
private Path path;
private LocationIndexTree index;
private String latLon = "";
private GraphicsWrapper mg;
private JPanel infoPanel;
private LayeredPanel mainPanel;
private MapLayer roadsLayer;
private boolean fastPaint = false;
private QueryResult fromRes;
private QueryResult toRes;
public MiniGraphUI(GraphHopper hopper, boolean debug) {
final Graph graph = hopper.getGraphHopperStorage();
this.na = graph.getNodeAccess();
encoder = hopper.getEncodingManager().getEncoder("car");
HintsMap map = new HintsMap("fastest").
setVehicle("car");
boolean ch = true;
if (ch) {
map.put(Parameters.Landmark.DISABLE, true);
weighting = hopper.getCHFactoryDecorator().getWeightings().get(0);
routingGraph = hopper.getGraphHopperStorage().getGraph(CHGraph.class, weighting);
final RoutingAlgorithmFactory tmpFactory = hopper.getAlgorithmFactory(map);
algoFactory = new RoutingAlgorithmFactory() {
class TmpAlgo extends PrepareContractionHierarchies.DijkstraBidirectionCH implements DebugAlgo {
private final GraphicsWrapper mg;
private Graphics2D g2;
public TmpAlgo(Graph graph, Weighting type, TraversalMode tMode, GraphicsWrapper mg) {
super(graph, type, tMode);
this.mg = mg;
}
@Override
public void setGraphics2D(Graphics2D g2) {
this.g2 = g2;
}
@Override
public void updateBestPath(EdgeIteratorState es, SPTEntry bestEE, int currLoc) {
if (g2 != null)
mg.plotNode(g2, currLoc, Color.YELLOW, 6);
super.updateBestPath(es, bestEE, currLoc);
}
}
@Override
public RoutingAlgorithm createAlgo(Graph g, AlgorithmOptions opts) {
// doable but ugly
Weighting w = ((PrepareContractionHierarchies) tmpFactory).getWeighting();
return new TmpAlgo(g, new PreparationWeighting(w), TraversalMode.NODE_BASED, mg).
setEdgeFilter(new LevelEdgeFilter((CHGraph) routingGraph));
}
};
algoOpts = new AlgorithmOptions(Algorithms.DIJKSTRA_BI, weighting);
} else {
map.put(Parameters.CH.DISABLE, true);
// map.put(Parameters.Landmark.DISABLE, true);
routingGraph = graph;
weighting = hopper.createWeighting(map, encoder, graph);
final RoutingAlgorithmFactory tmpFactory = hopper.getAlgorithmFactory(map);
algoFactory = new RoutingAlgorithmFactory() {
@Override
public RoutingAlgorithm createAlgo(Graph g, AlgorithmOptions opts) {
RoutingAlgorithm algo = tmpFactory.createAlgo(g, opts);
if (algo instanceof AStarBidirection) {
return new DebugAStarBi(g, opts.getWeighting(), opts.getTraversalMode(), mg).
setApproximation(((AStarBidirection) algo).getApproximation());
} else if (algo instanceof AStar) {
return new DebugAStar(g, opts.getWeighting(), opts.getTraversalMode(), mg);
} else if (algo instanceof DijkstraBidirectionRef) {
return new DebugDijkstraBidirection(g, opts.getWeighting(), opts.getTraversalMode(), mg);
} else if (algo instanceof Dijkstra) {
return new DebugDijkstraSimple(g, opts.getWeighting(), opts.getTraversalMode(), mg);
}
return algo;
}
};
algoOpts = new AlgorithmOptions(Algorithms.ASTAR_BI, weighting);
}
logger.info("locations:" + graph.getNodes() + ", debug:" + debug + ", algoOpts:" + algoOpts);
mg = new GraphicsWrapper(graph);
// prepare node quadtree to 'enter' the graph. create a 313*313 grid => <3km
// this.index = new DebugLocation2IDQuadtree(roadGraph, mg);
this.index = (LocationIndexTree) hopper.getLocationIndex();
// this.algo = new DebugDijkstraBidirection(graph, mg);
// this.algo = new DijkstraBidirection(graph);
// this.algo = new DebugAStar(graph, mg);
// this.algo = new AStar(graph);
// this.algo = new DijkstraSimple(graph);
// this.algo = new DebugDijkstraSimple(graph, mg);
infoPanel = new JPanel() {
@Override
protected void paintComponent(Graphics g) {
g.setColor(Color.WHITE);
Rectangle b = infoPanel.getBounds();
g.fillRect(0, 0, b.width, b.height);
g.setColor(Color.BLUE);
g.drawString(latLon, 40, 20);
g.drawString("scale:" + mg.getScaleX(), 40, 40);
int w = mainPanel.getBounds().width;
int h = mainPanel.getBounds().height;
g.drawString(mg.setBounds(0, w, 0, h).toLessPrecisionString(), 40, 60);
}
};
mainPanel = new LayeredPanel();
// TODO make it correct with bitset-skipping too
final GHBitSet bitset = new GHTBitSet(graph.getNodes());
mainPanel.addLayer(roadsLayer = new DefaultMapLayer() {
Random rand = new Random();
@Override
public void paintComponent(Graphics2D g2) {
clearGraphics(g2);
int locs = graph.getNodes();
Rectangle d = getBounds();
BBox b = mg.setBounds(0, d.width, 0, d.height);
if (fastPaint) {
rand.setSeed(0);
bitset.clear();
}
// g2.setColor(Color.BLUE);
// double fromLat = 42.56819, fromLon = 1.603231;
// mg.plotText(g2, fromLat, fromLon, "from");
// QueryResult from = index.findClosest(fromLat, fromLon, EdgeFilter.ALL_EDGES);
// double toLat = 42.571034, toLon = 1.520662;
// mg.plotText(g2, toLat, toLon, "to");
// QueryResult to = index.findClosest(toLat, toLon, EdgeFilter.ALL_EDGES);
//
// g2.setColor(Color.RED.brighter().brighter());
// path = prepare.createAlgo().calcPath(from, to);
// System.out.println("now: " + path.toDetailsString());
// plotPath(path, g2, 1);
g2.setColor(Color.black);
Color[] speedColors = generateColors(15);
AllEdgesIterator edge = graph.getAllEdges();
while (edge.next()) {
if (fastPaint && rand.nextInt(30) > 1)
continue;
int nodeIndex = edge.getBaseNode();
double lat = na.getLatitude(nodeIndex);
double lon = na.getLongitude(nodeIndex);
int nodeId = edge.getAdjNode();
double lat2 = na.getLatitude(nodeId);
double lon2 = na.getLongitude(nodeId);
// mg.plotText(g2, lat, lon, "" + nodeIndex);
if (!b.contains(lat, lon) && !b.contains(lat2, lon2))
continue;
int sum = nodeIndex + nodeId;
if (fastPaint) {
if (bitset.contains(sum))
continue;
bitset.add(sum);
}
// mg.plotText(g2, lat * 0.9 + lat2 * 0.1, lon * 0.9 + lon2 * 0.1, iter.getName());
//mg.plotText(g2, lat * 0.9 + lat2 * 0.1, lon * 0.9 + lon2 * 0.1, "s:" + (int) encoder.getSpeed(iter.getFlags()));
double speed = encoder.getSpeed(edge.getFlags());
Color color;
if (speed >= 120) {
// red
color = speedColors[12];
} else if (speed >= 100) {
color = speedColors[10];
} else if (speed >= 80) {
color = speedColors[8];
} else if (speed >= 60) {
color = speedColors[6];
} else if (speed >= 50) {
color = speedColors[5];
} else if (speed >= 40) {
color = speedColors[4];
} else if (speed >= 30) {
color = Color.GRAY;
} else {
color = Color.LIGHT_GRAY;
}
g2.setColor(color);
boolean fwd = encoder.isForward(edge.getFlags());
boolean bwd = encoder.isBackward(edge.getFlags());
float width = 1.2f;
if (fwd && !bwd) {
mg.plotDirectedEdge(g2, lat, lon, lat2, lon2, width);
} else {
mg.plotEdge(g2, lat, lon, lat2, lon2, width);
}
}
g2.setColor(Color.WHITE);
g2.fillRect(0, 0, 1000, 20);
for (int i = 4; i < speedColors.length; i++) {
g2.setColor(speedColors[i]);
g2.drawString("" + (i * 10), i * 30 - 100, 10);
}
g2.setColor(Color.BLACK);
}
});
mainPanel.addLayer(pathLayer = new DefaultMapLayer() {
@Override
public void paintComponent(final Graphics2D g2) {
if (fromRes == null || toRes == null)
return;
makeTransparent(g2);
QueryGraph qGraph = new QueryGraph(routingGraph).lookup(fromRes, toRes);
RoutingAlgorithm algo = algoFactory.createAlgo(qGraph, algoOpts);
if (algo instanceof DebugAlgo) {
((DebugAlgo) algo).setGraphics2D(g2);
}
StopWatch sw = new StopWatch().start();
logger.info("start searching with " + algo + " from:" + fromRes + " to:" + toRes + " " + weighting);
// GHPoint qp = fromRes.getQueryPoint();
// TIntHashSet set = index.findNetworkEntries(qp.lat, qp.lon, 1);
// TIntIterator nodeIter = set.iterator();
// DistanceCalc distCalc = new DistancePlaneProjection();
// System.out.println("set:" + set.size());
// while (nodeIter.hasNext())
// {
// int nodeId = nodeIter.next();
// double lat = graph.getNodeAccess().getLat(nodeId);
// double lon = graph.getNodeAccess().getLon(nodeId);
// int dist = (int) Math.round(distCalc.calcDist(qp.lat, qp.lon, lat, lon));
// mg.plotText(g2, lat, lon, nodeId + ": " + dist);
// mg.plotNode(g2, nodeId, Color.red);
// }
path = algo.calcPath(fromRes.getClosestNode(), toRes.getClosestNode());
sw.stop();
// if directed edges
if (!path.isFound()) {
logger.warn("path not found! direction not valid?");
return;
}
logger.info("found path in " + sw.getSeconds() + "s with nodes:"
+ path.calcNodes().size() + ", millis: " + path.getTime()
+ ", visited nodes:" + algo.getVisitedNodes());
g2.setColor(Color.BLUE.brighter().brighter());
plotPath(path, g2, 2);
}
});
if (debug) {
// disable double buffering for debugging drawing - nice! when do we need DebugGraphics then?
RepaintManager repaintManager = RepaintManager.currentManager(mainPanel);
repaintManager.setDoubleBufferingEnabled(false);
mainPanel.setBuffering(false);
}
}
public static void main(String[] strs) throws Exception {
CmdArgs args = CmdArgs.read(strs);
GraphHopper hopper = new GraphHopperOSM().init(args).importOrLoad();
boolean debug = args.getBool("minigraphui.debug", false);
new MiniGraphUI(hopper, debug).visualize();
}
public Color[] generateColors(int n) {
Color[] cols = new Color[n];
for (int i = 0; i < n; i++) {
cols[i] = Color.getHSBColor((float) i / (float) n, 0.85f, 1.0f);
}
return cols;
}
// for debugging
private Path calcPath(RoutingAlgorithm algo) {
// int from = index.findID(50.042, 10.19);
// int to = index.findID(50.049, 10.23);
//
//// System.out.println("path " + from + "->" + to);
// return algo.calcPath(from, to);
// System.out.println(GraphUtility.getNodeInfo(graph, 60139, new DefaultEdgeFilter(new CarFlagEncoder()).direction(false, true)));
// System.out.println(((GraphStorage) graph).debug(202947, 10));
// GraphUtility.printInfo(graph, 106511, 10);
return algo.calcPath(162810, 35120);
}
void plotNodeName(Graphics2D g2, int node) {
double lat = na.getLatitude(node);
double lon = na.getLongitude(node);
mg.plotText(g2, lat, lon, "" + node);
}
private Path plotPath(Path tmpPath, Graphics2D g2, int w) {
if (!tmpPath.isFound()) {
logger.info("nothing found " + w);
return tmpPath;
}
double prevLat = Double.NaN;
double prevLon = Double.NaN;
boolean plotNodes = false;
IntIndexedContainer nodes = tmpPath.calcNodes();
if (plotNodes) {
for (int i = 0; i < nodes.size(); i++) {
plotNodeName(g2, nodes.get(i));
}
}
PointList list = tmpPath.calcPoints();
for (int i = 0; i < list.getSize(); i++) {
double lat = list.getLatitude(i);
double lon = list.getLongitude(i);
if (!Double.isNaN(prevLat)) {
mg.plotEdge(g2, prevLat, prevLon, lat, lon, w);
} else {
mg.plot(g2, lat, lon, w);
}
prevLat = lat;
prevLon = lon;
}
logger.info("dist:" + tmpPath.getDistance() + ", path points(" + list.getSize() + ")");
return tmpPath;
}
public void visualize() {
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
int frameHeight = 800;
int frameWidth = 1200;
JFrame frame = new JFrame("GraphHopper UI - Small&Ugly ;)");
frame.setLayout(new BorderLayout());
frame.add(mainPanel, BorderLayout.CENTER);
frame.add(infoPanel, BorderLayout.NORTH);
infoPanel.setPreferredSize(new Dimension(300, 100));
// scale
mainPanel.addMouseWheelListener(new MouseWheelListener() {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
mg.scale(e.getX(), e.getY(), e.getWheelRotation() < 0);
repaintRoads();
}
});
// listener to investigate findID behavior
// MouseAdapter ml = new MouseAdapter() {
//
// @Override public void mouseClicked(MouseEvent e) {
// findIDLat = mg.getLat(e.getY());
// findIDLon = mg.getLon(e.getX());
// findIdLayer.repaint();
// mainPanel.repaint();
// }
//
// @Override public void mouseMoved(MouseEvent e) {
// updateLatLon(e);
// }
//
// @Override public void mousePressed(MouseEvent e) {
// updateLatLon(e);
// }
// };
MouseAdapter ml = new MouseAdapter() {
// for routing:
double fromLat, fromLon;
boolean fromDone = false;
boolean dragging = false;
@Override
public void mouseClicked(MouseEvent e) {
if (!fromDone) {
fromLat = mg.getLat(e.getY());
fromLon = mg.getLon(e.getX());
} else {
double toLat = mg.getLat(e.getY());
double toLon = mg.getLon(e.getX());
StopWatch sw = new StopWatch().start();
logger.info("start searching from " + fromLat + "," + fromLon
+ " to " + toLat + "," + toLon);
// get from and to node id
fromRes = index.findClosest(fromLat, fromLon, EdgeFilter.ALL_EDGES);
toRes = index.findClosest(toLat, toLon, EdgeFilter.ALL_EDGES);
logger.info("found ids " + fromRes + " -> " + toRes + " in " + sw.stop().getSeconds() + "s");
repaintPaths();
}
fromDone = !fromDone;
}
@Override
public void mouseDragged(MouseEvent e) {
dragging = true;
fastPaint = true;
update(e);
updateLatLon(e);
}
@Override
public void mouseReleased(MouseEvent e) {
if (dragging) {
// update only if mouse release comes from dragging! (at the moment equal to fastPaint)
dragging = false;
fastPaint = false;
update(e);
}
}
public void update(MouseEvent e) {
mg.setNewOffset(e.getX() - currentPosX, e.getY() - currentPosY);
repaintRoads();
}
@Override
public void mouseMoved(MouseEvent e) {
updateLatLon(e);
}
@Override
public void mousePressed(MouseEvent e) {
updateLatLon(e);
}
};
mainPanel.addMouseListener(ml);
mainPanel.addMouseMotionListener(ml);
// just for fun
// mainPanel.getInputMap().put(KeyStroke.getKeyStroke("DELETE"), "removedNodes");
// mainPanel.getActionMap().put("removedNodes", new AbstractAction() {
// @Override public void actionPerformed(ActionEvent e) {
// int counter = 0;
// for (CoordTrig<Long> coord : quadTreeNodes) {
// int ret = quadTree.remove(coord.lat, coord.lon);
// if (ret < 1) {
//// logger.info("cannot remove " + coord + " " + ret);
//// ret = quadTree.remove(coord.getLatitude(), coord.getLongitude());
// } else
// counter += ret;
// }
// logger.info("Removed " + counter + " of " + quadTreeNodes.size() + " nodes");
// }
// });
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(frameWidth + 10, frameHeight + 30);
frame.setVisible(true);
}
});
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
void updateLatLon(MouseEvent e) {
latLon = mg.getLat(e.getY()) + "," + mg.getLon(e.getX());
infoPanel.repaint();
currentPosX = e.getX();
currentPosY = e.getY();
}
void repaintPaths() {
pathLayer.repaint();
mainPanel.repaint();
}
void repaintRoads() {
// avoid threading as there should be no updated to scale or offset while painting
// (would to lead to artifacts)
StopWatch sw = new StopWatch().start();
pathLayer.repaint();
roadsLayer.repaint();
mainPanel.repaint();
logger.info("roads painting took " + sw.stop().getSeconds() + " sec");
}
}