// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.pt_assistant.gui; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.Point; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.OsmPrimitiveType; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.RelationMember; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.data.validation.PaintVisitor; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.plugins.pt_assistant.data.PTWay; import org.openstreetmap.josm.plugins.pt_assistant.utils.RouteUtils; import org.openstreetmap.josm.tools.Pair; /** * Visits the primitives to be visualized in the pt_assistant layer * * @author darya * */ public class PTAssistantPaintVisitor extends PaintVisitor { /** The graphics */ private final Graphics g; /** The MapView */ private final MapView mv; /** * Constructor * * @param g graphics * @param mv map view */ public PTAssistantPaintVisitor(Graphics g, MapView mv) { super(g, mv); this.g = g; this.mv = mv; } @Override public void visit(Relation r) { // first, draw primitives: for (RelationMember rm : r.getMembers()) { if (RouteUtils.isPTStop(rm)) { drawStop(rm.getMember()); } else if (RouteUtils.isPTWay(rm)) { if (rm.isWay()) { visit(rm.getWay()); } else if (rm.isRelation()) { visit(rm.getRelation()); } //else { // if the relation has members that do not fit with the // PT_Assistant data model, do nothing //} } //else { // if the relation has members that do not fit with the // PT_Assistant data model, do nothing //} } // in the end, draw labels: HashMap<Long, String> stopOrderMap = new HashMap<>(); int stopCount = 1; for (RelationMember rm : r.getMembers()) { if (RouteUtils.isPTStop(rm) || (rm.getMember().isIncomplete() && (rm.isNode() || rm.hasRole("stop") || rm.hasRole("stop_entry_only") || rm.hasRole("stop_exit_only") || rm.hasRole("platform") || rm.hasRole("platform_entry_only") || rm.hasRole("platform_exit_only")))) { String label = ""; if (stopOrderMap.containsKey(rm.getUniqueId())) { label = stopOrderMap.get(rm.getUniqueId()); label = label + ";" + stopCount; } else { if (r.hasKey("ref")) { label = label + r.get("ref"); } else if (r.hasKey("name")) { label = label + r.get("name"); } else { label = "NA"; } label = label + " - " + stopCount; } stopOrderMap.put(rm.getUniqueId(), label); try { drawStopLabel(rm.getMember(), label); } catch (NullPointerException ex) { // do nothing Main.trace(ex); } stopCount++; } } } @Override public void visit(Way w) { if (w == null) { return; } /*- * oneway values: * 0 two-way street * 1 oneway street in the way's direction * 2 oneway street in ways's direction but public transport allowed * -1 oneway street in reverse direction * -2 oneway street in reverse direction but public transport allowed */ int oneway = 0; if (w.hasTag("junction", "roundabout") || w.hasTag("highway", "motorway")) { oneway = 1; } else if (w.hasTag("oneway", "1") || w.hasTag("oneway", "yes") || w.hasTag("oneway", "true")) { if (w.hasTag("busway", "lane") || w.hasTag("busway:left", "lane") || w.hasTag("busway:right", "lane") || w.hasTag("oneway:bus", "no") || w.hasTag("busway", "opposite_lane") || w.hasTag("oneway:psv", "no") || w.hasTag("trolley_wire", "backward")) { oneway = 2; } else { oneway = 1; } } else if (w.hasTag("oneway", "-1") || w.hasTag("oneway", "reverse")) { if (w.hasTag("busway", "lane") || w.hasTag("busway:left", "lane") || w.hasTag("busway:right", "lane") || w.hasTag("oneway:bus", "no") || w.hasTag("busway", "opposite_lane") || w.hasTag("oneway:psv", "no") || w.hasTag("trolley_wire", "backward")) { oneway = -2; } else { oneway = -1; } } visit(w.getNodes(), oneway); } /** * Variation of the visit method that allows a special visualization of * oneway roads * * @param nodes nodes * @param oneway oneway */ public void visit(List<Node> nodes, int oneway) { Node lastN = null; for (Node n : nodes) { if (lastN == null) { lastN = n; continue; } this.drawSegment(lastN, n, new Color(128, 0, 128, 100), oneway); lastN = n; } } /** * Draw a small rectangle. White if selected (as always) or red otherwise. * * @param n * The node to draw. */ @Override public void visit(Node n) { if (n.isDrawable() && isNodeVisible(n)) { drawNode(n, Color.BLUE); } } /** * Draws a line around the segment * * @param n1 * The first node of segment * @param n2 * The second node of segment * @param color * The color */ protected void drawSegment(Node n1, Node n2, Color color, int oneway) { if (n1.isDrawable() && n2.isDrawable() && isSegmentVisible(n1, n2)) { try { drawSegment(mv.getPoint(n1), mv.getPoint(n2), color, oneway); } catch (NullPointerException ex) { // do nothing Main.trace(ex); } } } /** * Draws a line around the segment * * @param p1 * The first point of segment * @param p2 * The second point of segment * @param color * The color */ protected void drawSegment(Point p1, Point p2, Color color, int oneway) { double t = Math.atan2((double) p2.x - p1.x, (double) p2.y - p1.y); double cosT = 9 * Math.cos(t); double sinT = 9 * Math.sin(t); int[] xPoints = {(int) (p1.x + cosT), (int) (p2.x + cosT), (int) (p2.x - cosT), (int) (p1.x - cosT)}; int[] yPoints = {(int) (p1.y - sinT), (int) (p2.y - sinT), (int) (p2.y + sinT), (int) (p1.y + sinT)}; g.setColor(color); g.fillPolygon(xPoints, yPoints, 4); g.fillOval(p1.x - 9, p1.y - 9, 18, 18); g.fillOval(p2.x - 9, p2.y - 9, 18, 18); if (oneway != 0) { double middleX = (p1.x + p2.x) / 2.0; double middleY = (p1.y + p2.y) / 2.0; double cosTriangle = 6 * Math.cos(t); double sinTriangle = 6 * Math.sin(t); g.setColor(Color.WHITE); if (oneway > 0) { int[] xFillTriangle = {(int) (middleX + cosTriangle), (int) (middleX - cosTriangle), (int) (middleX + 2 * sinTriangle)}; int[] yFillTriangle = {(int) (middleY - sinTriangle), (int) (middleY + sinTriangle), (int) (middleY + 2 * cosTriangle)}; g.fillPolygon(xFillTriangle, yFillTriangle, 3); if (oneway == 2) { int[] xDrawTriangle = {(int) (middleX + cosTriangle), (int) (middleX - cosTriangle), (int) (middleX - 2 * sinTriangle)}; int[] yDrawTriangle = {(int) (middleY - sinTriangle), (int) (middleY + sinTriangle), (int) (middleY - 2 * cosTriangle)}; g.drawPolygon(xDrawTriangle, yDrawTriangle, 3); } } if (oneway < 0) { int[] xFillTriangle = {(int) (middleX + cosTriangle), (int) (middleX - cosTriangle), (int) (middleX - 2 * sinTriangle)}; int[] yFillTriangle = {(int) (middleY - sinTriangle), (int) (middleY + sinTriangle), (int) (middleY - 2 * cosTriangle)}; g.fillPolygon(xFillTriangle, yFillTriangle, 3); if (oneway == -2) { int[] xDrawTriangle = {(int) (middleX + cosTriangle), (int) (middleX - cosTriangle), (int) (middleX + 2 * sinTriangle)}; int[] yDrawTriangle = {(int) (middleY - sinTriangle), (int) (middleY + sinTriangle), (int) (middleY + 2 * cosTriangle)}; g.drawPolygon(xDrawTriangle, yDrawTriangle, 3); } } } } /** * Draws a circle around the node * * @param n * The node * @param color * The circle color */ @Override protected void drawNode(Node n, Color color) { if (mv == null || g == null) { return; } Point p = mv.getPoint(n); if (p == null) { return; } g.setColor(color); g.drawOval(p.x - 5, p.y - 5, 10, 10); } /** * Draws s stop_position as a blue circle; draws a platform as a blue square * * @param primitive primitive */ protected void drawStop(OsmPrimitive primitive) { // find the point to which the stop visualization will be linked: Node n = new Node(primitive.getBBox().getCenter()); Point p = mv.getPoint(n); g.setColor(Color.BLUE); if (primitive.hasTag("public_transport", "stop_position") && p != null) { g.fillOval(p.x - 8, p.y - 8, 16, 16); } else { g.fillRect(p.x - 8, p.y - 8, 16, 16); } } /** * Draws the labels for the stops, which include the ordered position of the * stop in the route and the ref numbers of other routes that use this stop * * @param primitive primitive * @param label label */ protected void drawStopLabel(OsmPrimitive primitive, String label) { // find the point to which the stop visualization will be linked: Node n = new Node(primitive.getBBox().getCenter()); Point p = mv.getPoint(n); if (label != null && !label.equals("")) { g.setColor(Color.WHITE); Font stringFont = new Font("SansSerif", Font.PLAIN, 24); g.setFont(stringFont); g.drawString(label, p.x + 20, p.y - 20); } // draw the ref values of all parent routes: List<String> parentsLabelList = new ArrayList<>(); for (OsmPrimitive parent : primitive.getReferrers()) { if (parent.getType().equals(OsmPrimitiveType.RELATION)) { Relation relation = (Relation) parent; if (RouteUtils.isTwoDirectionRoute(relation) && relation.get("ref") != null && !relation.get("ref").equals("")) { boolean stringFound = false; for (String s : parentsLabelList) { if (s.equals(relation.get("ref"))) { stringFound = true; } } if (!stringFound) { parentsLabelList.add(relation.get("ref")); } } } } Collections.sort(parentsLabelList, new RefTagComparator()); String parentsLabel = ""; for (String s : parentsLabelList) { parentsLabel = parentsLabel + s + ";"; } if (!parentsLabel.equals("")) { // remove the last semicolon: parentsLabel = parentsLabel.substring(0, parentsLabel.length() - 1); g.setColor(new Color(255, 20, 147)); Font parentLabelFont = new Font("SansSerif", Font.ITALIC, 20); g.setFont(parentLabelFont); g.drawString(parentsLabel, p.x + 20, p.y + 20); } } /** * Compares route ref numbers * @author darya * */ private class RefTagComparator implements Comparator<String> { @Override public int compare(String s1, String s2) { if (s1 == null || s1.equals("") || s2 == null || s2.equals("")) { // if at least one of the strings is null or empty, there is no // point in comparing: return 0; } String[] splitString1 = s1.split("\\D"); String[] splitString2 = s2.split("\\D"); if (splitString1.length == 0 && splitString2.length != 0) { // if the first ref does not start a digit and the second ref // starts with a digit: return 1; } if (splitString1.length != 0 && splitString2.length == 0) { // if the first ref starts a digit and the second ref does not // start with a digit: return -1; } if (splitString1.length == 0 && splitString2.length == 0) { // if both ref values do not start with a digit: return s1.compareTo(s2); } String firstNumberString1 = splitString1[0]; String firstNumberString2 = splitString2[0]; try { int firstNumber1 = Integer.valueOf(firstNumberString1); int firstNumber2 = Integer.valueOf(firstNumberString2); if (firstNumber1 > firstNumber2) { return 1; } else if (firstNumber1 < firstNumber2) { return -1; } else { // if the first number is the same: return s1.compareTo(s2); } } catch (NumberFormatException ex) { return s1.compareTo(s2); } } } /** * Visualizes the fix variants, assigns colors to them based on their order * @param fixVariants fix variants */ protected void visitFixVariants(HashMap<Character, List<PTWay>> fixVariants, HashMap<Way, List<Character>> wayColoring) { drawFixVariantsWithParallelLines(wayColoring, fixVariants.size()); Color[] colors = { new Color(255, 0, 0, 150), new Color(0, 255, 0, 150), new Color(0, 0, 255, 150), new Color(255, 255, 0, 150), new Color(0, 255, 255, 150)}; int colorIndex = 0; double letterX = Main.map.mapView.getBounds().getMinX() + 20; double letterY = Main.map.mapView.getBounds().getMinY() + 100; for (Character c : fixVariants.keySet()) { if (fixVariants.get(c) != null) { // drawFixVariant(fixVariants.get(c), colors[colorIndex % 5]); drawFixVariantLetter(c.toString(), colors[colorIndex % 5], letterX, letterY); colorIndex++; letterY = letterY + 60; } } // display the "Esc" label: if (!fixVariants.isEmpty()) { drawFixVariantLetter("Esc", Color.WHITE, letterX, letterY); } } @SuppressWarnings("unused") private void drawFixVariant(List<PTWay> fixVariant, Color color) { for (PTWay ptway : fixVariant) { for (Way way : ptway.getWays()) { for (Pair<Node, Node> nodePair : way.getNodePairs(false)) { drawSegment(nodePair.a, nodePair.b, color, 0); } } } } protected void drawFixVariantsWithParallelLines(Map<Way, List<Character>> wayColoring, int numberOfFixVariants) { HashMap<Character, Color> colors = new HashMap<>(); colors.put('A', new Color(255, 0, 0, 200)); colors.put('B', new Color(0, 255, 0, 200)); colors.put('C', new Color(0, 0, 255, 200)); colors.put('D', new Color(255, 255, 0, 200)); colors.put('E', new Color(0, 255, 255, 200)); for (Way way : wayColoring.keySet()) { List<Character> letterList = wayColoring.get(way); List<Color> wayColors = new ArrayList<>(); for (Character letter : letterList) { wayColors.add(colors.get(letter)); } for (Pair<Node, Node> nodePair : way.getNodePairs(false)) { drawSegmentWithParallelLines(nodePair.a, nodePair.b, wayColors); } } } protected void drawSegmentWithParallelLines(Node n1, Node n2, List<Color> colors) { if (!n1.isDrawable() || !n2.isDrawable() || !isSegmentVisible(n1, n2)) { return; } Point p1 = mv.getPoint(n1); Point p2 = mv.getPoint(n2); double t = Math.atan2((double) p2.x - p1.x, (double) p2.y - p1.y); double cosT = 9 * Math.cos(t); double sinT = 9 * Math.sin(t); double heightCosT = 9 * Math.cos(t); double heightSinT = 9 * Math.sin(t); double prevPointX = p1.x; double prevPointY = p1.y; double nextPointX = p1.x + heightSinT; double nextPointY = p1.y + heightCosT; Color currentColor = colors.get(0); int i = 0; g.setColor(currentColor); g.fillOval(p1.x - 9, p1.y - 9, 18, 18); if (colors.size() == 1) { int[] xPoints = {(int) (p1.x + cosT), (int) (p2.x + cosT), (int) (p2.x - cosT), (int) (p1.x - cosT)}; int[] yPoints = {(int) (p1.y - sinT), (int) (p2.y - sinT), (int) (p2.y + sinT), (int) (p1.y + sinT)}; g.setColor(currentColor); g.fillPolygon(xPoints, yPoints, 4); } else { boolean iterate = true; while (iterate) { currentColor = colors.get(i % colors.size()); int[] xPoints = {(int) (prevPointX + cosT), (int) (nextPointX + cosT), (int) (nextPointX - cosT), (int) (prevPointX - cosT)}; int[] yPoints = {(int) (prevPointY - sinT), (int) (nextPointY - sinT), (int) (nextPointY + sinT), (int) (prevPointY + sinT)}; g.setColor(currentColor); g.fillPolygon(xPoints, yPoints, 4); prevPointX = prevPointX + heightSinT; prevPointY = prevPointY + heightCosT; nextPointX = nextPointX + heightSinT; nextPointY = nextPointY + heightCosT; i++; if ((p1.x < p2.x && nextPointX >= p2.x) || (p1.x >= p2.x && nextPointX <= p2.x)) { iterate = false; } } int[] lastXPoints = {(int) (prevPointX + cosT), (int) (p2.x + cosT), (int) (p2.x - cosT), (int) (prevPointX - cosT)}; int[] lastYPoints = {(int) (prevPointY - sinT), (int) (p2.y - sinT), (int) (p2.y + sinT), (int) (prevPointY + sinT)}; g.setColor(currentColor); g.fillPolygon(lastXPoints, lastYPoints, 4); } g.setColor(currentColor); g.fillOval(p2.x - 9, p2.y - 9, 18, 18); } /** * Visuallizes the letters for each fix variant * @param letter letter * @param color color * @param letterX letter X * @param letterY letter Y */ private void drawFixVariantLetter(String letter, Color color, double letterX, double letterY) { g.setColor(color); Font stringFont = new Font("SansSerif", Font.PLAIN, 50); g.setFont(stringFont); try { g.drawString(letter, (int) letterX, (int) letterY); g.drawString(letter, (int) letterX, (int) letterY); } catch (NullPointerException ex) { // do nothing Main.trace(ex); } } }