// License: GPL. For details, see LICENSE file.
package com.innovant.josm.plugin.routing;
import static org.openstreetmap.josm.tools.I18n.marktr;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import javax.swing.Action;
import javax.swing.Icon;
import org.apache.log4j.Logger;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.RenameLayerAction;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.gui.NavigatableComponent;
import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.tools.ColorHelper;
import org.openstreetmap.josm.tools.ImageProvider;
import com.innovant.josm.jrt.osm.OsmEdge;
/**
* A JOSM layer that encapsulates the representation of the shortest path.
* @author juangui
* @author Jose Vidal
*/
public class RoutingLayer extends Layer {
public enum PreferencesKeys {
KEY_ACTIVE_ROUTE_COLOR(marktr("routing active route")),
KEY_INACTIVE_ROUTE_COLOR(marktr("routing inactive route")),
KEY_ROUTE_WIDTH("routing.route.width"),
KEY_ROUTE_SELECT("routing.route.select");
public final String key;
PreferencesKeys(String key) {
this.key = key;
}
public String getKey() {
return key;
}
}
/**
* Logger
*/
static Logger logger = Logger.getLogger(RoutingLayer.class);
/**
* Constant
*/
private static final double ARROW_PHI = Math.toRadians(20);
/**
* Routing Model
*/
private RoutingModel routingModel;
/**
* Start, Middle and End icons
*/
private Icon startIcon, middleIcon, endIcon;
/**
* Associated OSM layer
*/
private OsmDataLayer dataLayer;
/**
* Default constructor
* @param name Layer name.
*/
public RoutingLayer(String name, OsmDataLayer dataLayer) {
super(name);
logger.debug("Creating Routing Layer...");
if (startIcon == null) startIcon = ImageProvider.get("routing", "startflag");
if (middleIcon == null) middleIcon = ImageProvider.get("routing", "middleflag");
if (endIcon == null) endIcon = ImageProvider.get("routing", "endflag");
this.dataLayer = dataLayer;
this.routingModel = new RoutingModel(dataLayer.data);
logger.debug("Routing Layer created.");
this.routingModel.routingGraph.createGraph(); /* construct the graph right after we we create the layer */
Main.map.repaint(); /* update MapView */
}
/**
* Getter Routing Model.
* @return the routingModel
*/
public RoutingModel getRoutingModel() {
return this.routingModel;
}
/**
* Gets associated data layer
* @return OsmDataLayer associated to the RoutingLayer
*/
public OsmDataLayer getDataLayer() {
return dataLayer;
}
/**
* Gets nearest node belonging to a highway tagged way
* @param p Point on the screen
* @return The nearest highway node, in the range of the snap distance
*/
public final Node getNearestHighwayNode(Point p) {
Node nearest = null;
int snapDistance = NavigatableComponent.PROP_SNAP_DISTANCE.get();
double minDist = 0;
for (Way w : dataLayer.data.getWays()) {
if (w.isDeleted() || w.isIncomplete() || w.get("highway") == null) continue;
for (Node n : w.getNodes()) {
if (n.isDeleted() || n.isIncomplete()) continue;
Point P = Main.map.mapView.getPoint(n);
double dist = p.distance(P);
if (dist < snapDistance) {
if ((nearest == null) || (dist < minDist)) {
nearest = n;
minDist = dist;
}
}
}
}
return nearest;
}
@Override
public Icon getIcon() {
Icon icon = ImageProvider.get("layer", "routing_small");
return icon;
}
@Override
public Object getInfoComponent() {
String info = "<html>"
+ "<body>"
+"Graph Vertex: "+this.routingModel.routingGraph.getVertexCount()+"<br/>"
+"Graph Edges: "+this.routingModel.routingGraph.getEdgeCount()+"<br/>"
+ "</body>"
+ "</html>";
return info;
}
@Override
public Action[] getMenuEntries() {
Collection<Action> components = new ArrayList<>();
components.add(LayerListDialog.getInstance().createShowHideLayerAction());
// components.add(new JMenuItem(new LayerListDialog.ShowHideMarkerText(this)));
components.add(LayerListDialog.getInstance().createDeleteLayerAction());
components.add(SeparatorLayerAction.INSTANCE);
components.add(new RenameLayerAction(getAssociatedFile(), this));
components.add(SeparatorLayerAction.INSTANCE);
components.add(new LayerListPopup.InfoAction(this));
return components.toArray(new Action[0]);
}
@Override
public String getToolTipText() {
String tooltip = this.routingModel.routingGraph.getVertexCount() + " vertices, "
+ this.routingModel.routingGraph.getEdgeCount() + " edges";
return tooltip;
}
@Override
public boolean isMergable(Layer other) {
return false;
}
@Override
public void mergeFrom(Layer from) {
// This layer is not mergable, so do nothing
}
@Override
public void paint(Graphics2D g, MapView mv, Bounds bounds) {
boolean isActiveLayer = (mv.getLayerManager().getActiveLayer().equals(this));
// Get routing nodes (start, middle, end)
List<Node> nodes = routingModel.getSelectedNodes();
// Get path stroke color from preferences
// Color is different for active and inactive layers
Color color;
if (isActiveLayer) {
color = Main.pref.getColor(PreferencesKeys.KEY_ACTIVE_ROUTE_COLOR.key, Color.RED);
} else {
color = Main.pref.getColor(PreferencesKeys.KEY_INACTIVE_ROUTE_COLOR.key, Color.decode("#dd2222"));
}
// Get path stroke width from preferences
String widthString = Main.pref.get(PreferencesKeys.KEY_ROUTE_WIDTH.key);
if (widthString.length() == 0) {
widthString = "2"; /* I think 2 is better */
// FIXME add after good width is found: Main.pref.put(KEY_ROUTE_WIDTH, widthString);
}
int width = Integer.parseInt(widthString);
// draw our graph
if (isActiveLayer) {
if (routingModel != null) {
if (routingModel.routingGraph != null && routingModel.routingGraph.getGraph() != null) {
Set<OsmEdge> graphEdges = routingModel.routingGraph.getGraph().edgeSet();
if (!graphEdges.isEmpty()) {
Color color2 = ColorHelper.html2color("#00ff00"); /* just green for now */
OsmEdge firstedge = (OsmEdge) graphEdges.toArray()[0];
Point from = mv.getPoint(firstedge.fromEastNorth());
g.drawRect(from.x-4, from.y+4, from.x+4, from.y-4);
for (OsmEdge edge : graphEdges) {
drawGraph(g, mv, edge, color2, width);
}
}
}
}
}
if (nodes == null || nodes.size() == 0) return;
// Paint routing path
List<OsmEdge> routeEdges = routingModel.getRouteEdges();
if (routeEdges != null) {
for (OsmEdge edge : routeEdges) {
drawEdge(g, mv, edge, color, width, true);
}
}
// paint start icon
Node node = nodes.get(0);
Point screen = mv.getPoint(node);
startIcon.paintIcon(mv, g, screen.x - startIcon.getIconWidth()/2,
screen.y - startIcon.getIconHeight());
// paint middle icons
for (int index = 1; index < nodes.size() - 1; ++index) {
node = nodes.get(index);
screen = mv.getPoint(node);
middleIcon.paintIcon(mv, g, screen.x - startIcon.getIconWidth()/2,
screen.y - middleIcon.getIconHeight());
}
// paint end icon
if (nodes.size() > 1) {
node = nodes.get(nodes.size() - 1);
screen = mv.getPoint(node);
endIcon.paintIcon(mv, g, screen.x - startIcon.getIconWidth()/2,
screen.y - endIcon.getIconHeight());
}
}
@Override
public void visitBoundingBox(BoundingXYVisitor v) {
for (Node node : routingModel.getSelectedNodes()) {
v.visit(node);
}
}
@Override
public void destroy() {
routingModel.reset();
// layerAdded = false;
}
/**
* Draw a line with the given color.
*/
private void drawEdge(Graphics g, MapView mv, OsmEdge edge, Color col, int width,
boolean showDirection) {
g.setColor(col);
Point from;
Point to;
from = mv.getPoint(edge.fromEastNorth());
to = mv.getPoint(edge.toEastNorth());
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Anti-alias!
Stroke oldStroke = g2d.getStroke();
g2d.setStroke(new BasicStroke(width)); // thickness
g2d.drawLine(from.x, from.y, to.x, to.y);
if (showDirection) {
double t = Math.atan2(to.y-from.y, to.x-from.x) + Math.PI;
g.drawLine(to.x, to.y, (int) (to.x + 10*Math.cos(t-ARROW_PHI)), (int) (to.y + 10*Math.sin(t-ARROW_PHI)));
g.drawLine(to.x, to.y, (int) (to.x + 10*Math.cos(t+ARROW_PHI)), (int) (to.y + 10*Math.sin(t+ARROW_PHI)));
}
g2d.setStroke(oldStroke);
}
private void drawGraph(Graphics g, MapView mv, OsmEdge edge, Color col, int width) {
g.setColor(col);
Point from;
Point to;
from = mv.getPoint(edge.fromEastNorth());
to = mv.getPoint(edge.toEastNorth());
Graphics2D g2d = (Graphics2D) g;
Stroke oldStroke = g2d.getStroke();
g2d.setStroke(new BasicStroke(width)); // thickness
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Anti-alias!
g2d.drawLine(from.x, from.y, to.x, to.y);
g2d.drawRect(to.x- 4, to.y+4, 4, 4);
g2d.setStroke(oldStroke);
}
}