001    /* License: GPL. Copyright 2007 by Immanuel Scholz and others */
002    package org.openstreetmap.josm.data.osm.visitor.paint;
003    
004    import java.awt.BasicStroke;
005    import java.awt.Color;
006    import java.awt.Graphics2D;
007    import java.awt.Point;
008    import java.awt.Polygon;
009    import java.awt.Rectangle;
010    import java.awt.RenderingHints;
011    import java.awt.Stroke;
012    import java.awt.geom.GeneralPath;
013    import java.awt.geom.Point2D;
014    import java.util.Collection;
015    import java.util.Iterator;
016    
017    import org.openstreetmap.josm.Main;
018    import org.openstreetmap.josm.data.Bounds;
019    import org.openstreetmap.josm.data.osm.BBox;
020    import org.openstreetmap.josm.data.osm.Changeset;
021    import org.openstreetmap.josm.data.osm.DataSet;
022    import org.openstreetmap.josm.data.osm.Node;
023    import org.openstreetmap.josm.data.osm.OsmPrimitive;
024    import org.openstreetmap.josm.data.osm.Relation;
025    import org.openstreetmap.josm.data.osm.RelationMember;
026    import org.openstreetmap.josm.data.osm.Way;
027    import org.openstreetmap.josm.data.osm.WaySegment;
028    import org.openstreetmap.josm.data.osm.visitor.Visitor;
029    import org.openstreetmap.josm.gui.NavigatableComponent;
030    
031    /**
032     * A map renderer that paints a simple scheme of every primitive it visits to a
033     * previous set graphic environment.
034     */
035    public class WireframeMapRenderer extends AbstractMapRenderer implements Visitor {
036    
037        /** Color Preference for inactive objects */
038        protected Color inactiveColor;
039        /** Color Preference for selected objects */
040        protected Color selectedColor;
041        /** Color Preference for nodes */
042        protected Color nodeColor;
043        /** Color Preference for ways not matching any other group */
044        protected Color dfltWayColor;
045        /** Color Preference for relations */
046        protected Color relationColor;
047        /** Color Preference for untagged ways */
048        protected Color untaggedWayColor;
049        /** Color Preference for background */
050        protected Color backgroundColor;
051        /** Color Preference for hightlighted objects */
052        protected Color highlightColor;
053        /** Color Preference for tagged nodes */
054        protected Color taggedColor;
055        /** Color Preference for multiply connected nodes */
056        protected Color connectionColor;
057        /** Color Preference for tagged and multiply connected nodes */
058        protected Color taggedConnectionColor;
059        /** Preference: should directional arrows be displayed */
060        protected boolean showDirectionArrow;
061        /** Preference: should arrows for oneways be displayed */
062        protected boolean showOnewayArrow;
063        /** Preference: should only the last arrow of a way be displayed */
064        protected boolean showHeadArrowOnly;
065        /** Preference: should the segement numbers of ways be displayed */
066        protected boolean showOrderNumber;
067        /** Preference: should selected nodes be filled */
068        protected boolean fillSelectedNode;
069        /** Preference: should unselected nodes be filled */
070        protected boolean fillUnselectedNode;
071        /** Preference: should tagged nodes be filled */
072        protected boolean fillTaggedNode;
073        /** Preference: should multiply connected nodes be filled */
074        protected boolean fillConnectionNode;
075        /** Preference: size of selected nodes */
076        protected int selectedNodeSize;
077        /** Preference: size of unselected nodes */
078        protected int unselectedNodeSize;
079        /** Preference: size of multiply connected nodes */
080        protected int connectionNodeSize;
081        /** Preference: size of tagged nodes */
082        protected int taggedNodeSize;
083        /** Preference: size of virtual nodes (0 displayes display) */
084        protected int virtualNodeSize;
085        /** Preference: minimum space (displayed way length) to display virtual nodes */
086        protected int virtualNodeSpace;
087        /** Preference: minimum space (displayed way length) to display segment numbers */
088        protected int segmentNumberSpace;
089    
090        /** Color cache to draw subsequent segments of same color as one <code>Path</code>. */
091        protected Color currentColor = null;
092        /** Path store to draw subsequent segments of same color as one <code>Path</code>. */
093        protected GeneralPath currentPath = new GeneralPath();
094        /**
095          * <code>DataSet</code> passed to the @{link render} function to overcome the argument
096          * limitations of @{link Visitor} interface. Only valid until end of rendering call.
097          */
098        private DataSet ds;
099    
100        /** Helper variable for {@link #drawSgement} */
101        private static final double PHI = Math.toRadians(20);
102        /** Helper variable for {@link #drawSgement} */
103        private static final double cosPHI = Math.cos(PHI);
104        /** Helper variable for {@link #drawSgement} */
105        private static final double sinPHI = Math.sin(PHI);
106    
107        /** Helper variable for {@link #visit(Relation) */
108        private Stroke relatedWayStroke = new BasicStroke(
109                4, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL);
110    
111        /**
112         * Creates an wireframe render
113         * 
114         * @param g the graphics context. Must not be null.
115         * @param nc the map viewport. Must not be null.
116         * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
117         * look inactive. Example: rendering of data in an inactive layer using light gray as color only.
118         * @throws IllegalArgumentException thrown if {@code g} is null
119         * @throws IllegalArgumentException thrown if {@code nc} is null
120         */
121        public WireframeMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
122            super(g, nc, isInactiveMode);
123        }
124    
125        /**
126         * Reads the color definitions from preferences. This function is <code>public</code>, so that
127         * color names in preferences can be displayed even without calling the wireframe display before.
128         */
129        public void getColors()
130        {
131            inactiveColor = PaintColors.INACTIVE.get();
132            selectedColor = PaintColors.SELECTED.get();
133            nodeColor = PaintColors.NODE.get();
134            dfltWayColor = PaintColors.DEFAULT_WAY.get();
135            relationColor = PaintColors.RELATION.get();
136            untaggedWayColor = PaintColors.UNTAGGED_WAY.get();
137            backgroundColor = PaintColors.BACKGROUND.get();
138            highlightColor = PaintColors.HIGHLIGHT_WIREFRAME.get();
139            taggedColor = PaintColors.TAGGED.get();
140            connectionColor = PaintColors.CONNECTION.get();
141    
142            if (taggedColor != nodeColor) {
143                taggedConnectionColor = taggedColor;
144            } else {
145                taggedConnectionColor = connectionColor;
146            }
147        }
148    
149        /**
150         * Reads all the settings from preferences. Calls the @{link #getColors}
151         * function.
152         *
153         * @param virtual <code>true</code> if virtual nodes are used
154         */
155        protected void getSettings(boolean virtual) {
156            MapPaintSettings settings = MapPaintSettings.INSTANCE;
157            showDirectionArrow = settings.isShowDirectionArrow();
158            showOnewayArrow = settings.isShowOnewayArrow();
159            showHeadArrowOnly = settings.isShowHeadArrowOnly();
160            showOrderNumber = settings.isShowOrderNumber();
161            selectedNodeSize = settings.getSelectedNodeSize();
162            unselectedNodeSize = settings.getUnselectedNodeSize();
163            connectionNodeSize = settings.getConnectionNodeSize();
164            taggedNodeSize = settings.getTaggedNodeSize();
165            fillSelectedNode = settings.isFillSelectedNode();
166            fillUnselectedNode = settings.isFillUnselectedNode();
167            fillConnectionNode = settings.isFillConnectionNode();
168            fillTaggedNode = settings.isFillTaggedNode();
169            virtualNodeSize = virtual ? Main.pref.getInteger("mappaint.node.virtual-size", 8) / 2 : 0;
170            virtualNodeSpace = Main.pref.getInteger("mappaint.node.virtual-space", 70);
171            segmentNumberSpace = Main.pref.getInteger("mappaint.segmentnumber.space", 40);
172            getColors();
173    
174            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
175                    Main.pref.getBoolean("mappaint.wireframe.use-antialiasing", false) ?
176                            RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
177        }
178    
179        /**
180         * Renders the dataset for display.
181         *
182         * @param data <code>DataSet</code> to display
183         * @param virtual <code>true</code> if virtual nodes are used
184         * @param bounds display boundaries
185         */
186        public void render(DataSet data, boolean virtual, Bounds bounds) {
187            BBox bbox = new BBox(bounds);
188            this.ds = data;
189            getSettings(virtual);
190    
191            /* draw tagged ways first, then untagged ways. takes
192               time to iterate through list twice, OTOH does not
193               require changing the colour while painting... */
194            for (final OsmPrimitive osm: data.searchRelations(bbox)) {
195                if (osm.isDrawable() && !ds.isSelected(osm) && !osm.isDisabledAndHidden()) {
196                    osm.visit(this);
197                }
198            }
199    
200            for (final OsmPrimitive osm:data.searchWays(bbox)){
201                if (osm.isDrawable() && !ds.isSelected(osm) && !osm.isDisabledAndHidden() && osm.isTagged()) {
202                    osm.visit(this);
203                }
204            }
205            displaySegments();
206    
207            for (final OsmPrimitive osm:data.searchWays(bbox)){
208                if (osm.isDrawable() && !ds.isSelected(osm) && !osm.isDisabledAndHidden() && !osm.isTagged()) {
209                    osm.visit(this);
210                }
211            }
212            displaySegments();
213            for (final OsmPrimitive osm : data.getSelected()) {
214                if (osm.isDrawable()) {
215                    osm.visit(this);
216                }
217            }
218            displaySegments();
219    
220            for (final OsmPrimitive osm: data.searchNodes(bbox)) {
221                if (osm.isDrawable() && !ds.isSelected(osm) && !osm.isDisabledAndHidden())
222                {
223                    osm.visit(this);
224                }
225            }
226            drawVirtualNodes(data.searchWays(bbox), data.getHighlightedVirtualNodes());
227    
228            // draw highlighted way segments over the already drawn ways. Otherwise each
229            // way would have to be checked if it contains a way segment to highlight when
230            // in most of the cases there won't be more than one segment. Since the wireframe
231            // renderer does not feature any transparency there should be no visual difference.
232            for(final WaySegment wseg : data.getHighlightedWaySegments()) {
233                drawSegment(nc.getPoint(wseg.getFirstNode()), nc.getPoint(wseg.getSecondNode()), highlightColor, false);
234            }
235            displaySegments();
236        }
237    
238        /**
239         * Helper function to calculate maximum of 4 values.
240         *
241         * @param a First value
242         * @param b Second value
243         * @param c Third value
244         * @param d Fourth value
245         */
246        private static final int max(int a, int b, int c, int d) {
247            return Math.max(Math.max(a, b), Math.max(c, d));
248        }
249    
250        /**
251         * Draw a small rectangle.
252         * White if selected (as always) or red otherwise.
253         *
254         * @param n The node to draw.
255         */
256        @Override
257        public void visit(Node n) {
258            if (n.isIncomplete()) return;
259    
260            if (n.isHighlighted()) {
261                drawNode(n, highlightColor, selectedNodeSize, fillSelectedNode);
262            } else {
263                Color color;
264    
265                if (isInactiveMode || n.isDisabled()) {
266                    color = inactiveColor;
267                } else if (ds.isSelected(n)) {
268                    color = selectedColor;
269                } else if (n.isConnectionNode()) {
270                    if (n.isTagged()) {
271                        color = taggedConnectionColor;
272                    } else {
273                        color = connectionColor;
274                    }
275                } else {
276                    if (n.isTagged()) {
277                        color = taggedColor;
278                    } else {
279                        color = nodeColor;
280                    }
281                }
282    
283                final int size = max((ds.isSelected(n) ? selectedNodeSize : 0),
284                        (n.isTagged() ? taggedNodeSize : 0),
285                        (n.isConnectionNode() ? connectionNodeSize : 0),
286                        unselectedNodeSize);
287    
288                final boolean fill = (ds.isSelected(n) && fillSelectedNode) ||
289                (n.isTagged() && fillTaggedNode) ||
290                (n.isConnectionNode() && fillConnectionNode) ||
291                fillUnselectedNode;
292    
293                drawNode(n, color, size, fill);
294            }
295        }
296    
297        /**
298         * Checks if a way segemnt is large enough for additional information display.
299         *
300         * @param p1 First point of the way segment.
301         * @param p2 Second point of the way segment.
302         * @param space The free space to check against.
303         * @return <code>true</code> if segment is larger than required space
304         */
305        public static boolean isLargeSegment(Point2D p1, Point2D p2, int space)
306        {
307            double xd = Math.abs(p1.getX()-p2.getX());
308            double yd = Math.abs(p1.getY()-p2.getY());
309            return (xd+yd > space);
310        }
311    
312        /**
313         * Draws virtual nodes.
314         *
315         * @param ways The ways to draw nodes for.
316         * @param highlightVirtualNodes Way segements, where nodesshould be highlighted.
317         */
318        public void drawVirtualNodes(Collection<Way> ways, Collection<WaySegment> highlightVirtualNodes) {
319            if (virtualNodeSize == 0)
320                return;
321            // print normal virtual nodes
322            GeneralPath path = new GeneralPath();
323            for (Way osm : ways) {
324                if (osm.isUsable() && !osm.isDisabledAndHidden() && !osm.isDisabled()) {
325                    visitVirtual(path, osm);
326                }
327            }
328            g.setColor(nodeColor);
329            g.draw(path);
330            // print highlighted virtual nodes. Since only the color changes, simply
331            // drawing them over the existing ones works fine (at least in their current
332            // simple style)
333            path = new GeneralPath();
334            for (WaySegment wseg: highlightVirtualNodes){
335                if (wseg.way.isUsable() && !wseg.way.isDisabled()) {
336                    visitVirtual(path, wseg.toWay());
337                }
338            }
339            g.setColor(highlightColor);
340            g.draw(path);
341        }
342    
343        /**
344         * Creates path for drawing virtual nodes for one way.
345         *
346         * @param path The path to append drawing to.
347         * @param w The ways to draw node for.
348         */
349        public void visitVirtual(GeneralPath path, Way w) {
350            Iterator<Node> it = w.getNodes().iterator();
351            if (it.hasNext()) {
352                Point lastP = nc.getPoint(it.next());
353                while(it.hasNext())
354                {
355                    Point p = nc.getPoint(it.next());
356                    if(isSegmentVisible(lastP, p) && isLargeSegment(lastP, p, virtualNodeSpace))
357                    {
358                        int x = (p.x+lastP.x)/2;
359                        int y = (p.y+lastP.y)/2;
360                        path.moveTo(x-virtualNodeSize, y);
361                        path.lineTo(x+virtualNodeSize, y);
362                        path.moveTo(x, y-virtualNodeSize);
363                        path.lineTo(x, y+virtualNodeSize);
364                    }
365                    lastP = p;
366                }
367            }
368        }
369    
370        /**
371         * Draw a line for all way segments.
372         * @param w The way to draw.
373         */
374        @Override
375        public void visit(Way w) {
376            if (w.isIncomplete() || w.getNodesCount() < 2)
377                return;
378    
379            /* show direction arrows, if draw.segment.relevant_directions_only is not set, the way is tagged with a direction key
380               (even if the tag is negated as in oneway=false) or the way is selected */
381    
382            boolean showThisDirectionArrow = ds.isSelected(w) || showDirectionArrow;
383            /* head only takes over control if the option is true,
384               the direction should be shown at all and not only because it's selected */
385            boolean showOnlyHeadArrowOnly = showThisDirectionArrow && !ds.isSelected(w) && showHeadArrowOnly;
386            Color wayColor;
387    
388            if (isInactiveMode || w.isDisabled()) {
389                wayColor = inactiveColor;
390            } else if(w.isHighlighted()) {
391                wayColor = highlightColor;
392            } else if(ds.isSelected(w)) {
393                wayColor = selectedColor;
394            } else if (!w.isTagged()) {
395                wayColor = untaggedWayColor;
396            } else {
397                wayColor = dfltWayColor;
398            }
399    
400            Iterator<Node> it = w.getNodes().iterator();
401            if (it.hasNext()) {
402                Point lastP = nc.getPoint(it.next());
403                for (int orderNumber = 1; it.hasNext(); orderNumber++) {
404                    Point p = nc.getPoint(it.next());
405                    drawSegment(lastP, p, wayColor,
406                            showOnlyHeadArrowOnly ? !it.hasNext() : showThisDirectionArrow);
407                    if (showOrderNumber && !isInactiveMode) {
408                        drawOrderNumber(lastP, p, orderNumber);
409                    }
410                    lastP = p;
411                }
412            }
413        }
414    
415        /**
416         * Draw objects used in relations.
417         * @param r The relation to draw.
418         */
419        @Override
420        public void visit(Relation r) {
421            if (r.isIncomplete()) return;
422    
423            Color col;
424            if (isInactiveMode || r.isDisabled()) {
425                col = inactiveColor;
426            } else if (ds.isSelected(r)) {
427                col = selectedColor;
428            } else {
429                col = relationColor;
430            }
431            g.setColor(col);
432    
433            for (RelationMember m : r.getMembers()) {
434                if (m.getMember().isIncomplete() || !m.getMember().isDrawable()) {
435                    continue;
436                }
437    
438                if (m.isNode()) {
439                    Point p = nc.getPoint(m.getNode());
440                    if (p.x < 0 || p.y < 0
441                            || p.x > nc.getWidth() || p.y > nc.getHeight()) {
442                        continue;
443                    }
444    
445                    g.drawOval(p.x-3, p.y-3, 6, 6);
446                } else if (m.isWay()) {
447                    GeneralPath path = new GeneralPath();
448    
449                    boolean first = true;
450                    for (Node n : m.getWay().getNodes()) {
451                        if (!n.isDrawable()) {
452                            continue;
453                        }
454                        Point p = nc.getPoint(n);
455                        if (first) {
456                            path.moveTo(p.x, p.y);
457                            first = false;
458                        } else {
459                            path.lineTo(p.x, p.y);
460                        }
461                    }
462    
463                    g.draw(relatedWayStroke.createStrokedShape(path));
464                }
465            }
466        }
467    
468        /**
469         * Visitor for changesets not used in this class
470         * @param cs The changeset for inspection.
471         */
472        @Override
473        public void visit(Changeset cs) {/* ignore */}
474    
475        /**
476         * Draw an number of the order of the two consecutive nodes within the
477         * parents way
478         *
479         * @param p1 First point of the way segment.
480         * @param p2 Second point of the way segment.
481         * @param orderNumber The number of the segment in the way.
482         */
483        protected void drawOrderNumber(Point p1, Point p2, int orderNumber) {
484            if (isSegmentVisible(p1, p2) && isLargeSegment(p1, p2, segmentNumberSpace)) {
485                String on = Integer.toString(orderNumber);
486                int strlen = on.length();
487                int x = (p1.x+p2.x)/2 - 4*strlen;
488                int y = (p1.y+p2.y)/2 + 4;
489    
490                if(virtualNodeSize != 0 && isLargeSegment(p1, p2, virtualNodeSpace))
491                {
492                    y = (p1.y+p2.y)/2 - virtualNodeSize - 3;
493                }
494    
495                displaySegments(); /* draw nodes on top! */
496                Color c = g.getColor();
497                g.setColor(backgroundColor);
498                g.fillRect(x-1, y-12, 8*strlen+1, 14);
499                g.setColor(c);
500                g.drawString(on, x, y);
501            }
502        }
503    
504        /**
505         * Draw the node as small rectangle with the given color.
506         *
507         * @param n     The node to draw.
508         * @param color The color of the node.
509         */
510        public void drawNode(Node n, Color color, int size, boolean fill) {
511            if (size > 1) {
512                int radius = size / 2;
513                Point p = nc.getPoint(n);
514                if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth())
515                        || (p.y > nc.getHeight()))
516                    return;
517                g.setColor(color);
518                if (fill) {
519                    g.fillRect(p.x - radius, p.y - radius, size, size);
520                    g.drawRect(p.x - radius, p.y - radius, size, size);
521                } else {
522                    g.drawRect(p.x - radius, p.y - radius, size, size);
523                }
524            }
525        }
526    
527        /**
528         * Draw a line with the given color.
529         *
530         * @param path The path to append this segment.
531         * @param p1 First point of the way segment.
532         * @param p2 Second point of the way segment.
533         * @param showDirection <code>true</code> if segment direction should be indicated
534         */
535        protected void drawSegment(GeneralPath path, Point p1, Point p2, boolean showDirection) {
536            Rectangle bounds = g.getClipBounds();
537            bounds.grow(100, 100);                  // avoid arrow heads at the border
538            LineClip clip = new LineClip(p1, p2, bounds);
539            if (clip.execute()) {
540                p1 = clip.getP1();
541                p2 = clip.getP2();
542                path.moveTo(p1.x, p1.y);
543                path.lineTo(p2.x, p2.y);
544    
545                if (showDirection) {
546                    final double l =  10. / p1.distance(p2);
547    
548                    final double sx = l * (p1.x - p2.x);
549                    final double sy = l * (p1.y - p2.y);
550    
551                    path.lineTo (p2.x + (int) Math.round(cosPHI * sx - sinPHI * sy), p2.y + (int) Math.round(sinPHI * sx + cosPHI * sy));
552                    path.moveTo (p2.x + (int) Math.round(cosPHI * sx + sinPHI * sy), p2.y + (int) Math.round(- sinPHI * sx + cosPHI * sy));
553                    path.lineTo(p2.x, p2.y);
554                }
555            }
556        }
557    
558        /**
559         * Draw a line with the given color.
560         *
561         * @param p1 First point of the way segment.
562         * @param p2 Second point of the way segment.
563         * @param col The color to use for drawing line.
564         * @param showDirection <code>true</code> if segment direction should be indicated.
565         */
566        protected void drawSegment(Point p1, Point p2, Color col, boolean showDirection) {
567            if (col != currentColor) {
568                displaySegments(col);
569            }
570            drawSegment(currentPath, p1, p2, showDirection);
571        }
572    
573        /**
574         * Checks if segment is visible in display.
575         *
576         * @param p1 First point of the way segment.
577         * @param p2 Second point of the way segment.
578         * @return <code>true</code> if segment is visible.
579         */
580        protected boolean isSegmentVisible(Point p1, Point p2) {
581            if ((p1.x < 0) && (p2.x < 0)) return false;
582            if ((p1.y < 0) && (p2.y < 0)) return false;
583            if ((p1.x > nc.getWidth()) && (p2.x > nc.getWidth())) return false;
584            if ((p1.y > nc.getHeight()) && (p2.y > nc.getHeight())) return false;
585            return true;
586        }
587    
588        /**
589         * Checks if a polygon is visible in display.
590         *
591         * @param polygon The polygon to check.
592         * @return <code>true</code> if polygon is visible.
593         */
594        protected boolean isPolygonVisible(Polygon polygon) {
595            Rectangle bounds = polygon.getBounds();
596            if (bounds.width == 0 && bounds.height == 0) return false;
597            if (bounds.x > nc.getWidth()) return false;
598            if (bounds.y > nc.getHeight()) return false;
599            if (bounds.x + bounds.width < 0) return false;
600            if (bounds.y + bounds.height < 0) return false;
601            return true;
602        }
603    
604        /**
605         * Finally display all segments in currect path.
606         */
607        protected void displaySegments() {
608            displaySegments(null);
609        }
610    
611        /**
612         * Finally display all segments in currect path.
613         *
614         * @param newColor This color is set after the path is drawn.
615         */
616        protected void displaySegments(Color newColor) {
617            if (currentPath != null) {
618                g.setColor(currentColor);
619                g.draw(currentPath);
620                currentPath = new GeneralPath();
621                currentColor = newColor;
622            }
623        }
624    }