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.HashSet;
011import java.util.Iterator;
012import java.util.LinkedList;
013import java.util.List;
014import java.util.Set;
015
016import org.openstreetmap.josm.command.ChangeCommand;
017import org.openstreetmap.josm.command.Command;
018import org.openstreetmap.josm.data.osm.Node;
019import org.openstreetmap.josm.data.osm.OsmPrimitive;
020import org.openstreetmap.josm.data.osm.Way;
021import org.openstreetmap.josm.data.validation.Severity;
022import org.openstreetmap.josm.data.validation.Test;
023import org.openstreetmap.josm.data.validation.TestError;
024import org.openstreetmap.josm.gui.progress.ProgressMonitor;
025import org.openstreetmap.josm.tools.Geometry;
026
027/**
028 * Check coastlines for errors
029 *
030 * @author frsantos
031 * @author Teemu Koskinen
032 * @author Gerd Petermann
033 */
034public class Coastlines extends Test {
035
036    protected static final int UNORDERED_COASTLINE = 901;
037    protected static final int REVERSED_COASTLINE = 902;
038    protected static final int UNCONNECTED_COASTLINE = 903;
039    protected static final int WRONG_ORDER_COASTLINE = 904;
040
041    private List<Way> coastlineWays;
042
043    /**
044     * Constructor
045     */
046    public Coastlines() {
047        super(tr("Coastlines"), tr("This test checks that coastlines are correct."));
048    }
049
050    @Override
051    public void startTest(ProgressMonitor monitor) {
052        super.startTest(monitor);
053        coastlineWays = new LinkedList<>();
054    }
055
056    @Override
057    public void endTest() {
058        checkConnections();
059        checkDirection();
060        coastlineWays = null;
061        super.endTest();
062    }
063
064    /**
065     * Check connections between coastline ways.
066     * The nodes of a coastline way have to fulfil these rules:
067     * 1) the first node must be connected to the last node of a coastline way (which might be the same way)
068     * 2) the last node must be connected to the first node of a coastline way (which might be the same way)
069     * 3) all other nodes must not be connected to a coastline way
070     * 4) the number of referencing coastline ways must be 1 or 2
071     * Nodes outside the download area are special cases, we may not know enough about the connected ways.
072     */
073    private void checkConnections() {
074        Area downloadedArea = null;
075        for (Way w : coastlineWays) {
076            if (downloadedArea == null) {
077                downloadedArea = w.getDataSet().getDataSourceArea();
078            }
079            int numNodes = w.getNodesCount();
080            for (int i = 0; i < numNodes; i++) {
081                Node n = w.getNode(i);
082                List<OsmPrimitive> refs = n.getReferrers();
083                Set<Way> connectedWays = new HashSet<>();
084                for (OsmPrimitive p : refs) {
085                    if (p != w && isCoastline(p)) {
086                        connectedWays.add((Way) p);
087                    }
088                }
089                if (i == 0) {
090                    if (connectedWays.isEmpty() && n != w.lastNode() && n.getCoor().isIn(downloadedArea)) {
091                        addError(UNCONNECTED_COASTLINE, w, null, n);
092                    }
093                    if (connectedWays.size() == 1 && n != connectedWays.iterator().next().lastNode()) {
094                        checkIfReversed(w, connectedWays.iterator().next(), n);
095                    }
096                    if (connectedWays.size() == 1 && w.isClosed() && connectedWays.iterator().next().isClosed()) {
097                        addError(UNORDERED_COASTLINE, w, connectedWays, n);
098                    }
099                } else if (i == numNodes - 1) {
100                    if (connectedWays.isEmpty() && n != w.firstNode() && n.getCoor().isIn(downloadedArea)) {
101                        addError(UNCONNECTED_COASTLINE, w, null, n);
102                    }
103                    if (connectedWays.size() == 1 && n != connectedWays.iterator().next().firstNode()) {
104                        checkIfReversed(w, connectedWays.iterator().next(), n);
105                    }
106                } else if (!connectedWays.isEmpty()) {
107                    addError(UNORDERED_COASTLINE, w, connectedWays, n);
108                }
109            }
110        }
111    }
112
113    /**
114     * Check if two or more coastline ways form a closed clockwise way
115     */
116    private void checkDirection() {
117        HashSet<Way> done = new HashSet<>();
118        for (Way w : coastlineWays) {
119            if (done.contains(w))
120                continue;
121            List<Way> visited = new ArrayList<>();
122            done.add(w);
123            visited.add(w);
124            List<Node> nodes = new ArrayList<>(w.getNodes());
125            Way curr = w;
126            while (nodes.get(0) != nodes.get(nodes.size()-1)) {
127                boolean foundContinuation = false;
128                for (OsmPrimitive p : curr.lastNode().getReferrers()) {
129                    if (p != curr && isCoastline(p)) {
130                        Way other = (Way) p;
131                        if (done.contains(other))
132                            continue;
133                        if (other.firstNode() == curr.lastNode()) {
134                            foundContinuation = true;
135                            curr = other;
136                            done.add(curr);
137                            visited.add(curr);
138                            nodes.remove(nodes.size()-1); // remove duplicate connection node
139                            nodes.addAll(curr.getNodes());
140                            break;
141                        }
142                    }
143                }
144                if (!foundContinuation)
145                    break;
146            }
147            // simple closed ways are reported by WronglyOrderedWays
148            if (visited.size() > 1 && nodes.get(0) == nodes.get(nodes.size()-1) && Geometry.isClockwise(nodes)) {
149                errors.add(TestError.builder(this, Severity.WARNING, WRONG_ORDER_COASTLINE)
150                        .message(tr("Reversed coastline: land not on left side"))
151                        .primitives(visited)
152                        .build());
153            }
154        }
155    }
156
157    /**
158     * Check if a reversed way would fit, if yes, add fixable "reversed" error, "unordered" else
159     * @param w way that might be reversed
160     * @param other other way that is connected to w
161     * @param n1 node at which w and other are connected
162     */
163    private void checkIfReversed(Way w, Way other, Node n1) {
164        boolean headFixedWithReverse = false;
165        boolean tailFixedWithReverse = false;
166        int otherCoastlineWays = 0;
167        Way connectedToFirst = null;
168        for (int i = 0; i < 2; i++) {
169            Node n = (i == 0) ? w.firstNode() : w.lastNode();
170            List<OsmPrimitive> refs = n.getReferrers();
171            for (OsmPrimitive p : refs) {
172                if (p != w && isCoastline(p)) {
173                    Way cw = (Way) p;
174                    if (i == 0 && cw.firstNode() == n) {
175                        headFixedWithReverse = true;
176                        connectedToFirst = cw;
177                    } else if (i == 1 && cw.lastNode() == n) {
178                        if (cw != connectedToFirst)
179                            tailFixedWithReverse = true;
180                    } else
181                        otherCoastlineWays++;
182                }
183            }
184        }
185        if (otherCoastlineWays == 0 && headFixedWithReverse && tailFixedWithReverse)
186            addError(REVERSED_COASTLINE, w, null, null);
187        else
188            addError(UNORDERED_COASTLINE, w, Collections.singletonList(other), n1);
189    }
190
191    /**
192     * Add error if not already done
193     * @param errCode the error code
194     * @param w the way that is in error
195     * @param otherWays collection of other ways in error or null
196     * @param n the node to be highlighted or null
197     */
198    private void addError(int errCode, Way w, Collection<Way> otherWays, Node n) {
199        String msg;
200        switch (errCode) {
201        case UNCONNECTED_COASTLINE:
202            msg = tr("Unconnected coastline");
203            break;
204        case UNORDERED_COASTLINE:
205            msg = tr("Unordered coastline");
206            break;
207        case REVERSED_COASTLINE:
208            msg = tr("Reversed coastline");
209            break;
210        default:
211            msg = tr("invalid coastline"); // should not happen
212        }
213        Set<OsmPrimitive> primitives = new HashSet<>();
214        primitives.add(w);
215        if (otherWays != null)
216            primitives.addAll(otherWays);
217        // check for repeated error
218        for (TestError e : errors) {
219            if (e.getCode() != errCode)
220                continue;
221            if (errCode != REVERSED_COASTLINE && !e.getHighlighted().contains(n))
222                continue;
223            if (e.getPrimitives().size() != primitives.size())
224                continue;
225            if (!e.getPrimitives().containsAll(primitives))
226                continue;
227            return; // we already know this error
228        }
229        if (errCode != REVERSED_COASTLINE)
230            errors.add(TestError.builder(this, Severity.ERROR, errCode)
231                    .message(msg)
232                    .primitives(primitives)
233                    .highlight(n)
234                    .build());
235        else
236            errors.add(TestError.builder(this, Severity.ERROR, errCode)
237                    .message(msg)
238                    .primitives(primitives)
239                    .build());
240    }
241
242    @Override
243    public void visit(Way way) {
244        if (!way.isUsable())
245            return;
246
247        if (isCoastline(way)) {
248            coastlineWays.add(way);
249        }
250    }
251
252    private static boolean isCoastline(OsmPrimitive osm) {
253        return osm instanceof Way && osm.hasTag("natural", "coastline");
254    }
255
256    @Override
257    public Command fixError(TestError testError) {
258        if (isFixable(testError)) {
259            // primitives list can be empty if all primitives have been purged
260            Iterator<? extends OsmPrimitive> it = testError.getPrimitives().iterator();
261            if (it.hasNext()) {
262                Way way = (Way) it.next();
263                Way newWay = new Way(way);
264
265                List<Node> nodesCopy = newWay.getNodes();
266                Collections.reverse(nodesCopy);
267                newWay.setNodes(nodesCopy);
268
269                return new ChangeCommand(way, newWay);
270            }
271        }
272        return null;
273    }
274
275    @Override
276    public boolean isFixable(TestError testError) {
277        if (testError.getTester() instanceof Coastlines)
278            return testError.getCode() == REVERSED_COASTLINE;
279
280        return false;
281    }
282}