001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation.tests;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.geom.Area;
007import java.util.ArrayList;
008import java.util.Collection;
009import java.util.Collections;
010import java.util.Iterator;
011import java.util.LinkedList;
012import java.util.List;
013
014import org.openstreetmap.josm.Main;
015import org.openstreetmap.josm.command.ChangeCommand;
016import org.openstreetmap.josm.command.Command;
017import org.openstreetmap.josm.data.osm.Node;
018import org.openstreetmap.josm.data.osm.OsmPrimitive;
019import org.openstreetmap.josm.data.osm.Way;
020import org.openstreetmap.josm.data.validation.Severity;
021import org.openstreetmap.josm.data.validation.Test;
022import org.openstreetmap.josm.data.validation.TestError;
023import org.openstreetmap.josm.gui.layer.OsmDataLayer;
024import org.openstreetmap.josm.gui.progress.ProgressMonitor;
025
026/**
027 * Check coastlines for errors
028 *
029 * @author frsantos
030 * @author Teemu Koskinen
031 */
032public class Coastlines extends Test {
033
034    protected static final int UNORDERED_COASTLINE = 901;
035    protected static final int REVERSED_COASTLINE = 902;
036    protected static final int UNCONNECTED_COASTLINE = 903;
037
038    private List<Way> coastlines;
039
040    private Area downloadedArea;
041
042    /**
043     * Constructor
044     */
045    public Coastlines() {
046        super(tr("Coastlines"),
047                tr("This test checks that coastlines are correct."));
048    }
049
050    @Override
051    public void startTest(ProgressMonitor monitor) {
052
053        super.startTest(monitor);
054
055        OsmDataLayer layer = Main.main.getEditLayer();
056
057        if (layer != null) {
058            downloadedArea = layer.data.getDataSourceArea();
059        }
060
061        coastlines = new LinkedList<>();
062    }
063
064    @Override
065    public void endTest() {
066        for (Way c1 : coastlines) {
067            Node head = c1.firstNode();
068            Node tail = c1.lastNode();
069
070            if (c1.getNodesCount() == 0 || head.equals(tail)) {
071                continue;
072            }
073
074            int headWays = 0;
075            int tailWays = 0;
076            boolean headReversed = false;
077            boolean tailReversed = false;
078            boolean headUnordered = false;
079            boolean tailUnordered = false;
080            Way next = null;
081            Way prev = null;
082
083            for (Way c2 : coastlines) {
084                if (c1 == c2) {
085                    continue;
086                }
087
088                if (c2.containsNode(head)) {
089                    headWays++;
090                    next = c2;
091
092                    if (head.equals(c2.firstNode())) {
093                        headReversed = true;
094                    } else if (!head.equals(c2.lastNode())) {
095                        headUnordered = true;
096                    }
097                }
098
099                if (c2.containsNode(tail)) {
100                    tailWays++;
101                    prev = c2;
102
103                    if (tail.equals(c2.lastNode())) {
104                        tailReversed = true;
105                    } else if (!tail.equals(c2.firstNode())) {
106                        tailUnordered = true;
107                    }
108                }
109            }
110
111            // To avoid false positives on upload (only modified primitives
112            // are visited), we have to check possible connection to ways
113            // that are not in the set of validated primitives.
114            if (headWays == 0) {
115                Collection<OsmPrimitive> refs = head.getReferrers();
116                for (OsmPrimitive ref : refs) {
117                    if (ref != c1 && isCoastline(ref)) {
118                        // ref cannot be in <code>coastlines</code>, otherwise we would
119                        // have picked it up already
120                        headWays++;
121                        next = (Way) ref;
122
123                        if (head.equals(next.firstNode())) {
124                            headReversed = true;
125                        } else if (!head.equals(next.lastNode())) {
126                            headUnordered = true;
127                        }
128                    }
129                }
130            }
131            if (tailWays == 0) {
132                Collection<OsmPrimitive> refs = tail.getReferrers();
133                for (OsmPrimitive ref : refs) {
134                    if (ref != c1 && isCoastline(ref)) {
135                        tailWays++;
136                        prev = (Way) ref;
137
138                        if (tail.equals(prev.lastNode())) {
139                            tailReversed = true;
140                        } else if (!tail.equals(prev.firstNode())) {
141                            tailUnordered = true;
142                        }
143                    }
144                }
145            }
146
147            List<OsmPrimitive> primitives = new ArrayList<>();
148            primitives.add(c1);
149
150            if (headWays == 0 || tailWays == 0) {
151                List<OsmPrimitive> highlight = new ArrayList<>();
152
153                if (headWays == 0 && head.getCoor().isIn(downloadedArea)) {
154                    highlight.add(head);
155                }
156                if (tailWays == 0 && tail.getCoor().isIn(downloadedArea)) {
157                    highlight.add(tail);
158                }
159
160                if (!highlight.isEmpty()) {
161                    errors.add(new TestError(this, Severity.ERROR, tr("Unconnected coastline"),
162                            UNCONNECTED_COASTLINE, primitives, highlight));
163                }
164            }
165
166            boolean unordered = false;
167            boolean reversed = headWays == 1 && headReversed && tailWays == 1 && tailReversed;
168
169            if (headWays > 1 || tailWays > 1) {
170                unordered = true;
171            } else if (headUnordered || tailUnordered) {
172                unordered = true;
173            } else if (reversed && next == prev) {
174                unordered = true;
175            } else if ((headReversed || tailReversed) && headReversed != tailReversed) {
176                unordered = true;
177            }
178
179            if (unordered) {
180                List<OsmPrimitive> highlight = new ArrayList<>();
181
182                if (headWays > 1 || headUnordered || headReversed || reversed) {
183                    highlight.add(head);
184                }
185                if (tailWays > 1 || tailUnordered || tailReversed || reversed) {
186                    highlight.add(tail);
187                }
188
189                errors.add(new TestError(this, Severity.ERROR, tr("Unordered coastline"),
190                        UNORDERED_COASTLINE, primitives, highlight));
191            } else if (reversed) {
192                errors.add(new TestError(this, Severity.ERROR, tr("Reversed coastline"),
193                        REVERSED_COASTLINE, primitives));
194            }
195        }
196
197        coastlines = null;
198        downloadedArea = null;
199
200        super.endTest();
201    }
202
203    @Override
204    public void visit(Way way) {
205        if (!way.isUsable())
206            return;
207
208        if (isCoastline(way)) {
209            coastlines.add(way);
210        }
211    }
212
213    private static boolean isCoastline(OsmPrimitive osm) {
214        return osm instanceof Way && "coastline".equals(osm.get("natural"));
215    }
216
217    @Override
218    public Command fixError(TestError testError) {
219        if (isFixable(testError)) {
220            // primitives list can be empty if all primitives have been purged
221            Iterator<? extends OsmPrimitive> it = testError.getPrimitives().iterator();
222            if (it.hasNext()) {
223                Way way = (Way) it.next();
224                Way newWay = new Way(way);
225
226                List<Node> nodesCopy = newWay.getNodes();
227                Collections.reverse(nodesCopy);
228                newWay.setNodes(nodesCopy);
229
230                return new ChangeCommand(way, newWay);
231            }
232        }
233        return null;
234    }
235
236    @Override
237    public boolean isFixable(TestError testError) {
238        if (testError.getTester() instanceof Coastlines)
239            return testError.getCode() == REVERSED_COASTLINE;
240
241        return false;
242    }
243}