001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm.visitor.paint;
003
004import java.awt.Color;
005import java.awt.Graphics2D;
006import java.awt.geom.GeneralPath;
007import java.awt.geom.Path2D;
008import java.awt.geom.Rectangle2D;
009import java.util.Iterator;
010
011import org.openstreetmap.josm.Main;
012import org.openstreetmap.josm.data.osm.BBox;
013import org.openstreetmap.josm.data.osm.DataSet;
014import org.openstreetmap.josm.data.osm.Node;
015import org.openstreetmap.josm.data.osm.Way;
016import org.openstreetmap.josm.data.osm.WaySegment;
017import org.openstreetmap.josm.gui.MapViewState;
018import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
019import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle;
020import org.openstreetmap.josm.gui.NavigatableComponent;
021import org.openstreetmap.josm.tools.CheckParameterUtil;
022
023/**
024 * <p>Abstract common superclass for {@link Rendering} implementations.</p>
025 *
026 */
027public abstract class AbstractMapRenderer implements Rendering {
028
029    /** the graphics context to which the visitor renders OSM objects */
030    protected Graphics2D g;
031    /** the map viewport - provides projection and hit detection functionality */
032    protected NavigatableComponent nc;
033
034    /**
035     * The {@link MapViewState} to use to convert between coordinates.
036     */
037    protected final MapViewState mapState;
038
039    /** if true, the paint visitor shall render OSM objects such that they
040     * look inactive. Example: rendering of data in an inactive layer using light gray as color only. */
041    protected boolean isInactiveMode;
042    /** Color Preference for background */
043    protected Color backgroundColor;
044    /** Color Preference for inactive objects */
045    protected Color inactiveColor;
046    /** Color Preference for selected objects */
047    protected Color selectedColor;
048    /** Color Preference for members of selected relations */
049    protected Color relationSelectedColor;
050    /** Color Preference for nodes */
051    protected Color nodeColor;
052
053    /** Color Preference for hightlighted objects */
054    protected Color highlightColor;
055    /** Preference: size of virtual nodes (0 displayes display) */
056    protected int virtualNodeSize;
057    /** Preference: minimum space (displayed way length) to display virtual nodes */
058    protected int virtualNodeSpace;
059
060    /** Preference: minimum space (displayed way length) to display segment numbers */
061    protected int segmentNumberSpace;
062
063    /**
064     * <p>Creates an abstract paint visitor</p>
065     *
066     * @param g the graphics context. Must not be null.
067     * @param nc the map viewport. Must not be null.
068     * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they
069     * look inactive. Example: rendering of data in an inactive layer using light gray as color only.
070     * @throws IllegalArgumentException if {@code g} is null
071     * @throws IllegalArgumentException if {@code nc} is null
072     */
073    public AbstractMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) {
074        CheckParameterUtil.ensureParameterNotNull(g);
075        CheckParameterUtil.ensureParameterNotNull(nc);
076        this.g = g;
077        this.nc = nc;
078        this.mapState = nc.getState();
079        this.isInactiveMode = isInactiveMode;
080    }
081
082    /**
083     * Draw the node as small square with the given color.
084     *
085     * @param n  The node to draw.
086     * @param color The color of the node.
087     * @param size size in pixels
088     * @param fill determines if the square mmust be filled
089     */
090    public abstract void drawNode(Node n, Color color, int size, boolean fill);
091
092    /**
093     * Draw an number of the order of the two consecutive nodes within the
094     * parents way
095     *
096     * @param p1 First point of the way segment.
097     * @param p2 Second point of the way segment.
098     * @param orderNumber The number of the segment in the way.
099     * @param clr The color to use for drawing the text.
100     * @since 10827
101     */
102    protected void drawOrderNumber(MapViewPoint p1, MapViewPoint p2, int orderNumber, Color clr) {
103        if (isSegmentVisible(p1, p2) && isLargeSegment(p1, p2, segmentNumberSpace)) {
104            String on = Integer.toString(orderNumber);
105            int strlen = on.length();
106            double centerX = (p1.getInViewX()+p2.getInViewX())/2;
107            double centerY = (p1.getInViewY()+p2.getInViewY())/2;
108            double x = centerX - 4*strlen;
109            double y = centerY + 4;
110
111            if (virtualNodeSize != 0 && isLargeSegment(p1, p2, virtualNodeSpace)) {
112                y = centerY - virtualNodeSize - 3;
113            }
114
115            g.setColor(backgroundColor);
116            g.fill(new Rectangle2D.Double(x-1, y-12, 8*strlen+1, 14));
117            g.setColor(clr);
118            g.drawString(on, (int) x, (int) y);
119        }
120    }
121
122    /**
123     * Draws virtual nodes.
124     *
125     * @param data The data set being rendered.
126     * @param bbox The bounding box being displayed.
127     */
128    public void drawVirtualNodes(DataSet data, BBox bbox) {
129        if (virtualNodeSize == 0 || data == null || bbox == null)
130            return;
131        // print normal virtual nodes
132        GeneralPath path = new GeneralPath();
133        for (Way osm : data.searchWays(bbox)) {
134            if (osm.isUsable() && !osm.isDisabledAndHidden() && !osm.isDisabled()) {
135                visitVirtual(path, osm);
136            }
137        }
138        g.setColor(nodeColor);
139        g.draw(path);
140        try {
141            // print highlighted virtual nodes. Since only the color changes, simply
142            // drawing them over the existing ones works fine (at least in their current simple style)
143            path = new GeneralPath();
144            for (WaySegment wseg: data.getHighlightedVirtualNodes()) {
145                if (wseg.way.isUsable() && !wseg.way.isDisabled()) {
146                    visitVirtual(path, wseg.toWay());
147                }
148            }
149            g.setColor(highlightColor);
150            g.draw(path);
151        } catch (ArrayIndexOutOfBoundsException e) {
152            // Silently ignore any ArrayIndexOutOfBoundsException that may be raised
153            // if the way has changed while being rendered (fix #7979)
154            // TODO: proper solution ?
155            // Idea from bastiK:
156            // avoid the WaySegment class and add another data class with { Way way; Node firstNode, secondNode; int firstIdx; }.
157            // On read, it would first check, if the way still has firstIdx+2 nodes, then check if the corresponding way nodes are still
158            // the same and report changes in a more controlled manner.
159            Main.trace(e);
160        }
161    }
162
163    /**
164     * Reads the color definitions from preferences. This function is <code>public</code>, so that
165     * color names in preferences can be displayed even without calling the wireframe display before.
166     */
167    public void getColors() {
168        this.backgroundColor = PaintColors.BACKGROUND.get();
169        this.inactiveColor = PaintColors.INACTIVE.get();
170        this.selectedColor = PaintColors.SELECTED.get();
171        this.relationSelectedColor = PaintColors.RELATIONSELECTED.get();
172        this.nodeColor = PaintColors.NODE.get();
173        this.highlightColor = PaintColors.HIGHLIGHT.get();
174    }
175
176    /**
177     * Reads all the settings from preferences. Calls the @{link #getColors}
178     * function.
179     *
180     * @param virtual <code>true</code> if virtual nodes are used
181     */
182    protected void getSettings(boolean virtual) {
183        this.virtualNodeSize = virtual ? Main.pref.getInteger("mappaint.node.virtual-size", 8) / 2 : 0;
184        this.virtualNodeSpace = Main.pref.getInteger("mappaint.node.virtual-space", 70);
185        this.segmentNumberSpace = Main.pref.getInteger("mappaint.segmentnumber.space", 40);
186        getColors();
187    }
188
189    /**
190     * Checks if a way segemnt is large enough for additional information display.
191     *
192     * @param p1 First point of the way segment.
193     * @param p2 Second point of the way segment.
194     * @param space The free space to check against.
195     * @return <code>true</code> if segment is larger than required space
196     * @since 10827
197     */
198    public static boolean isLargeSegment(MapViewPoint p1, MapViewPoint p2, int space) {
199        return p1.oneNormInView(p2) > space;
200    }
201
202    /**
203     * Checks if segment is visible in display.
204     *
205     * @param p1 First point of the way segment.
206     * @param p2 Second point of the way segment.
207     * @return <code>true</code> if segment may be visible.
208     * @since 10827
209     */
210    protected boolean isSegmentVisible(MapViewPoint p1, MapViewPoint p2) {
211        MapViewRectangle view = mapState.getViewArea();
212        // not outside in the same direction
213        return (p1.getOutsideRectangleFlags(view) & p2.getOutsideRectangleFlags(view)) == 0;
214    }
215
216    /**
217     * Creates path for drawing virtual nodes for one way.
218     *
219     * @param path The path to append drawing to.
220     * @param w The ways to draw node for.
221     * @since 10827
222     */
223    public void visitVirtual(Path2D path, Way w) {
224        Iterator<Node> it = w.getNodes().iterator();
225        if (it.hasNext()) {
226            MapViewPoint lastP = mapState.getPointFor(it.next());
227            while (it.hasNext()) {
228                MapViewPoint p = mapState.getPointFor(it.next());
229                if (isSegmentVisible(lastP, p) && isLargeSegment(lastP, p, virtualNodeSpace)) {
230                    double x = (p.getInViewX()+lastP.getInViewX())/2;
231                    double y = (p.getInViewY()+lastP.getInViewY())/2;
232                    path.moveTo(x-virtualNodeSize, y);
233                    path.lineTo(x+virtualNodeSize, y);
234                    path.moveTo(x, y-virtualNodeSize);
235                    path.lineTo(x, y+virtualNodeSize);
236                }
237                lastP = p;
238            }
239        }
240    }
241}