001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation;
003
004import java.awt.Color;
005import java.awt.Graphics;
006import java.awt.Point;
007import java.util.HashSet;
008import java.util.List;
009import java.util.Objects;
010import java.util.Set;
011
012import org.openstreetmap.josm.data.coor.LatLon;
013import org.openstreetmap.josm.data.osm.Node;
014import org.openstreetmap.josm.data.osm.OsmPrimitive;
015import org.openstreetmap.josm.data.osm.Relation;
016import org.openstreetmap.josm.data.osm.Way;
017import org.openstreetmap.josm.data.osm.WaySegment;
018import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
019import org.openstreetmap.josm.gui.MapView;
020
021/**
022 * Visitor that highlights the primitives affected by an error
023 * @author frsantos
024 * @since 5671
025 */
026public class PaintVisitor extends AbstractVisitor implements ValidatorVisitor {
027    /** The graphics */
028    private final Graphics g;
029    /** The MapView */
030    private final MapView mv;
031
032    /** The severity color */
033    private Color color;
034    /** Is the error selected ? */
035    private boolean selected;
036
037    private final Set<PaintedPoint> paintedPoints = new HashSet<>();
038    private final Set<PaintedSegment> paintedSegments = new HashSet<>();
039
040    /**
041     * Constructor
042     * @param g The graphics
043     * @param mv The Mapview
044     */
045    public PaintVisitor(Graphics g, MapView mv) {
046        this.g = g;
047        this.mv = mv;
048    }
049
050    protected static class PaintedPoint {
051        protected final LatLon p1;
052        protected final Color color;
053
054        public PaintedPoint(LatLon p1, Color color) {
055            this.p1 = p1;
056            this.color = color;
057        }
058
059        @Override
060        public int hashCode() {
061            return Objects.hash(p1, color);
062        }
063
064        @Override
065        public boolean equals(Object obj) {
066            if (this == obj) return true;
067            if (obj == null || getClass() != obj.getClass()) return false;
068            PaintedPoint that = (PaintedPoint) obj;
069            return Objects.equals(p1, that.p1) &&
070                    Objects.equals(color, that.color);
071        }
072    }
073
074    protected static class PaintedSegment extends PaintedPoint {
075        private final LatLon p2;
076
077        public PaintedSegment(LatLon p1, LatLon p2, Color color) {
078            super(p1, color);
079            this.p2 = p2;
080        }
081
082        @Override
083        public int hashCode() {
084            return Objects.hash(super.hashCode(), p2);
085        }
086
087        @Override
088        public boolean equals(Object obj) {
089            if (this == obj) return true;
090            if (obj == null || getClass() != obj.getClass()) return false;
091            if (!super.equals(obj)) return false;
092            PaintedSegment that = (PaintedSegment) obj;
093            return Objects.equals(p2, that.p2);
094        }
095    }
096
097    @Override
098    public void visit(TestError error) {
099        if (error != null && !error.isIgnored()) {
100            color = error.getSeverity().getColor();
101            selected = error.isSelected();
102            error.visitHighlighted(this);
103        }
104    }
105
106    @Override
107    public void visit(OsmPrimitive p) {
108        if (p.isUsable()) {
109            p.accept(this);
110        }
111    }
112
113    /**
114     * Draws a circle around the node
115     * @param n The node
116     * @param color The circle color
117     */
118    protected void drawNode(Node n, Color color) {
119        PaintedPoint pp = new PaintedPoint(n.getCoor(), color);
120
121        if (!paintedPoints.contains(pp)) {
122            Point p = mv.getPoint(n);
123
124            if (selected) {
125                g.setColor(getHighlightColor(color));
126                g.fillOval(p.x - 5, p.y - 5, 10, 10);
127            }
128            g.setColor(color);
129            g.drawOval(p.x - 5, p.y - 5, 10, 10);
130            paintedPoints.add(pp);
131        }
132    }
133
134    /**
135     * Draws a line around the segment
136     *
137     * @param p1 The first point of segment
138     * @param p2 The second point of segment
139     * @param color The color
140     */
141    protected void drawSegment(Point p1, Point p2, Color color) {
142
143        double t = Math.atan2((double) p2.x - p1.x, (double) p2.y - p1.y);
144        double cosT = 5 * Math.cos(t);
145        double sinT = 5 * Math.sin(t);
146        int deg = (int) Math.toDegrees(t);
147        if (selected) {
148            g.setColor(getHighlightColor(color));
149            int[] x = new int[] {(int) (p1.x + cosT), (int) (p2.x + cosT),
150                                 (int) (p2.x - cosT), (int) (p1.x - cosT)};
151            int[] y = new int[] {(int) (p1.y - sinT), (int) (p2.y - sinT),
152                                 (int) (p2.y + sinT), (int) (p1.y + sinT)};
153            g.fillPolygon(x, y, 4);
154            g.fillArc(p1.x - 5, p1.y - 5, 10, 10, deg,  180);
155            g.fillArc(p2.x - 5, p2.y - 5, 10, 10, deg, -180);
156        }
157        g.setColor(color);
158        g.drawLine((int) (p1.x + cosT), (int) (p1.y - sinT),
159                (int) (p2.x + cosT), (int) (p2.y - sinT));
160        g.drawLine((int) (p1.x - cosT), (int) (p1.y + sinT),
161                (int) (p2.x - cosT), (int) (p2.y + sinT));
162        g.drawArc(p1.x - 5, p1.y - 5, 10, 10, deg,  180);
163        g.drawArc(p2.x - 5, p2.y - 5, 10, 10, deg, -180);
164    }
165
166    /**
167     * Draws a line around the segment
168     *
169     * @param n1 The first node of segment
170     * @param n2 The second node of segment
171     * @param color The color
172     */
173    protected void drawSegment(Node n1, Node n2, Color color) {
174        if (n1.isDrawable() && n2.isDrawable() && isSegmentVisible(n1, n2)) {
175            PaintedSegment ps = new PaintedSegment(n1.getCoor(), n2.getCoor(), color);
176            if (!paintedSegments.contains(ps)) {
177                drawSegment(mv.getPoint(n1), mv.getPoint(n2), color);
178                paintedSegments.add(ps);
179            }
180        }
181    }
182
183    /**
184     * Draw a small rectangle.
185     * White if selected (as always) or red otherwise.
186     *
187     * @param n The node to draw.
188     */
189    @Override
190    public void visit(Node n) {
191        if (n.isDrawable() && isNodeVisible(n)) {
192            drawNode(n, color);
193        }
194    }
195
196    @Override
197    public void visit(Way w) {
198        visit(w.getNodes());
199    }
200
201    @Override
202    public void visit(WaySegment ws) {
203        if (ws.lowerIndex < 0 || ws.lowerIndex + 1 >= ws.way.getNodesCount())
204            return;
205        Node a = ws.way.getNodes().get(ws.lowerIndex);
206        Node b = ws.way.getNodes().get(ws.lowerIndex + 1);
207        drawSegment(a, b, color);
208    }
209
210    @Override
211    public void visit(Relation r) {
212        /* No idea how to draw a relation. */
213    }
214
215    /**
216     * Checks if the given node is in the visible area.
217     * @param n The node to check for visibility
218     * @return true if the node is visible
219     */
220    protected boolean isNodeVisible(Node n) {
221        Point p = mv.getPoint(n);
222        return !((p.x < 0) || (p.y < 0) || (p.x > mv.getWidth()) || (p.y > mv.getHeight()));
223    }
224
225    /**
226     * Checks if the given segment is in the visible area.
227     * NOTE: This will return true for a small number of non-visible
228     *       segments.
229     * @param n1 The first point of the segment to check
230     * @param n2 The second point of the segment to check
231     * @return {@code true} if the segment is visible
232     */
233    protected boolean isSegmentVisible(Node n1, Node n2) {
234        Point p1 = mv.getPoint(n1);
235        Point p2 = mv.getPoint(n2);
236        if ((p1.x < 0) && (p2.x < 0))
237            return false;
238        if ((p1.y < 0) && (p2.y < 0))
239            return false;
240        if ((p1.x > mv.getWidth()) && (p2.x > mv.getWidth()))
241            return false;
242        if ((p1.y > mv.getHeight()) && (p2.y > mv.getHeight()))
243            return false;
244        return true;
245    }
246
247    @Override
248    public void visit(List<Node> nodes) {
249        Node lastN = null;
250        for (Node n : nodes) {
251            if (lastN == null) {
252                lastN = n;
253                continue;
254            }
255            drawSegment(lastN, n, color);
256            lastN = n;
257        }
258    }
259
260    /**
261     * Gets the color to draw highlight markers with.
262     * @param color severity color
263     * @return The color.
264     */
265    private static Color getHighlightColor(Color color) {
266        return new Color(color.getRed(), color.getGreen(), color.getBlue(), (int) (color.getAlpha() * .4));
267    }
268
269    /**
270     * Clears the internal painted objects collections.
271     */
272    public void clearPaintedObjects() {
273        paintedPoints.clear();
274        paintedSegments.clear();
275    }
276}