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