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"), tr("This test checks that coastlines are correct."));
047    }
048
049    @Override
050    public void startTest(ProgressMonitor monitor) {
051
052        super.startTest(monitor);
053
054        OsmDataLayer layer = Main.getLayerManager().getEditLayer();
055
056        if (layer != null) {
057            downloadedArea = layer.data.getDataSourceArea();
058        }
059
060        coastlines = new LinkedList<>();
061    }
062
063    @Override
064    public void endTest() {
065        for (Way c1 : coastlines) {
066            Node head = c1.firstNode();
067            Node tail = c1.lastNode();
068
069            if (c1.getNodesCount() == 0 || head.equals(tail)) {
070                continue;
071            }
072
073            int headWays = 0;
074            int tailWays = 0;
075            boolean headReversed = false;
076            boolean tailReversed = false;
077            boolean headUnordered = false;
078            boolean tailUnordered = false;
079            Way next = null;
080            Way prev = null;
081
082            for (Way c2 : coastlines) {
083                if (c1 == c2) {
084                    continue;
085                }
086
087                if (c2.containsNode(head)) {
088                    headWays++;
089                    next = c2;
090
091                    if (head.equals(c2.firstNode())) {
092                        headReversed = true;
093                    } else if (!head.equals(c2.lastNode())) {
094                        headUnordered = true;
095                    }
096                }
097
098                if (c2.containsNode(tail)) {
099                    tailWays++;
100                    prev = c2;
101
102                    if (tail.equals(c2.lastNode())) {
103                        tailReversed = true;
104                    } else if (!tail.equals(c2.firstNode())) {
105                        tailUnordered = true;
106                    }
107                }
108            }
109
110            // To avoid false positives on upload (only modified primitives
111            // are visited), we have to check possible connection to ways
112            // that are not in the set of validated primitives.
113            if (headWays == 0) {
114                Collection<OsmPrimitive> refs = head.getReferrers();
115                for (OsmPrimitive ref : refs) {
116                    if (ref != c1 && isCoastline(ref)) {
117                        // ref cannot be in <code>coastlines</code>, otherwise we would
118                        // have picked it up already
119                        headWays++;
120                        next = (Way) ref;
121
122                        if (head.equals(next.firstNode())) {
123                            headReversed = true;
124                        } else if (!head.equals(next.lastNode())) {
125                            headUnordered = true;
126                        }
127                    }
128                }
129            }
130            if (tailWays == 0) {
131                Collection<OsmPrimitive> refs = tail.getReferrers();
132                for (OsmPrimitive ref : refs) {
133                    if (ref != c1 && isCoastline(ref)) {
134                        tailWays++;
135                        prev = (Way) ref;
136
137                        if (tail.equals(prev.lastNode())) {
138                            tailReversed = true;
139                        } else if (!tail.equals(prev.firstNode())) {
140                            tailUnordered = true;
141                        }
142                    }
143                }
144            }
145
146            List<OsmPrimitive> primitives = new ArrayList<>();
147            primitives.add(c1);
148
149            if (headWays == 0 || tailWays == 0) {
150                List<OsmPrimitive> highlight = new ArrayList<>();
151
152                if (headWays == 0 && head.getCoor().isIn(downloadedArea)) {
153                    highlight.add(head);
154                }
155                if (tailWays == 0 && tail.getCoor().isIn(downloadedArea)) {
156                    highlight.add(tail);
157                }
158
159                if (!highlight.isEmpty()) {
160                    errors.add(new TestError(this, Severity.ERROR, tr("Unconnected coastline"),
161                            UNCONNECTED_COASTLINE, primitives, highlight));
162                }
163            }
164
165            boolean unordered = false;
166            boolean reversed = headWays == 1 && headReversed && tailWays == 1 && tailReversed;
167
168            if (headWays > 1 || tailWays > 1) {
169                unordered = true;
170            } else if (headUnordered || tailUnordered) {
171                unordered = true;
172            } else if (reversed && next == prev) {
173                unordered = true;
174            } else if ((headReversed || tailReversed) && headReversed != tailReversed) {
175                unordered = true;
176            }
177
178            if (unordered) {
179                List<OsmPrimitive> highlight = new ArrayList<>();
180
181                if (headWays > 1 || headUnordered || headReversed || reversed) {
182                    highlight.add(head);
183                }
184                if (tailWays > 1 || tailUnordered || tailReversed || reversed) {
185                    highlight.add(tail);
186                }
187
188                errors.add(new TestError(this, Severity.ERROR, tr("Unordered coastline"),
189                        UNORDERED_COASTLINE, primitives, highlight));
190            } else if (reversed) {
191                errors.add(new TestError(this, Severity.ERROR, tr("Reversed coastline"),
192                        REVERSED_COASTLINE, primitives));
193            }
194        }
195
196        coastlines = null;
197        downloadedArea = null;
198
199        super.endTest();
200    }
201
202    @Override
203    public void visit(Way way) {
204        if (!way.isUsable())
205            return;
206
207        if (isCoastline(way)) {
208            coastlines.add(way);
209        }
210    }
211
212    private static boolean isCoastline(OsmPrimitive osm) {
213        return osm instanceof Way && "coastline".equals(osm.get("natural"));
214    }
215
216    @Override
217    public Command fixError(TestError testError) {
218        if (isFixable(testError)) {
219            // primitives list can be empty if all primitives have been purged
220            Iterator<? extends OsmPrimitive> it = testError.getPrimitives().iterator();
221            if (it.hasNext()) {
222                Way way = (Way) it.next();
223                Way newWay = new Way(way);
224
225                List<Node> nodesCopy = newWay.getNodes();
226                Collections.reverse(nodesCopy);
227                newWay.setNodes(nodesCopy);
228
229                return new ChangeCommand(way, newWay);
230            }
231        }
232        return null;
233    }
234
235    @Override
236    public boolean isFixable(TestError testError) {
237        if (testError.getTester() instanceof Coastlines)
238            return testError.getCode() == REVERSED_COASTLINE;
239
240        return false;
241    }
242}