001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.data.osm.visitor.paint;
003    
004    import java.awt.AlphaComposite;
005    import java.awt.BasicStroke;
006    import java.awt.Color;
007    import java.awt.Font;
008    import java.awt.FontMetrics;
009    import java.awt.Graphics2D;
010    import java.awt.Image;
011    import java.awt.Point;
012    import java.awt.Polygon;
013    import java.awt.Rectangle;
014    import java.awt.Shape;
015    import java.awt.TexturePaint;
016    import java.awt.font.FontRenderContext;
017    import java.awt.font.GlyphVector;
018    import java.awt.font.LineMetrics;
019    import java.awt.geom.AffineTransform;
020    import java.awt.geom.GeneralPath;
021    import java.awt.geom.Path2D;
022    import java.awt.geom.Point2D;
023    import java.awt.geom.Rectangle2D;
024    import java.util.Arrays;
025    import java.util.Collection;
026    import java.util.Iterator;
027    import java.util.List;
028    
029    import javax.swing.ImageIcon;
030    
031    import org.openstreetmap.josm.Main;
032    import org.openstreetmap.josm.data.coor.EastNorth;
033    import org.openstreetmap.josm.data.osm.Node;
034    import org.openstreetmap.josm.data.osm.OsmPrimitive;
035    import org.openstreetmap.josm.data.osm.OsmUtils;
036    import org.openstreetmap.josm.data.osm.Relation;
037    import org.openstreetmap.josm.data.osm.RelationMember;
038    import org.openstreetmap.josm.data.osm.Way;
039    import org.openstreetmap.josm.data.osm.WaySegment;
040    import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
041    import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
042    import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
043    import org.openstreetmap.josm.gui.NavigatableComponent;
044    import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle;
045    import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.HorizontalTextAlignment;
046    import org.openstreetmap.josm.gui.mappaint.BoxTextElemStyle.VerticalTextAlignment;
047    import org.openstreetmap.josm.gui.mappaint.MapImage;
048    import org.openstreetmap.josm.gui.mappaint.NodeElemStyle.Symbol;
049    import org.openstreetmap.josm.gui.mappaint.TextElement;
050    import org.openstreetmap.josm.tools.ImageProvider;
051    import org.openstreetmap.josm.tools.Pair;
052    import org.openstreetmap.josm.tools.Utils;
053    
054    public class MapPainter {
055    
056        private final Graphics2D g;
057        private final NavigatableComponent nc;
058        private final boolean inactive;
059        private final MapPaintSettings settings;
060        private final Collection<WaySegment> highlightWaySegments;
061    
062        private final boolean useStrokes;
063        private final boolean showNames;
064        private final boolean showIcons;
065    
066        private final boolean  isOutlineOnly;
067    
068        private final Color inactiveColor;
069        private final Color selectedColor;
070        private final Color relationSelectedColor;
071        private final Color nodeColor;
072        private final Color highlightColor;
073        private final Color highlightColorTransparent;
074        private final Color backgroundColor;
075    
076        private final Font orderFont;
077        private final int virtualNodeSize;
078        private final int virtualNodeSpace;
079        private final int segmentNumberSpace;
080    
081        private final double circum;
082    
083        private final boolean leftHandTraffic;
084    
085        private static final double PHI = Math.toRadians(20);
086        private static final double cosPHI = Math.cos(PHI);
087        private static final double sinPHI = Math.sin(PHI);
088    
089        public MapPainter(MapPaintSettings settings, Graphics2D g,
090                boolean inactive, NavigatableComponent nc, boolean virtual,
091                double circum, boolean leftHandTraffic,
092                Collection<WaySegment> highlightWaySegments){
093            this.settings = settings;
094            this.g = g;
095            this.inactive = inactive;
096            this.nc = nc;
097            this.highlightWaySegments = highlightWaySegments;
098            this.useStrokes = settings.getUseStrokesDistance() > circum;
099            this.showNames = settings.getShowNamesDistance() > circum;
100            this.showIcons = settings.getShowIconsDistance() > circum;
101    
102            this.isOutlineOnly = settings.isOutlineOnly();
103    
104            this.inactiveColor = PaintColors.INACTIVE.get();
105            this.selectedColor = PaintColors.SELECTED.get();
106            this.relationSelectedColor = PaintColors.RELATIONSELECTED.get();
107            this.nodeColor = PaintColors.NODE.get();
108            this.highlightColor = PaintColors.HIGHLIGHT.get();
109            this.highlightColorTransparent = new Color(highlightColor.getRed(), highlightColor.getGreen(), highlightColor.getBlue(), 100);
110            this.backgroundColor = PaintColors.getBackgroundColor();
111    
112            this.orderFont = new Font(Main.pref.get("mappaint.font", "Helvetica"), Font.PLAIN, Main.pref.getInteger("mappaint.fontsize", 8));
113            this.virtualNodeSize = virtual ? Main.pref.getInteger("mappaint.node.virtual-size", 8) / 2 : 0;
114            this.virtualNodeSpace = Main.pref.getInteger("mappaint.node.virtual-space", 70);
115            this.segmentNumberSpace = Main.pref.getInteger("mappaint.segmentnumber.space", 40);
116    
117            this.circum = circum;
118            this.leftHandTraffic = leftHandTraffic;
119        }
120    
121        /**
122         * draw way
123         * @param showOrientation show arrows that indicate the technical orientation of
124         *              the way (defined by order of nodes)
125         * @param showOneway show symbols that indicate the direction of the feature,
126         *              e.g. oneway street or waterway
127         * @param onewayReversed for oneway=-1 and similar
128         */
129        public void drawWay(Way way, Color color, BasicStroke line, BasicStroke dashes, Color dashedColor, float offset,
130                boolean showOrientation, boolean showHeadArrowOnly,
131                boolean showOneway, boolean onewayReversed) {
132    
133            GeneralPath path = new GeneralPath();
134            GeneralPath orientationArrows = showOrientation ? new GeneralPath() : null;
135            GeneralPath onewayArrows = showOneway ? new GeneralPath() : null;
136            GeneralPath onewayArrowsCasing = showOneway ? new GeneralPath() : null;
137            Rectangle bounds = g.getClipBounds();
138            bounds.grow(100, 100);                  // avoid arrow heads at the border
139    
140            double wayLength = 0;
141            Point lastPoint = null;
142            boolean initialMoveToNeeded = true;
143            List<Node> wayNodes = way.getNodes();
144            if (wayNodes.size() < 2) return;
145    
146            // only highlight the segment if the way itself is not highlighted
147            if(!way.isHighlighted()) {
148                GeneralPath highlightSegs = null;
149                for(WaySegment ws : highlightWaySegments) {
150                    if(ws.way != way || ws.lowerIndex < offset) {
151                        continue;
152                    }
153                    if(highlightSegs == null) {
154                        highlightSegs = new GeneralPath();
155                    }
156    
157                    Point p1 = nc.getPoint(ws.getFirstNode());
158                    Point p2 = nc.getPoint(ws.getSecondNode());
159                    highlightSegs.moveTo(p1.x, p1.y);
160                    highlightSegs.lineTo(p2.x, p2.y);
161                }
162    
163                drawPathHighlight(highlightSegs, line);
164            }
165    
166    
167            Iterator<Point> it = new OffsetIterator(wayNodes, offset);
168            while (it.hasNext()) {
169                Point p = it.next();
170                if (lastPoint != null) {
171                    Point p1 = lastPoint;
172                    Point p2 = p;
173    
174                    /**
175                     * Do custom clipping to work around openjdk bug. It leads to
176                     * drawing artefacts when zooming in a lot. (#4289, #4424)
177                     * (Looks like int overflow.)
178                     */
179                    LineClip clip = new LineClip(p1, p2, bounds);
180                    if (clip.execute()) {
181                        if (!p1.equals(clip.getP1())) {
182                            p1 = clip.getP1();
183                            path.moveTo(p1.x, p1.y);
184                        } else if (initialMoveToNeeded) {
185                            initialMoveToNeeded = false;
186                            path.moveTo(p1.x, p1.y);
187                        }
188                        p2 = clip.getP2();
189                        path.lineTo(p2.x, p2.y);
190    
191                        /* draw arrow */
192                        if (showHeadArrowOnly ? !it.hasNext() : showOrientation) {
193                            final double segmentLength = p1.distance(p2);
194                            if (segmentLength != 0.0) {
195                                final double l =  (10. + line.getLineWidth()) / segmentLength;
196    
197                                final double sx = l * (p1.x - p2.x);
198                                final double sy = l * (p1.y - p2.y);
199    
200                                orientationArrows.moveTo (p2.x + cosPHI * sx - sinPHI * sy, p2.y + sinPHI * sx + cosPHI * sy);
201                                orientationArrows.lineTo(p2.x, p2.y);
202                                orientationArrows.lineTo (p2.x + cosPHI * sx + sinPHI * sy, p2.y - sinPHI * sx + cosPHI * sy);
203                            }
204                        }
205                        if (showOneway) {
206                            final double segmentLength = p1.distance(p2);
207                            if (segmentLength != 0.0) {
208                                final double nx = (p2.x - p1.x) / segmentLength;
209                                final double ny = (p2.y - p1.y) / segmentLength;
210    
211                                final double interval = 60;
212                                // distance from p1
213                                double dist = interval - (wayLength % interval);
214    
215                                while (dist < segmentLength) {
216                                    for (Pair<Float, GeneralPath> sizeAndPath : Arrays.asList(new Pair[] {
217                                            new Pair<Float, GeneralPath>(3f, onewayArrowsCasing),
218                                            new Pair<Float, GeneralPath>(2f, onewayArrows)})) {
219    
220                                        // scale such that border is 1 px
221                                        final double fac = - (onewayReversed ? -1 : 1) * sizeAndPath.a * (1 + sinPHI) / (sinPHI * cosPHI);
222                                        final double sx = nx * fac;
223                                        final double sy = ny * fac;
224    
225                                        // Attach the triangle at the incenter and not at the tip.
226                                        // Makes the border even at all sides.
227                                        final double x = p1.x + nx * (dist + (onewayReversed ? -1 : 1) * (sizeAndPath.a / sinPHI));
228                                        final double y = p1.y + ny * (dist + (onewayReversed ? -1 : 1) * (sizeAndPath.a / sinPHI));
229    
230                                        sizeAndPath.b.moveTo(x, y);
231                                        sizeAndPath.b.lineTo (x + cosPHI * sx - sinPHI * sy, y + sinPHI * sx + cosPHI * sy);
232                                        sizeAndPath.b.lineTo (x + cosPHI * sx + sinPHI * sy, y - sinPHI * sx + cosPHI * sy);
233                                        sizeAndPath.b.lineTo(x, y);
234                                    }
235                                    dist += interval;
236                                }
237                            }
238                            wayLength += segmentLength;
239                        }
240                    }
241                }
242                lastPoint = p;
243            }
244            if(way.isHighlighted()) {
245                drawPathHighlight(path, line);
246            }
247            displaySegments(path, orientationArrows, onewayArrows, onewayArrowsCasing, color, line, dashes, dashedColor);
248        }
249    
250        /**
251         * Iterates over a list of Way Nodes and returns screen coordinates that
252         * represent a line that is shifted by a certain offset perpendicular
253         * to the way direction.
254         *
255         * There is no intention, to handle consecutive duplicate Nodes in a
256         * perfect way, but it is should not throw an exception.
257         */
258        public class OffsetIterator implements Iterator<Point> {
259    
260            private List<Node> nodes;
261            private float offset;
262            private int idx;
263    
264            private Point prev = null;
265            /* 'prev0' is a point that has distance 'offset' from 'prev' and the
266             * line from 'prev' to 'prev0' is perpendicular to the way segment from
267             * 'prev' to the next point.
268             */
269            private int x_prev0, y_prev0;
270    
271            public OffsetIterator(List<Node> nodes, float offset) {
272                this.nodes = nodes;
273                this.offset = offset;
274                idx = 0;
275            }
276    
277            @Override
278            public boolean hasNext() {
279                return idx < nodes.size();
280            }
281    
282            @Override
283            public Point next() {
284                if (Math.abs(offset) < 0.1f) return nc.getPoint(nodes.get(idx++));
285    
286                Point current = nc.getPoint(nodes.get(idx));
287    
288                if (idx == nodes.size() - 1) {
289                    ++idx;
290                    return new Point(x_prev0 + current.x - prev.x, y_prev0 + current.y - prev.y);
291                }
292    
293                Point next = nc.getPoint(nodes.get(idx+1));
294    
295                int dx_next = next.x - current.x;
296                int dy_next = next.y - current.y;
297                double len_next = Math.sqrt(dx_next*dx_next + dy_next*dy_next);
298    
299                if (len_next == 0) {
300                    len_next = 1; // value does not matter, because dy_next and dx_next is 0
301                }
302    
303                int x_current0 = current.x + (int) Math.round(offset * dy_next / len_next);
304                int y_current0 = current.y - (int) Math.round(offset * dx_next / len_next);
305    
306                if (idx==0) {
307                    ++idx;
308                    prev = current;
309                    x_prev0 = x_current0;
310                    y_prev0 = y_current0;
311                    return new Point(x_current0, y_current0);
312                } else {
313                    int dx_prev = current.x - prev.x;
314                    int dy_prev = current.y - prev.y;
315    
316                    // determine intersection of the lines parallel to the two
317                    // segments
318                    int det = dx_next*dy_prev - dx_prev*dy_next;
319    
320                    if (det == 0) {
321                        ++idx;
322                        prev = current;
323                        x_prev0 = x_current0;
324                        y_prev0 = y_current0;
325                        return new Point(x_current0, y_current0);
326                    }
327    
328                    int m = dx_next*(y_current0 - y_prev0) - dy_next*(x_current0 - x_prev0);
329    
330                    int cx_ = x_prev0 + Math.round((float)m * dx_prev / det);
331                    int cy_ = y_prev0 + Math.round((float)m * dy_prev / det);
332                    ++idx;
333                    prev = current;
334                    x_prev0 = x_current0;
335                    y_prev0 = y_current0;
336                    return new Point(cx_, cy_);
337                }
338            }
339    
340            @Override
341            public void remove() {
342                throw new UnsupportedOperationException();
343            }
344        }
345    
346        private void displaySegments(GeneralPath path, GeneralPath orientationArrows, GeneralPath onewayArrows, GeneralPath onewayArrowsCasing,
347                Color color, BasicStroke line, BasicStroke dashes, Color dashedColor) {
348            g.setColor(inactive ? inactiveColor : color);
349            if (useStrokes) {
350                g.setStroke(line);
351            }
352            g.draw(path);
353    
354            if(!inactive && useStrokes && dashes != null) {
355                g.setColor(dashedColor);
356                g.setStroke(dashes);
357                g.draw(path);
358            }
359    
360            if (orientationArrows != null) {
361                g.setColor(inactive ? inactiveColor : color);
362                g.setStroke(new BasicStroke(line.getLineWidth(), line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
363                g.draw(orientationArrows);
364            }
365    
366            if (onewayArrows != null) {
367                g.setStroke(new BasicStroke(1, line.getEndCap(), BasicStroke.JOIN_MITER, line.getMiterLimit()));
368                g.fill(onewayArrowsCasing);
369                g.setColor(inactive ? inactiveColor : backgroundColor);
370                g.fill(onewayArrows);
371            }
372    
373            if(useStrokes) {
374                g.setStroke(new BasicStroke());
375            }
376        }
377    
378        /**
379         * highlights a given GeneralPath using the settings from BasicStroke to match the line's
380         * style. Width of the highlight is hard coded.
381         * @param path
382         * @param line
383         */
384        private void drawPathHighlight(GeneralPath path, BasicStroke line) {
385            if(path == null)
386                return;
387            g.setColor(highlightColorTransparent);
388            float w = (line.getLineWidth() + 4);
389            while(w >= line.getLineWidth()) {
390                g.setStroke(new BasicStroke(w, line.getEndCap(), line.getLineJoin(), line.getMiterLimit()));
391                g.draw(path);
392                w -= 4;
393            }
394        }
395    
396        private boolean isSegmentVisible(Point p1, Point p2) {
397            if ((p1.x < 0) && (p2.x < 0)) return false;
398            if ((p1.y < 0) && (p2.y < 0)) return false;
399            if ((p1.x > nc.getWidth()) && (p2.x > nc.getWidth())) return false;
400            if ((p1.y > nc.getHeight()) && (p2.y > nc.getHeight())) return false;
401            return true;
402        }
403    
404        private static Boolean IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG = null;
405    
406        /**
407         * Check, if this System has the GlyphVector double translation bug.
408         *
409         * With this bug, <code>gv.setGlyphTransform(i, trfm)</code> has a different
410         * effect than on most other systems, namely the translation components
411         * ("m02" & "m12", {@link AffineTransform}) appear to be twice as large, as
412         * they actually are. The rotation is unaffected (scale & shear not tested
413         * so far).
414         *
415         * This bug has only been observed on Mac OS X, see #7841.
416         *
417         * @return true, if the GlyphVector double translation bug is present on
418         * this System
419         */
420        public static boolean isGlyphVectorDoubleTranslationBug() {
421            if (IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG != null)
422                return IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG;
423            FontRenderContext frc = new FontRenderContext(null, false, false);
424            Font font = new Font("Dialog", Font.PLAIN, 12);
425            GlyphVector gv = font.createGlyphVector(frc, "x");
426            gv.setGlyphTransform(0, AffineTransform.getTranslateInstance(1000, 1000));
427            Shape shape = gv.getGlyphOutline(0);
428            // x is about 1000 on normal stystems and about 2000 when the bug occurs
429            int x = shape.getBounds().x;
430            IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG = x > 1500;
431            return IS_GLYPH_VECTOR_DOUBLE_TRANSLATION_BUG;
432        }
433    
434        public void drawTextOnPath(Way way, TextElement text) {
435            if (way == null || text == null)
436                return;
437            String name = text.getString(way);
438            if (name == null || name.isEmpty())
439                return;
440    
441            Polygon poly = new Polygon();
442            Point lastPoint = null;
443            Iterator<Node> it = way.getNodes().iterator();
444            double pathLength = 0;
445            long dx, dy;
446            while (it.hasNext()) {
447                Node n = it.next();
448                Point p = nc.getPoint(n);
449                poly.addPoint(p.x, p.y);
450    
451                if(lastPoint != null) {
452                    dx = p.x - lastPoint.x;
453                    dy = p.y - lastPoint.y;
454                    pathLength += Math.sqrt(dx*dx + dy*dy);
455                }
456                lastPoint = p;
457            }
458    
459            FontMetrics fontMetrics = g.getFontMetrics(text.font); // if slow, use cache
460            Rectangle2D rec = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
461    
462            if (rec.getWidth() > pathLength)
463                return;
464    
465            double t1 = (pathLength/2 - rec.getWidth()/2) / pathLength;
466            double t2 = (pathLength/2 + rec.getWidth()/2) / pathLength;
467    
468            double[] p1 = pointAt(t1, poly, pathLength);
469            double[] p2 = pointAt(t2, poly, pathLength);
470            
471            if (p1 == null || p2 == null)
472                return;
473    
474            double angleOffset;
475            double offsetSign;
476            double tStart;
477    
478            if (p1[0] < p2[0] &&
479                    p1[2] < Math.PI/2 &&
480                    p1[2] > -Math.PI/2) {
481                angleOffset = 0;
482                offsetSign = 1;
483                tStart = t1;
484            } else {
485                angleOffset = Math.PI;
486                offsetSign = -1;
487                tStart = t2;
488            }
489    
490            FontRenderContext frc = g.getFontRenderContext();
491            GlyphVector gv = text.font.createGlyphVector(frc, name);
492    
493            for (int i=0; i<gv.getNumGlyphs(); ++i) {
494                Rectangle2D rect = gv.getGlyphLogicalBounds(i).getBounds2D();
495                double t = tStart + offsetSign * (rect.getX() + rect.getWidth()/2) / pathLength;
496                double[] p = pointAt(t, poly, pathLength);
497                if (p != null) {
498                    AffineTransform trfm = AffineTransform.getTranslateInstance(p[0] - rect.getX(), p[1]);
499                    trfm.rotate(p[2]+angleOffset);
500                    double off = -rect.getY() - rect.getHeight()/2 + text.yOffset;
501                    trfm.translate(-rect.getWidth()/2, off);
502                    if (isGlyphVectorDoubleTranslationBug()) {
503                        // scale the translation components by one half
504                        AffineTransform tmp = AffineTransform.getTranslateInstance(-0.5 * trfm.getTranslateX(), -0.5 * trfm.getTranslateY());
505                        tmp.concatenate(trfm);
506                        trfm = tmp;
507                    }
508                    gv.setGlyphTransform(i, trfm);
509                }
510            }
511            if (text.haloRadius != null) {
512                Shape textOutline = gv.getOutline();
513                g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
514                g.setColor(text.haloColor);
515                g.draw(textOutline);
516                g.setStroke(new BasicStroke());
517                g.setColor(text.color);
518                g.fill(textOutline);
519            } else {
520                g.setColor(text.color);
521                g.drawGlyphVector(gv, 0, 0);
522            }
523        }
524    
525        private double[] pointAt(double t, Polygon poly, double pathLength) {
526            double totalLen = t * pathLength;
527            double curLen = 0;
528            long dx, dy;
529            double segLen;
530    
531            // Yes, it is inefficient to iterate from the beginning for each glyph.
532            // Can be optimized if it turns out to be slow.
533            for (int i = 1; i < poly.npoints; ++i) {
534                dx = poly.xpoints[i] - poly.xpoints[i-1];
535                dy = poly.ypoints[i] - poly.ypoints[i-1];
536                segLen = Math.sqrt(dx*dx + dy*dy);
537                if (totalLen > curLen + segLen) {
538                    curLen += segLen;
539                    continue;
540                }
541                return new double[] {
542                        poly.xpoints[i-1]+(totalLen - curLen)/segLen*dx,
543                        poly.ypoints[i-1]+(totalLen - curLen)/segLen*dy,
544                        Math.atan2(dy, dx)};
545            }
546            return null;
547        }
548    
549        public void drawLinePattern(Way way, Image pattern) {
550            final int width = pattern.getWidth(null);
551            final int height = pattern.getHeight(null);
552    
553            Point lastP = null;
554            double wayLength = 0;
555    
556            Iterator<Node> it = way.getNodes().iterator();
557            while (it.hasNext()) {
558                Node n = it.next();
559                Point thisP = nc.getPoint(n);
560    
561                if (lastP != null) {
562                    final double segmentLength = thisP.distance(lastP);
563    
564                    final double dx = thisP.x - lastP.x;
565                    final double dy = thisP.y - lastP.y;
566    
567                    double dist = wayLength == 0 ? 0 : width - (wayLength % width);
568    
569                    AffineTransform saveTransform = g.getTransform();
570                    g.translate(lastP.x, lastP.y);
571                    g.rotate(Math.atan2(dy, dx));
572    
573                    if (dist > 0) {
574                        g.drawImage(pattern, 0, 0, (int) dist, height,
575                                width - (int) dist, 0, width, height, null);
576                    }
577                    while (dist < segmentLength) {
578                        if (dist + width > segmentLength) {
579                            g.drawImage(pattern, (int) dist, 0, (int) segmentLength, height,
580                                    0, 0, (int) segmentLength - (int) dist, height, null);
581                        } else {
582                            g.drawImage(pattern, (int) dist, 0, nc);
583                        }
584                        dist += width;
585                    }
586                    g.setTransform(saveTransform);
587    
588                    wayLength += segmentLength;
589                }
590                lastP = thisP;
591            }
592        }
593    
594        public void drawNodeIcon(Node n, Image img, float alpha, boolean selected, boolean member) {
595            Point p = nc.getPoint(n);
596    
597            final int w = img.getWidth(null), h=img.getHeight(null);
598            if(n.isHighlighted()) {
599                drawPointHighlight(p, Math.max(w, h));
600            }
601    
602            if (alpha != 1f) {
603                g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
604            }
605            g.drawImage(img, p.x-w/2, p.y-h/2, nc);
606            g.setPaintMode();
607            if (selected || member)
608            {
609                Color color = null;
610                if (inactive || n.isDisabled()) {
611                    color = inactiveColor;
612                } else if (selected) {
613                    color = selectedColor;
614                } else {
615                    color = relationSelectedColor;
616                }
617                g.setColor(color);
618                g.drawRect(p.x-w/2-2, p.y-h/2-2, w+4, h+4);
619            }
620        }
621    
622        private Polygon buildPolygon(Point center, int radius, int sides, double rotation) {
623            Polygon polygon = new Polygon();
624            for (int i = 0; i < sides; i++) {
625                double angle = ((2 * Math.PI / sides) * i) - rotation;
626                int x = (int) Math.round(center.x + radius * Math.cos(angle));
627                int y = (int) Math.round(center.y + radius * Math.sin(angle));
628                polygon.addPoint(x, y);
629            }
630            return polygon;
631        }
632    
633        private Polygon buildPolygon(Point center, int radius, int sides) {
634            return buildPolygon(center, radius, sides, 0.0);
635        }
636    
637        public void drawNodeSymbol(Node n, Symbol s, Color fillColor, Color strokeColor) {
638            Point p = nc.getPoint(n);
639            int radius = s.size / 2;
640    
641            if(n.isHighlighted()) {
642                drawPointHighlight(p, s.size);
643            }
644    
645            if (fillColor != null) {
646                g.setColor(fillColor);
647                switch (s.symbol) {
648                case SQUARE:
649                    g.fillRect(p.x - radius, p.y - radius, s.size, s.size);
650                    break;
651                case CIRCLE:
652                    g.fillOval(p.x - radius, p.y - radius, s.size, s.size);
653                    break;
654                case TRIANGLE:
655                    g.fillPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
656                    break;
657                case PENTAGON:
658                    g.fillPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
659                    break;
660                case HEXAGON:
661                    g.fillPolygon(buildPolygon(p, radius, 6));
662                    break;
663                case HEPTAGON:
664                    g.fillPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
665                    break;
666                case OCTAGON:
667                    g.fillPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
668                    break;
669                case NONAGON:
670                    g.fillPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
671                    break;
672                case DECAGON:
673                    g.fillPolygon(buildPolygon(p, radius, 10));
674                    break;
675                default:
676                    throw new AssertionError();
677                }
678            }
679            if (s.stroke != null) {
680                g.setStroke(s.stroke);
681                g.setColor(strokeColor);
682                switch (s.symbol) {
683                case SQUARE:
684                    g.drawRect(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
685                    break;
686                case CIRCLE:
687                    g.drawOval(p.x - radius, p.y - radius, s.size - 1, s.size - 1);
688                    break;
689                case TRIANGLE:
690                    g.drawPolygon(buildPolygon(p, radius, 3, Math.PI / 2));
691                    break;
692                case PENTAGON:
693                    g.drawPolygon(buildPolygon(p, radius, 5, Math.PI / 2));
694                    break;
695                case HEXAGON:
696                    g.drawPolygon(buildPolygon(p, radius, 6));
697                    break;
698                case HEPTAGON:
699                    g.drawPolygon(buildPolygon(p, radius, 7, Math.PI / 2));
700                    break;
701                case OCTAGON:
702                    g.drawPolygon(buildPolygon(p, radius, 8, Math.PI / 8));
703                    break;
704                case NONAGON:
705                    g.drawPolygon(buildPolygon(p, radius, 9, Math.PI / 2));
706                    break;
707                case DECAGON:
708                    g.drawPolygon(buildPolygon(p, radius, 10));
709                    break;
710                default:
711                    throw new AssertionError();
712                }
713                g.setStroke(new BasicStroke());
714            }
715        }
716    
717        /**
718         * Draw the node as small rectangle with the given color.
719         *
720         * @param n  The node to draw.
721         * @param color The color of the node.
722         */
723        public void drawNode(Node n, Color color, int size, boolean fill) {
724            if(size <= 0 && !n.isHighlighted())
725                return;
726    
727            Point p = nc.getPoint(n);
728    
729            if(n.isHighlighted()) {
730                drawPointHighlight(p, size);
731            }
732    
733            if (size > 1) {
734                if ((p.x < 0) || (p.y < 0) || (p.x > nc.getWidth()) || (p.y > nc.getHeight())) return;
735                int radius = size / 2;
736    
737                if (inactive || n.isDisabled()) {
738                    g.setColor(inactiveColor);
739                } else {
740                    g.setColor(color);
741                }
742                if (fill) {
743                    g.fillRect(p.x-radius-1, p.y-radius-1, size + 1, size + 1);
744                } else {
745                    g.drawRect(p.x-radius-1, p.y-radius-1, size, size);
746                }
747            }
748        }
749    
750        /**
751         * highlights a given point by drawing a rounded rectangle around it. Give the
752         * size of the object you want to be highlighted, width is added automatically.
753         */
754        private void drawPointHighlight(Point p, int size) {
755            g.setColor(highlightColorTransparent);
756            int s = size + 7;
757            while(s >= size) {
758                int r = (int) Math.floor(s/2);
759                g.fillRoundRect(p.x-r, p.y-r, s, s, r, r);
760                s -= 4;
761            }
762        }
763    
764        public void drawBoxText(Node n, BoxTextElemStyle bs) {
765            if (!isShowNames() || bs == null)
766                return;
767    
768            Point p = nc.getPoint(n);
769            TextElement text = bs.text;
770            String s = text.labelCompositionStrategy.compose(n);
771            if (s == null) return;
772    
773            Font defaultFont = g.getFont();
774            g.setFont(text.font);
775    
776            int x = p.x + text.xOffset;
777            int y = p.y + text.yOffset;
778            /**
779             *
780             *       left-above __center-above___ right-above
781             *         left-top|                 |right-top
782             *                 |                 |
783             *      left-center|  center-center  |right-center
784             *                 |                 |
785             *      left-bottom|_________________|right-bottom
786             *       left-below   center-below    right-below
787             *
788             */
789            Rectangle box = bs.getBox();
790            if (bs.hAlign == HorizontalTextAlignment.RIGHT) {
791                x += box.x + box.width + 2;
792            } else {
793                FontRenderContext frc = g.getFontRenderContext();
794                Rectangle2D bounds = text.font.getStringBounds(s, frc);
795                int textWidth = (int) bounds.getWidth();
796                if (bs.hAlign == HorizontalTextAlignment.CENTER) {
797                    x -= textWidth / 2;
798                } else if (bs.hAlign == HorizontalTextAlignment.LEFT) {
799                    x -= - box.x + 4 + textWidth;
800                } else throw new AssertionError();
801            }
802    
803            if (bs.vAlign == VerticalTextAlignment.BOTTOM) {
804                y += box.y + box.height;
805            } else {
806                FontRenderContext frc = g.getFontRenderContext();
807                LineMetrics metrics = text.font.getLineMetrics(s, frc);
808                if (bs.vAlign == VerticalTextAlignment.ABOVE) {
809                    y -= - box.y + metrics.getDescent();
810                } else if (bs.vAlign == VerticalTextAlignment.TOP) {
811                    y -= - box.y - metrics.getAscent();
812                } else if (bs.vAlign == VerticalTextAlignment.CENTER) {
813                    y += (metrics.getAscent() - metrics.getDescent()) / 2;
814                } else if (bs.vAlign == VerticalTextAlignment.BELOW) {
815                    y += box.y + box.height + metrics.getAscent() + 2;
816                } else throw new AssertionError();
817            }
818            if (inactive || n.isDisabled()) {
819                g.setColor(inactiveColor);
820            } else {
821                g.setColor(text.color);
822            }
823            if (text.haloRadius != null) {
824                g.setStroke(new BasicStroke(2*text.haloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND));
825                g.setColor(text.haloColor);
826                FontRenderContext frc = g.getFontRenderContext();
827                GlyphVector gv = text.font.createGlyphVector(frc, s);
828                Shape textOutline = gv.getOutline(x, y);
829                g.draw(textOutline);
830                g.setStroke(new BasicStroke());
831                g.setColor(text.color);
832                g.fill(textOutline);
833            } else {
834                g.drawString(s, x, y);
835            }
836            g.setFont(defaultFont);
837        }
838    
839        private Path2D.Double getPath(Way w) {
840            Path2D.Double path = new Path2D.Double();
841            boolean initial = true;
842            for (Node n : w.getNodes())
843            {
844                Point2D p = n.getEastNorth();
845                if (p != null) {
846                    if (initial) {
847                        path.moveTo(p.getX(), p.getY());
848                        initial = false;
849                    } else {
850                        path.lineTo(p.getX(), p.getY());
851                    }
852                }
853            }
854            return path;
855        }
856    
857        public void drawArea(Way w, Color color, MapImage fillImage, TextElement text) {
858            drawArea(w, getPath(w), color, fillImage, text);
859        }
860    
861        protected void drawArea(OsmPrimitive osm, Path2D.Double path, Color color, MapImage fillImage, TextElement text) {
862    
863            Shape area = path.createTransformedShape(nc.getAffineTransform());
864    
865            if (!isOutlineOnly) {
866                if (fillImage == null) {
867                    if (inactive) {
868                        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.33f));
869                    }
870                    g.setColor(color);
871                    g.fill(area);
872                } else {
873                    TexturePaint texture = new TexturePaint(fillImage.getImage(),
874                            //                        new Rectangle(polygon.xpoints[0], polygon.ypoints[0], fillImage.getWidth(), fillImage.getHeight()));
875                            new Rectangle(0, 0, fillImage.getWidth(), fillImage.getHeight()));
876                    g.setPaint(texture);
877                    Float alpha = Utils.color_int2float(fillImage.alpha);
878                    if (alpha != 1f) {
879                        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
880                    }
881                    g.fill(area);
882                    g.setPaintMode();
883                }
884            }
885    
886            if (text != null && isShowNames()) {
887                /*
888                 * abort if we can't compose the label to be rendered
889                 */
890                if (text.labelCompositionStrategy == null) return;
891                String name = text.labelCompositionStrategy.compose(osm);
892                if (name == null) return;
893    
894                Rectangle pb = area.getBounds();
895                FontMetrics fontMetrics = g.getFontMetrics(orderFont); // if slow, use cache
896                Rectangle2D nb = fontMetrics.getStringBounds(name, g); // if slow, approximate by strlen()*maxcharbounds(font)
897    
898                // Point2D c = getCentroid(polygon);
899                // Using the Centroid is Nicer for buildings like: +--------+
900                // but this needs to be fast.  As most houses are  |   42   |
901                // boxes anyway, the center of the bounding box    +---++---+
902                // will have to do.                                    ++
903                // Centroids are not optimal either, just imagine a U-shaped house.
904                // Point2D c = new Point2D.Double(pb.x + pb.width / 2.0, pb.y + pb.height / 2.0);
905                // Rectangle2D.Double centeredNBounds =
906                //     new Rectangle2D.Double(c.getX() - nb.getWidth()/2,
907                //                            c.getY() - nb.getHeight()/2,
908                //                            nb.getWidth(),
909                //                            nb.getHeight());
910    
911                Rectangle centeredNBounds = new Rectangle(pb.x + (int)((pb.width - nb.getWidth())/2.0),
912                        pb.y + (int)((pb.height - nb.getHeight())/2.0),
913                        (int)nb.getWidth(),
914                        (int)nb.getHeight());
915    
916                if ((pb.width >= nb.getWidth() && pb.height >= nb.getHeight()) && // quick check
917                        area.contains(centeredNBounds) // slow but nice
918                ) {
919                    if (inactive || osm.isDisabled()) {
920                        g.setColor(inactiveColor);
921                    } else {
922                        g.setColor(text.color);
923                    }
924                    Font defaultFont = g.getFont();
925                    g.setFont (text.font);
926                    g.drawString (name,
927                            (int)(centeredNBounds.getMinX() - nb.getMinX()),
928                            (int)(centeredNBounds.getMinY() - nb.getMinY()));
929                    g.setFont(defaultFont);
930                }
931            }
932        }
933    
934        public void drawArea(Relation r, Color color, MapImage fillImage, TextElement text) {
935            Multipolygon multipolygon = MultipolygonCache.getInstance().get(nc, r);
936            if (!r.isDisabled() && !multipolygon.getOuterWays().isEmpty()) {
937                for (PolyData pd : multipolygon.getCombinedPolygons()) {
938                    Path2D.Double p = pd.get();
939                    if (!isAreaVisible(p)) {
940                        continue;
941                    }
942                    drawArea(r, p,
943                            pd.selected ? settings.getRelationSelectedColor(color.getAlpha()) : color,
944                                    fillImage, text);
945                }
946            }
947        }
948    
949        private boolean isAreaVisible(Path2D.Double area) {
950            Rectangle2D bounds = area.getBounds2D();
951            if (bounds.isEmpty()) return false;
952            Point2D p = nc.getPoint2D(new EastNorth(bounds.getX(), bounds.getY()));
953            if (p.getX() > nc.getWidth()) return false;
954            if (p.getY() < 0) return false;
955            p = nc.getPoint2D(new EastNorth(bounds.getX() + bounds.getWidth(), bounds.getY() + bounds.getHeight()));
956            if (p.getX() < 0) return false;
957            if (p.getY() > nc.getHeight()) return false;
958            return true;
959        }
960    
961        public void drawRestriction(Image img, Point pVia, double vx, double vx2, double vy, double vy2, double angle, boolean selected) {
962            /* rotate image with direction last node in from to */
963            Image rotatedImg = ImageProvider.createRotatedImage(null , img, angle);
964    
965            /* scale down image to 16*16 pixels */
966            Image smallImg = new ImageIcon(rotatedImg.getScaledInstance(16 , 16, Image.SCALE_SMOOTH)).getImage();
967            int w = smallImg.getWidth(null), h=smallImg.getHeight(null);
968            g.drawImage(smallImg, (int)(pVia.x+vx+vx2)-w/2, (int)(pVia.y+vy+vy2)-h/2, nc);
969    
970            if (selected) {
971                g.setColor(inactive ? inactiveColor : relationSelectedColor);
972                g.drawRect((int)(pVia.x+vx+vx2)-w/2-2,(int)(pVia.y+vy+vy2)-h/2-2, w+4, h+4);
973            }
974        }
975    
976        public void drawRestriction(Relation r, MapImage icon) {
977            Way fromWay = null;
978            Way toWay = null;
979            OsmPrimitive via = null;
980    
981            /* find the "from", "via" and "to" elements */
982            for (RelationMember m : r.getMembers())
983            {
984                if(m.getMember().isIncomplete())
985                    return;
986                else
987                {
988                    if(m.isWay())
989                    {
990                        Way w = m.getWay();
991                        if(w.getNodesCount() < 2) {
992                            continue;
993                        }
994    
995                        if("from".equals(m.getRole())) {
996                            if(fromWay == null) {
997                                fromWay = w;
998                            }
999                        } else if("to".equals(m.getRole())) {
1000                            if(toWay == null) {
1001                                toWay = w;
1002                            }
1003                        } else if("via".equals(m.getRole())) {
1004                            if(via == null) {
1005                                via = w;
1006                            }
1007                        }
1008                    }
1009                    else if(m.isNode())
1010                    {
1011                        Node n = m.getNode();
1012                        if("via".equals(m.getRole()) && via == null) {
1013                            via = n;
1014                        }
1015                    }
1016                }
1017            }
1018    
1019            if (fromWay == null || toWay == null || via == null)
1020                return;
1021    
1022            Node viaNode;
1023            if(via instanceof Node)
1024            {
1025                viaNode = (Node) via;
1026                if(!fromWay.isFirstLastNode(viaNode))
1027                    return;
1028            }
1029            else
1030            {
1031                Way viaWay = (Way) via;
1032                Node firstNode = viaWay.firstNode();
1033                Node lastNode = viaWay.lastNode();
1034                Boolean onewayvia = false;
1035    
1036                String onewayviastr = viaWay.get("oneway");
1037                if(onewayviastr != null)
1038                {
1039                    if("-1".equals(onewayviastr)) {
1040                        onewayvia = true;
1041                        Node tmp = firstNode;
1042                        firstNode = lastNode;
1043                        lastNode = tmp;
1044                    } else {
1045                        onewayvia = OsmUtils.getOsmBoolean(onewayviastr);
1046                        if (onewayvia == null) {
1047                            onewayvia = false;
1048                        }
1049                    }
1050                }
1051    
1052                if(fromWay.isFirstLastNode(firstNode)) {
1053                    viaNode = firstNode;
1054                } else if (!onewayvia && fromWay.isFirstLastNode(lastNode)) {
1055                    viaNode = lastNode;
1056                } else
1057                    return;
1058            }
1059    
1060            /* find the "direct" nodes before the via node */
1061            Node fromNode = null;
1062            if(fromWay.firstNode() == via) {
1063                fromNode = fromWay.getNode(1);
1064            } else {
1065                fromNode = fromWay.getNode(fromWay.getNodesCount()-2);
1066            }
1067    
1068            Point pFrom = nc.getPoint(fromNode);
1069            Point pVia = nc.getPoint(viaNode);
1070    
1071            /* starting from via, go back the "from" way a few pixels
1072               (calculate the vector vx/vy with the specified length and the direction
1073               away from the "via" node along the first segment of the "from" way)
1074             */
1075            double distanceFromVia=14;
1076            double dx = (pFrom.x >= pVia.x) ? (pFrom.x - pVia.x) : (pVia.x - pFrom.x);
1077            double dy = (pFrom.y >= pVia.y) ? (pFrom.y - pVia.y) : (pVia.y - pFrom.y);
1078    
1079            double fromAngle;
1080            if(dx == 0.0) {
1081                fromAngle = Math.PI/2;
1082            } else {
1083                fromAngle = Math.atan(dy / dx);
1084            }
1085            double fromAngleDeg = Math.toDegrees(fromAngle);
1086    
1087            double vx = distanceFromVia * Math.cos(fromAngle);
1088            double vy = distanceFromVia * Math.sin(fromAngle);
1089    
1090            if(pFrom.x < pVia.x) {
1091                vx = -vx;
1092            }
1093            if(pFrom.y < pVia.y) {
1094                vy = -vy;
1095            }
1096    
1097            /* go a few pixels away from the way (in a right angle)
1098               (calculate the vx2/vy2 vector with the specified length and the direction
1099               90degrees away from the first segment of the "from" way)
1100             */
1101            double distanceFromWay=10;
1102            double vx2 = 0;
1103            double vy2 = 0;
1104            double iconAngle = 0;
1105    
1106            if(pFrom.x >= pVia.x && pFrom.y >= pVia.y) {
1107                if(!leftHandTraffic) {
1108                    vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1109                    vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1110                } else {
1111                    vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1112                    vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1113                }
1114                iconAngle = 270+fromAngleDeg;
1115            }
1116            if(pFrom.x < pVia.x && pFrom.y >= pVia.y) {
1117                if(!leftHandTraffic) {
1118                    vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1119                    vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1120                } else {
1121                    vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1122                    vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1123                }
1124                iconAngle = 90-fromAngleDeg;
1125            }
1126            if(pFrom.x < pVia.x && pFrom.y < pVia.y) {
1127                if(!leftHandTraffic) {
1128                    vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 90));
1129                    vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 90));
1130                } else {
1131                    vx2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg - 90));
1132                    vy2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg - 90));
1133                }
1134                iconAngle = 90+fromAngleDeg;
1135            }
1136            if(pFrom.x >= pVia.x && pFrom.y < pVia.y) {
1137                if(!leftHandTraffic) {
1138                    vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg + 180));
1139                    vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg + 180));
1140                } else {
1141                    vx2 = distanceFromWay * Math.sin(Math.toRadians(fromAngleDeg));
1142                    vy2 = distanceFromWay * Math.cos(Math.toRadians(fromAngleDeg));
1143                }
1144                iconAngle = 270-fromAngleDeg;
1145            }
1146    
1147            drawRestriction(inactive || r.isDisabled() ? icon.getDisabled() : icon.getImage(),
1148                    pVia, vx, vx2, vy, vy2, iconAngle, r.isSelected());
1149        }
1150    
1151        public void drawVirtualNodes(Collection<Way> ways, Collection<WaySegment> highlightVirtualNodes) {
1152            if (virtualNodeSize == 0)
1153                return;
1154            // print normal virtual nodes
1155            GeneralPath path = new GeneralPath();
1156            for (Way osm: ways){
1157                if (osm.isUsable() && !osm.isDisabled()) {
1158                    visitVirtual(path, osm);
1159                }
1160            }
1161            g.setColor(nodeColor);
1162            g.draw(path);
1163            // print highlighted virtual nodes. Since only the color changes, simply
1164            // drawing them over the existing ones works fine (at least in their current
1165            // simple style)
1166            path = new GeneralPath();
1167            for (WaySegment wseg: highlightVirtualNodes){
1168                if (wseg.way.isUsable() && !wseg.way.isDisabled()) {
1169                    visitVirtual(path, wseg.toWay());
1170                }
1171            }
1172            g.setColor(highlightColor);
1173            g.draw(path);
1174        }
1175    
1176        public void visitVirtual(GeneralPath path, Way w) {
1177            Iterator<Node> it = w.getNodes().iterator();
1178            if (it.hasNext()) {
1179                Point lastP = nc.getPoint(it.next());
1180                while(it.hasNext())
1181                {
1182                    Point p = nc.getPoint(it.next());
1183                    if(isSegmentVisible(lastP, p) && isLargeSegment(lastP, p, virtualNodeSpace))
1184                    {
1185                        int x = (p.x+lastP.x)/2;
1186                        int y = (p.y+lastP.y)/2;
1187                        path.moveTo(x-virtualNodeSize, y);
1188                        path.lineTo(x+virtualNodeSize, y);
1189                        path.moveTo(x, y-virtualNodeSize);
1190                        path.lineTo(x, y+virtualNodeSize);
1191                    }
1192                    lastP = p;
1193                }
1194            }
1195        }
1196    
1197        private static boolean isLargeSegment(Point p1, Point p2, int space)  {
1198            int xd = p1.x-p2.x; if(xd < 0) {
1199                xd = -xd;
1200            }
1201            int yd = p1.y-p2.y; if(yd < 0) {
1202                yd = -yd;
1203            }
1204            return (xd+yd > space);
1205        }
1206    
1207        /**
1208         * Draw a number of the order of the two consecutive nodes within the
1209         * parents way
1210         */
1211        public void drawOrderNumber(Node n1, Node n2, int orderNumber, Color clr) {
1212            Point p1 = nc.getPoint(n1);
1213            Point p2 = nc.getPoint(n2);
1214            drawOrderNumber(p1, p2, orderNumber, clr);
1215        }
1216    
1217        /**
1218         * Draw an number of the order of the two consecutive nodes within the
1219         * parents way
1220         */
1221        protected void drawOrderNumber(Point p1, Point p2, int orderNumber, Color clr) {
1222            if (isSegmentVisible(p1, p2) && isLargeSegment(p1, p2, segmentNumberSpace)) {
1223                String on = Integer.toString(orderNumber);
1224                int strlen = on.length();
1225                int x = (p1.x+p2.x)/2 - 4*strlen;
1226                int y = (p1.y+p2.y)/2 + 4;
1227    
1228                if(virtualNodeSize != 0 && isLargeSegment(p1, p2, virtualNodeSpace))
1229                {
1230                    y = (p1.y+p2.y)/2 - virtualNodeSize - 3;
1231                }
1232    
1233                g.setColor(backgroundColor);
1234                g.fillRect(x-1, y-12, 8*strlen+1, 14);
1235                g.setColor(clr);
1236                g.drawString(on, x, y);
1237            }
1238        }
1239    
1240        public boolean isShowNames() {
1241            return showNames;
1242        }
1243    
1244        public double getCircum() {
1245            return circum;
1246        }
1247    
1248        public boolean isShowIcons() {
1249            return showIcons;
1250        }
1251    
1252        public boolean isInactiveMode() {
1253            return inactive;
1254        }
1255    }