001    // License: GPL. See LICENSE file for details.
002    package org.openstreetmap.josm.data.validation;
003    
004    import java.awt.Color;
005    import java.awt.Graphics;
006    import java.awt.Point;
007    import java.util.Collection;
008    import java.util.Collections;
009    import java.util.List;
010    import java.util.TreeSet;
011    import java.util.ArrayList;
012    
013    import org.openstreetmap.josm.command.Command;
014    import org.openstreetmap.josm.data.osm.Node;
015    import org.openstreetmap.josm.data.osm.OsmPrimitive;
016    import org.openstreetmap.josm.data.osm.Relation;
017    import org.openstreetmap.josm.data.osm.Way;
018    import org.openstreetmap.josm.data.osm.WaySegment;
019    import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
020    import org.openstreetmap.josm.gui.MapView;
021    
022    /**
023     * Validation error
024     * @author frsantos
025     */
026    public class TestError {
027        /** is this error on the ignore list */
028        private Boolean ignored = false;
029        /** Severity */
030        private Severity severity;
031        /** The error message */
032        private String message;
033        /** Deeper error description */
034        private String description;
035        private String description_en;
036        /** The affected primitives */
037        private Collection<? extends OsmPrimitive> primitives;
038        /** The primitives to be highlighted */
039        private Collection<?> highlighted;
040        /** The tester that raised this error */
041        private Test tester;
042        /** Internal code used by testers to classify errors */
043        private int code;
044        /** If this error is selected */
045        private boolean selected;
046    
047        /**
048         * Constructors
049         * @param tester The tester
050         * @param severity The severity of this error
051         * @param message The error message
052         * @param primitive The affected primitive
053         * @param primitives The affected primitives
054         * @param code The test error reference code
055         */
056        public TestError(Test tester, Severity severity, String message, String description, String description_en,
057                int code, Collection<? extends OsmPrimitive> primitives, Collection<?> highlighted) {
058            this.tester = tester;
059            this.severity = severity;
060            this.message = message;
061            this.description = description;
062            this.description_en = description_en;
063            this.primitives = primitives;
064            this.highlighted = highlighted;
065            this.code = code;
066        }
067    
068        public TestError(Test tester, Severity severity, String message, int code, Collection<? extends OsmPrimitive> primitives,
069                Collection<?> highlighted) {
070            this(tester, severity, message, null, null, code, primitives, highlighted);
071        }
072    
073        public TestError(Test tester, Severity severity, String message, String description, String description_en,
074                int code, Collection<? extends OsmPrimitive> primitives) {
075            this(tester, severity, message, description, description_en, code, primitives, primitives);
076        }
077    
078        public TestError(Test tester, Severity severity, String message, int code, Collection<? extends OsmPrimitive> primitives) {
079            this(tester, severity, message, null, null, code, primitives, primitives);
080        }
081    
082        public TestError(Test tester, Severity severity, String message, int code, OsmPrimitive primitive) {
083            this(tester, severity, message, null, null, code, Collections.singletonList(primitive), Collections
084                    .singletonList(primitive));
085        }
086    
087        public TestError(Test tester, Severity severity, String message, String description, String description_en,
088                int code, OsmPrimitive primitive) {
089            this(tester, severity, message, description, description_en, code, Collections.singletonList(primitive));
090        }
091    
092        /**
093         * Gets the error message
094         * @return the error message
095         */
096        public String getMessage() {
097            return message;
098        }
099    
100        /**
101         * Gets the error message
102         * @return the error description
103         */
104        public String getDescription() {
105            return description;
106        }
107    
108        /**
109         * Sets the error message
110         * @param message The error message
111         */
112        public void setMessage(String message) {
113            this.message = message;
114        }
115    
116        /**
117         * Gets the list of primitives affected by this error
118         * @return the list of primitives affected by this error
119         */
120        public Collection<? extends OsmPrimitive> getPrimitives() {
121            return primitives;
122        }
123    
124        /**
125         * Gets the list of primitives affected by this error and are selectable
126         * @return the list of selectable primitives affected by this error
127         */
128        public Collection<? extends OsmPrimitive> getSelectablePrimitives() {
129            List<OsmPrimitive> selectablePrimitives = new ArrayList<OsmPrimitive>(primitives.size());
130            for (OsmPrimitive o : primitives) {
131                if (o.isSelectable()) {
132                    selectablePrimitives.add(o);
133                }
134            }
135            return selectablePrimitives;
136        }
137    
138    
139        /**
140         * Sets the list of primitives affected by this error
141         * @param primitives the list of primitives affected by this error
142         */
143    
144        public void setPrimitives(List<OsmPrimitive> primitives) {
145            this.primitives = primitives;
146        }
147    
148        /**
149         * Gets the severity of this error
150         * @return the severity of this error
151         */
152        public Severity getSeverity() {
153            return severity;
154        }
155    
156        /**
157         * Sets the severity of this error
158         * @param severity the severity of this error
159         */
160        public void setSeverity(Severity severity) {
161            this.severity = severity;
162        }
163    
164        /**
165         * Sets the ignore state for this error
166         */
167        public String getIgnoreState() {
168            Collection<String> strings = new TreeSet<String>();
169            String ignorestring = getIgnoreSubGroup();
170            for (OsmPrimitive o : primitives) {
171                // ignore data not yet uploaded
172                if (o.isNew())
173                    return null;
174                String type = "u";
175                if (o instanceof Way) {
176                    type = "w";
177                } else if (o instanceof Relation) {
178                    type = "r";
179                } else if (o instanceof Node) {
180                    type = "n";
181                }
182                strings.add(type + "_" + o.getId());
183            }
184            for (String o : strings) {
185                ignorestring += ":" + o;
186            }
187            return ignorestring;
188        }
189    
190        public String getIgnoreSubGroup() {
191            String ignorestring = getIgnoreGroup();
192            if (description_en != null) {
193                ignorestring += "_" + description_en;
194            }
195            return ignorestring;
196        }
197    
198        public String getIgnoreGroup() {
199            return Integer.toString(code);
200        }
201    
202        public void setIgnored(boolean state) {
203            ignored = state;
204        }
205    
206        public Boolean getIgnored() {
207            return ignored;
208        }
209    
210        /**
211         * Gets the tester that raised this error
212         * @return the tester that raised this error
213         */
214        public Test getTester() {
215            return tester;
216        }
217    
218        /**
219         * Gets the code
220         * @return the code
221         */
222        public int getCode() {
223            return code;
224        }
225    
226        /**
227         * Returns true if the error can be fixed automatically
228         *
229         * @return true if the error can be fixed
230         */
231        public boolean isFixable() {
232            return tester != null && tester.isFixable(this);
233        }
234    
235        /**
236         * Fixes the error with the appropriate command
237         *
238         * @return The command to fix the error
239         */
240        public Command getFix() {
241            if (tester == null || !tester.isFixable(this))
242                return null;
243    
244            return tester.fixError(this);
245        }
246    
247        /**
248         * Paints the error on affected primitives
249         *
250         * @param g The graphics
251         * @param mv The MapView
252         */
253        public void paint(Graphics g, MapView mv) {
254            if (!ignored) {
255                PaintVisitor v = new PaintVisitor(g, mv);
256                visitHighlighted(v);
257            }
258        }
259    
260        @SuppressWarnings("unchecked")
261        public void visitHighlighted(ValidatorVisitor v) {
262            for (Object o : highlighted) {
263                if (o instanceof OsmPrimitive) {
264                    v.visit((OsmPrimitive) o);
265                } else if (o instanceof WaySegment) {
266                    v.visit((WaySegment) o);
267                } else if (o instanceof List<?>) {
268                    v.visit((List<Node>)o);
269                }
270            }
271        }
272    
273        /**
274         * Visitor that highlights the primitives affected by this error
275         * @author frsantos
276         */
277        class PaintVisitor extends AbstractVisitor implements ValidatorVisitor {
278            /** The graphics */
279            private final Graphics g;
280            /** The MapView */
281            private final MapView mv;
282    
283            /**
284             * Constructor
285             * @param g The graphics
286             * @param mv The Mapview
287             */
288            public PaintVisitor(Graphics g, MapView mv) {
289                this.g = g;
290                this.mv = mv;
291            }
292    
293            @Override
294            public void visit(OsmPrimitive p) {
295                if (p.isUsable()) {
296                    p.visit(this);
297                }
298            }
299    
300            /**
301             * Draws a circle around the node
302             * @param n The node
303             * @param color The circle color
304             */
305            public void drawNode(Node n, Color color) {
306                Point p = mv.getPoint(n);
307                g.setColor(color);
308                if (selected) {
309                    g.fillOval(p.x - 5, p.y - 5, 10, 10);
310                } else {
311                    g.drawOval(p.x - 5, p.y - 5, 10, 10);
312                }
313            }
314    
315            public void drawSegment(Point p1, Point p2, Color color) {
316                g.setColor(color);
317    
318                double t = Math.atan2(p2.x - p1.x, p2.y - p1.y);
319                double cosT = Math.cos(t);
320                double sinT = Math.sin(t);
321                int deg = (int) Math.toDegrees(t);
322                if (selected) {
323                    int[] x = new int[] { (int) (p1.x + 5 * cosT), (int) (p2.x + 5 * cosT),
324                                          (int) (p2.x - 5 * cosT), (int) (p1.x - 5 * cosT) };
325                    int[] y = new int[] { (int) (p1.y - 5 * sinT), (int) (p2.y - 5 * sinT),
326                                          (int) (p2.y + 5 * sinT), (int) (p1.y + 5 * sinT) };
327                    g.fillPolygon(x, y, 4);
328                    g.fillArc(p1.x - 5, p1.y - 5, 10, 10, deg, 180);
329                    g.fillArc(p2.x - 5, p2.y - 5, 10, 10, deg, -180);
330                } else {
331                    g.drawLine((int) (p1.x + 5 * cosT), (int) (p1.y - 5 * sinT),
332                               (int) (p2.x + 5 * cosT), (int) (p2.y - 5 * sinT));
333                    g.drawLine((int) (p1.x - 5 * cosT), (int) (p1.y + 5 * sinT),
334                               (int) (p2.x - 5 * cosT), (int) (p2.y + 5 * sinT));
335                    g.drawArc(p1.x - 5, p1.y - 5, 10, 10, deg, 180);
336                    g.drawArc(p2.x - 5, p2.y - 5, 10, 10, deg, -180);
337                }
338            }
339    
340            /**
341             * Draws a line around the segment
342             *
343             * @param s The segment
344             * @param color The color
345             */
346            public void drawSegment(Node n1, Node n2, Color color) {
347                drawSegment(mv.getPoint(n1), mv.getPoint(n2), color);
348            }
349    
350            /**
351             * Draw a small rectangle.
352             * White if selected (as always) or red otherwise.
353             *
354             * @param n The node to draw.
355             */
356            @Override
357            public void visit(Node n) {
358                if (isNodeVisible(n)) {
359                    drawNode(n, severity.getColor());
360                }
361            }
362    
363            @Override
364            public void visit(Way w) {
365                visit(w.getNodes());
366            }
367    
368            @Override
369            public void visit(WaySegment ws) {
370                if (ws.lowerIndex < 0 || ws.lowerIndex + 1 >= ws.way.getNodesCount())
371                    return;
372                Node a = ws.way.getNodes().get(ws.lowerIndex), b = ws.way.getNodes().get(ws.lowerIndex + 1);
373                if (isSegmentVisible(a, b)) {
374                    drawSegment(a, b, severity.getColor());
375                }
376            }
377    
378            @Override
379            public void visit(Relation r) {
380                /* No idea how to draw a relation. */
381            }
382    
383            /**
384             * Checks if the given node is in the visible area.
385             * @param n The node to check for visibility
386             * @return true if the node is visible
387             */
388            protected boolean isNodeVisible(Node n) {
389                Point p = mv.getPoint(n);
390                return !((p.x < 0) || (p.y < 0) || (p.x > mv.getWidth()) || (p.y > mv.getHeight()));
391            }
392    
393            /**
394             * Checks if the given segment is in the visible area.
395             * NOTE: This will return true for a small number of non-visible
396             *       segments.
397             * @param ls The segment to check
398             * @return true if the segment is visible
399             */
400            protected boolean isSegmentVisible(Node n1, Node n2) {
401                Point p1 = mv.getPoint(n1);
402                Point p2 = mv.getPoint(n2);
403                if ((p1.x < 0) && (p2.x < 0))
404                    return false;
405                if ((p1.y < 0) && (p2.y < 0))
406                    return false;
407                if ((p1.x > mv.getWidth()) && (p2.x > mv.getWidth()))
408                    return false;
409                if ((p1.y > mv.getHeight()) && (p2.y > mv.getHeight()))
410                    return false;
411                return true;
412            }
413    
414            @Override
415            public void visit(List<Node> nodes) {
416                Node lastN = null;
417                for (Node n : nodes) {
418                    if (lastN == null) {
419                        lastN = n;
420                        continue;
421                    }
422                    if (n.isDrawable() && isSegmentVisible(lastN, n)) {
423                        drawSegment(lastN, n, severity.getColor());
424                    }
425                    lastN = n;
426                }
427            }
428        }
429    
430        /**
431         * Sets the selection flag of this error
432         * @param selected if this error is selected
433         */
434        public void setSelected(boolean selected) {
435            this.selected = selected;
436        }
437    }