// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.osm.visitor.paint; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.geom.GeneralPath; import java.awt.geom.Rectangle2D; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import javax.swing.ImageIcon; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.gui.NavigatableComponent; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.LanguageInfo; public class MapPainter { private final Graphics2D g; private final NavigatableComponent nc; private final boolean inactive; private final boolean useStrokes; private final boolean showNames; private final boolean showIcons; private final Color inactiveColor; private final Color textColor; private final Color selectedColor; private final Color areaTextColor; private final Color nodeColor; private final Color backgroundColor; private final Font orderFont; private final int fillAlpha; private final int virtualNodeSize; private final int virtualNodeSpace; private final int segmentNumberSpace; private final double circum; private final Collection<String> regionalNameOrder; public MapPainter(MapPaintSettings settings, Graphics2D g, boolean inactive, NavigatableComponent nc, boolean virtual, double dist, double circum) { this.g = g; this.inactive = inactive; this.nc = nc; this.useStrokes = settings.getUseStrokesDistance() > dist; this.showNames = settings.getShowNamesDistance() > dist; this.showIcons = settings.getShowIconsDistance() > dist; this.inactiveColor = PaintColors.INACTIVE.get(); this.textColor = PaintColors.TEXT.get(); this.selectedColor = PaintColors.SELECTED.get(); this.areaTextColor = PaintColors.AREA_TEXT.get(); this.nodeColor = PaintColors.NODE.get(); this.backgroundColor = PaintColors.BACKGROUND.get(); this.orderFont = new Font(Main.pref.get("mappaint.font", "Helvetica"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8)); this.fillAlpha = Math.min(255, Math.max(0, Integer.valueOf(Main.pref.getInteger("mappaint.fillalpha", 50)))); this.virtualNodeSize = virtual ? Main.pref.getInteger("mappaint.node.virtual-size", 8) / 2 : 0; this.virtualNodeSpace = Main.pref.getInteger("mappaint.node.virtual-space", 70); this.segmentNumberSpace = Main.pref.getInteger("mappaint.segmentnumber.space", 40); String[] names = {"name:" + LanguageInfo.getJOSMLocaleCode(), "name", "int_name", "ref", "operator", "brand", "addr:housenumber"}; this.regionalNameOrder = Main.pref.getCollection("mappaint.nameOrder", Arrays.asList(names)); this.circum = circum; } public void drawWay(Way way, Color color, int width, float dashed[], Color dashedColor, boolean showDirection, boolean reversedDirection, boolean showHeadArrowOnly) { GeneralPath path = new GeneralPath(); Point lastPoint = null; Iterator<Node> it = way.getNodes().iterator(); while (it.hasNext()) { Node n = it.next(); Point p = nc.getPoint(n); if(lastPoint != null) { drawSegment(path, lastPoint, p, showHeadArrowOnly ? !it.hasNext() : showDirection, reversedDirection); } lastPoint = p; } displaySegments(path, color, width, dashed, dashedColor); } private void displaySegments(GeneralPath path, Color color, int width, float dashed[], Color dashedColor) { g.setColor(inactive ? inactiveColor : color); if (useStrokes) { if (dashed.length > 0) { g.setStroke(new BasicStroke(width,BasicStroke.CAP_BUTT,BasicStroke.JOIN_ROUND,0, dashed,0)); } else { g.setStroke(new BasicStroke(width,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND)); } } g.draw(path); if(!inactive && useStrokes && dashedColor != null) { g.setColor(dashedColor); if (dashed.length > 0) { float[] dashedOffset = new float[dashed.length]; System.arraycopy(dashed, 1, dashedOffset, 0, dashed.length - 1); dashedOffset[dashed.length-1] = dashed[0]; float offset = dashedOffset[0]; g.setStroke(new BasicStroke(width,BasicStroke.CAP_BUTT,BasicStroke.JOIN_ROUND,0,dashedOffset,offset)); } else { g.setStroke(new BasicStroke(width,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND)); } g.draw(path); } if(useStrokes) { g.setStroke(new BasicStroke()); } } private static final double PHI = Math.toRadians(20); private static final double cosPHI = Math.cos(PHI); private static final double sinPHI = Math.sin(PHI); private void drawSegment(GeneralPath path, Point p1, Point p2, boolean showDirection, boolean reversedDirection) { boolean drawIt = false; if (Main.isOpenjdk) { /** * Work around openjdk bug. It leads to drawing artefacts when zooming in a lot. (#4289, #4424) * (It looks like int overflow when clipping.) We do custom clipping. */ Rectangle bounds = g.getClipBounds(); bounds.grow(100, 100); // avoid arrow heads at the border LineClip clip = new LineClip(); drawIt = clip.cohenSutherland(p1.x, p1.y, p2.x, p2.y, bounds.x, bounds.y, bounds.x+bounds.width, bounds.y+bounds.height); p1 = clip.getP1(); p2 = clip.getP2(); } else { drawIt = isSegmentVisible(p1, p2); } if (drawIt) { /* draw segment line */ path.moveTo(p1.x, p1.y); path.lineTo(p2.x, p2.y); /* draw arrow */ if (showDirection) { Point q1 = p1; Point q2 = p2; if (reversedDirection) { q1 = p2; q2 = p1; path.moveTo(q2.x, q2.y); } final double l = 10. / q1.distance(q2); final double sx = l * (q1.x - q2.x); final double sy = l * (q1.y - q2.y); path.lineTo (q2.x + (int) Math.round(cosPHI * sx - sinPHI * sy), q2.y + (int) Math.round(sinPHI * sx + cosPHI * sy)); path.moveTo (q2.x + (int) Math.round(cosPHI * sx + sinPHI * sy), q2.y + (int) Math.round(- sinPHI * sx + cosPHI * sy)); path.lineTo(q2.x, q2.y); } } } private boolean isSegmentVisible(Point p1, Point p2) { if ((p1.x < 0) && (p2.x < 0)) return false; if ((p1.y < 0) && (p2.y < 0)) return false; if ((p1.x > nc.getWidth()) && (p2.x > nc.getWidth())) return false; if ((p1.y > nc.getHeight()) && (p2.y > nc.getHeight())) return false; return true; } public void drawNodeIcon(Node n, ImageIcon icon, boolean annotate, boolean selected, String name) { Point p = nc.getPoint(n); if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth()) || (p.y > nc.getHeight())) return; int w = icon.getIconWidth(), h=icon.getIconHeight(); icon.paintIcon ( nc, g, p.x-w/2, p.y-h/2 ); if(name != null) { if (inactive || n.isDisabled()) { g.setColor(inactiveColor); } else { g.setColor(textColor); } Font defaultFont = g.getFont(); g.setFont (orderFont); g.drawString (name, p.x+w/2+2, p.y+h/2+2); g.setFont(defaultFont); } if (selected) { g.setColor ( selectedColor ); g.drawRect (p.x-w/2-2, p.y-h/2-2, w+4, h+4); } } /** * Draw the node as small rectangle with the given color. * * @param n The node to draw. * @param color The color of the node. */ public void drawNode(Node n, Color color, int size, boolean fill, String name) { if (size > 1) { int radius = size / 2; Point p = nc.getPoint(n); if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth()) || (p.y > nc.getHeight())) return; if (inactive || n.isDisabled()) { g.setColor(inactiveColor); } else { g.setColor(color); } if (fill) { g.fillRect(p.x - radius, p.y - radius, size, size); g.drawRect(p.x - radius, p.y - radius, size, size); } else { g.drawRect(p.x - radius, p.y - radius, size, size); } if(name != null) { if (inactive || n.isDisabled()) { g.setColor(inactiveColor); } else { g.setColor(textColor); } Font defaultFont = g.getFont(); g.setFont (orderFont); g.drawString (name, p.x+radius+2, p.y+radius+2); g.setFont(defaultFont); } } } protected void drawArea(Polygon polygon, Color color, String name) { /* set the opacity (alpha) level of the filled polygon */ g.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), fillAlpha)); g.fillPolygon(polygon); if (name != null) { Rectangle pb = polygon.getBounds(); FontMetrics fontMetrics = g.getFontMetrics(orderFont); // if slow, use cache Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font) // Point2D c = getCentroid(polygon); // Using the Centroid is Nicer for buildings like: +--------+ // but this needs to be fast. As most houses are | 42 | // boxes anyway, the center of the bounding box +---++---+ // will have to do. ++ // Centroids are not optimal either, just imagine a U-shaped house. // Point2D c = new Point2D.Double(pb.x + pb.width / 2.0, pb.y + pb.height / 2.0); // Rectangle2D.Double centeredNBounds = // new Rectangle2D.Double(c.getX() - nb.getWidth()/2, // c.getY() - nb.getHeight()/2, // nb.getWidth(), // nb.getHeight()); Rectangle centeredNBounds = new Rectangle(pb.x + (int)((pb.width - nb.getWidth())/2.0), pb.y + (int)((pb.height - nb.getHeight())/2.0), (int)nb.getWidth(), (int)nb.getHeight()); if ((pb.width >= nb.getWidth() && pb.height >= nb.getHeight()) && // quick check polygon.contains(centeredNBounds) // slow but nice ) { g.setColor(areaTextColor); Font defaultFont = g.getFont(); g.setFont (orderFont); g.drawString (name, (int)(centeredNBounds.getMinX() - nb.getMinX()), (int)(centeredNBounds.getMinY() - nb.getMinY())); g.setFont(defaultFont); } } } public void drawRestriction(ImageIcon icon, Point pVia, double vx, double vx2, double vy, double vy2, double iconAngle, boolean selected) { /* rotate icon with direction last node in from to */ ImageIcon rotatedIcon = ImageProvider.createRotatedImage(null /*icon2*/, icon, iconAngle); /* scale down icon to 16*16 pixels */ ImageIcon smallIcon = new ImageIcon(rotatedIcon.getImage().getScaledInstance(16 , 16, Image.SCALE_SMOOTH)); int w = smallIcon.getIconWidth(), h=smallIcon.getIconHeight(); smallIcon.paintIcon (nc, g, (int)(pVia.x+vx+vx2)-w/2, (int)(pVia.y+vy+vy2)-h/2 ); if (selected) { g.setColor(selectedColor); g.drawRect((int)(pVia.x+vx+vx2)-w/2-2,(int)(pVia.y+vy+vy2)-h/2-2, w+4, h+4); } } public void drawVirtualNodes(Collection<Way> ways) { if (virtualNodeSize != 0) { GeneralPath path = new GeneralPath(); for (Way osm: ways){ if (osm.isUsable() && !osm.isFiltered() && !osm.isDisabled()) { visitVirtual(path, osm); } } g.setColor(nodeColor); g.draw(path); } } public void visitVirtual(GeneralPath path, Way w) { Iterator<Node> it = w.getNodes().iterator(); if (it.hasNext()) { Point lastP = nc.getPoint(it.next()); while(it.hasNext()) { Point p = nc.getPoint(it.next()); if(isSegmentVisible(lastP, p) && isLargeSegment(lastP, p, virtualNodeSpace)) { int x = (p.x+lastP.x)/2; int y = (p.y+lastP.y)/2; path.moveTo(x-virtualNodeSize, y); path.lineTo(x+virtualNodeSize, y); path.moveTo(x, y-virtualNodeSize); path.lineTo(x, y+virtualNodeSize); } lastP = p; } } } private static boolean isLargeSegment(Point p1, Point p2, int space) { int xd = p1.x-p2.x; if(xd < 0) { xd = -xd; } int yd = p1.y-p2.y; if(yd < 0) { yd = -yd; } return (xd+yd > space); } /** * Draw a number of the order of the two consecutive nodes within the * parents way */ public void drawOrderNumber(Node n1, Node n2, int orderNumber) { Point p1 = nc.getPoint(n1); Point p2 = nc.getPoint(n2); drawOrderNumber(p1, p2, orderNumber); } /** * Draw an number of the order of the two consecutive nodes within the * parents way */ protected void drawOrderNumber(Point p1, Point p2, int orderNumber) { if (isSegmentVisible(p1, p2) && isLargeSegment(p1, p2, segmentNumberSpace)) { String on = Integer.toString(orderNumber); int strlen = on.length(); int x = (p1.x+p2.x)/2 - 4*strlen; int y = (p1.y+p2.y)/2 + 4; if(virtualNodeSize != 0 && isLargeSegment(p1, p2, virtualNodeSpace)) { y = (p1.y+p2.y)/2 - virtualNodeSize - 3; } Color c = g.getColor(); g.setColor(backgroundColor); g.fillRect(x-1, y-12, 8*strlen+1, 14); g.setColor(c); g.drawString(on, x, y); } } //TODO Not a good place for this method public String getNodeName(Node n) { String name = null; if (n.hasKeys()) { for (String rn : regionalNameOrder) { name = n.get(rn); if (name != null) { break; } } } return name; } //TODO Not a good place for this method public String getWayName(Way w) { String name = null; if (w.hasKeys()) { for (String rn : regionalNameOrder) { name = w.get(rn); if (name != null) { break; } } } return name; } public boolean isInactive() { return inactive; } public boolean isShowNames() { return showNames; } public double getCircum() { return circum; } public boolean isShowIcons() { return showIcons; } }