001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.HashSet;
009import java.util.List;
010import java.util.Map;
011import java.util.Set;
012import java.util.stream.Collectors;
013
014import org.openstreetmap.josm.data.coor.LatLon;
015import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
016import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
017import org.openstreetmap.josm.spi.preferences.Config;
018import org.openstreetmap.josm.tools.CopyList;
019import org.openstreetmap.josm.tools.Geometry;
020import org.openstreetmap.josm.tools.Pair;
021import org.openstreetmap.josm.tools.Utils;
022
023/**
024 * One full way, consisting of a list of way {@link Node nodes}.
025 *
026 * @author imi
027 * @since 64
028 */
029public final class Way extends OsmPrimitive implements IWay<Node> {
030
031    /**
032     * All way nodes in this way
033     */
034    private Node[] nodes = new Node[0];
035    private BBox bbox;
036
037    @Override
038    public List<Node> getNodes() {
039        return new CopyList<>(nodes);
040    }
041
042    @Override
043    public void setNodes(List<Node> nodes) {
044        checkDatasetNotReadOnly();
045        boolean locked = writeLock();
046        try {
047            for (Node node:this.nodes) {
048                node.removeReferrer(this);
049                node.clearCachedStyle();
050            }
051
052            if (nodes == null) {
053                this.nodes = new Node[0];
054            } else {
055                this.nodes = nodes.toArray(new Node[0]);
056            }
057            for (Node node: this.nodes) {
058                node.addReferrer(this);
059                node.clearCachedStyle();
060            }
061
062            clearCachedStyle();
063            fireNodesChanged();
064        } finally {
065            writeUnlock(locked);
066        }
067    }
068
069    /**
070     * Prevent directly following identical nodes in ways.
071     * @param nodes list of nodes
072     * @return {@code nodes} with consecutive identical nodes removed
073     */
074    private static List<Node> removeDouble(List<Node> nodes) {
075        Node last = null;
076        int count = nodes.size();
077        for (int i = 0; i < count && count > 2;) {
078            Node n = nodes.get(i);
079            if (last == n) {
080                nodes.remove(i);
081                --count;
082            } else {
083                last = n;
084                ++i;
085            }
086        }
087        return nodes;
088    }
089
090    @Override
091    public int getNodesCount() {
092        return nodes.length;
093    }
094
095    @Override
096    public Node getNode(int index) {
097        return nodes[index];
098    }
099
100    @Override
101    public long getNodeId(int idx) {
102        return nodes[idx].getUniqueId();
103    }
104
105    @Override
106    public List<Long> getNodeIds() {
107        return Arrays.stream(nodes).map(Node::getId).collect(Collectors.toList());
108    }
109
110    /**
111     * Replies true if this way contains the node <code>node</code>, false
112     * otherwise. Replies false if  <code>node</code> is null.
113     *
114     * @param node the node. May be null.
115     * @return true if this way contains the node <code>node</code>, false
116     * otherwise
117     * @since 1911
118     */
119    public boolean containsNode(Node node) {
120        if (node == null) return false;
121
122        Node[] nodes = this.nodes;
123        for (Node n : nodes) {
124            if (n.equals(node))
125                return true;
126        }
127        return false;
128    }
129
130    /**
131     * Return nodes adjacent to <code>node</code>
132     *
133     * @param node the node. May be null.
134     * @return Set of nodes adjacent to <code>node</code>
135     * @since 4671
136     */
137    public Set<Node> getNeighbours(Node node) {
138        Set<Node> neigh = new HashSet<>();
139
140        if (node == null) return neigh;
141
142        Node[] nodes = this.nodes;
143        for (int i = 0; i < nodes.length; i++) {
144            if (nodes[i].equals(node)) {
145                if (i > 0)
146                    neigh.add(nodes[i-1]);
147                if (i < nodes.length-1)
148                    neigh.add(nodes[i+1]);
149            }
150        }
151        return neigh;
152    }
153
154    /**
155     * Replies the ordered {@link List} of chunks of this way. Each chunk is replied as a {@link Pair} of {@link Node nodes}.
156     * @param sort If true, the nodes of each pair are sorted as defined by {@link Pair#sort}.
157     *             If false, Pair.a and Pair.b are in the way order
158     *             (i.e for a given Pair(n), Pair(n-1).b == Pair(n).a, Pair(n).b == Pair(n+1).a, etc.)
159     * @return The ordered list of chunks of this way.
160     * @since 3348
161     */
162    public List<Pair<Node, Node>> getNodePairs(boolean sort) {
163        List<Pair<Node, Node>> chunkSet = new ArrayList<>();
164        if (isIncomplete()) return chunkSet;
165        Node lastN = null;
166        Node[] nodes = this.nodes;
167        for (Node n : nodes) {
168            if (lastN == null) {
169                lastN = n;
170                continue;
171            }
172            Pair<Node, Node> np = new Pair<>(lastN, n);
173            if (sort) {
174                Pair.sort(np);
175            }
176            chunkSet.add(np);
177            lastN = n;
178        }
179        return chunkSet;
180    }
181
182    @Override public void accept(OsmPrimitiveVisitor visitor) {
183        visitor.visit(this);
184    }
185
186    @Override public void accept(PrimitiveVisitor visitor) {
187        visitor.visit(this);
188    }
189
190    protected Way(long id, boolean allowNegative) {
191        super(id, allowNegative);
192    }
193
194    /**
195     * Contructs a new {@code Way} with id 0.
196     * @since 86
197     */
198    public Way() {
199        super(0, false);
200    }
201
202    /**
203     * Contructs a new {@code Way} from an existing {@code Way}.
204     * @param original The original {@code Way} to be identically cloned. Must not be null
205     * @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}.
206     * If {@code false}, does nothing
207     * @since 2410
208     */
209    public Way(Way original, boolean clearMetadata) {
210        super(original.getUniqueId(), true);
211        cloneFrom(original);
212        if (clearMetadata) {
213            clearOsmMetadata();
214        }
215    }
216
217    /**
218     * Contructs a new {@code Way} from an existing {@code Way} (including its id).
219     * @param original The original {@code Way} to be identically cloned. Must not be null
220     * @since 86
221     */
222    public Way(Way original) {
223        this(original, false);
224    }
225
226    /**
227     * Contructs a new {@code Way} for the given id. If the id &gt; 0, the way is marked
228     * as incomplete. If id == 0 then way is marked as new
229     *
230     * @param id the id. &gt;= 0 required
231     * @throws IllegalArgumentException if id &lt; 0
232     * @since 343
233     */
234    public Way(long id) {
235        super(id, false);
236    }
237
238    /**
239     * Contructs a new {@code Way} with given id and version.
240     * @param id the id. &gt;= 0 required
241     * @param version the version
242     * @throws IllegalArgumentException if id &lt; 0
243     * @since 2620
244     */
245    public Way(long id, int version) {
246        super(id, version, false);
247    }
248
249    @Override
250    public void load(PrimitiveData data) {
251        if (!(data instanceof WayData))
252            throw new IllegalArgumentException("Not a way data: " + data);
253        boolean locked = writeLock();
254        try {
255            super.load(data);
256
257            List<Long> nodeIds = ((WayData) data).getNodeIds();
258
259            if (!nodeIds.isEmpty() && getDataSet() == null) {
260                throw new AssertionError("Data consistency problem - way without dataset detected");
261            }
262
263            List<Node> newNodes = new ArrayList<>(nodeIds.size());
264            for (Long nodeId : nodeIds) {
265                Node node = (Node) getDataSet().getPrimitiveById(nodeId, OsmPrimitiveType.NODE);
266                if (node != null) {
267                    newNodes.add(node);
268                } else {
269                    throw new AssertionError("Data consistency problem - way with missing node detected");
270                }
271            }
272            setNodes(newNodes);
273        } finally {
274            writeUnlock(locked);
275        }
276    }
277
278    @Override
279    public WayData save() {
280        WayData data = new WayData();
281        saveCommonAttributes(data);
282        for (Node node:nodes) {
283            data.getNodeIds().add(node.getUniqueId());
284        }
285        return data;
286    }
287
288    @Override
289    public void cloneFrom(OsmPrimitive osm) {
290        if (!(osm instanceof Way))
291            throw new IllegalArgumentException("Not a way: " + osm);
292        boolean locked = writeLock();
293        try {
294            super.cloneFrom(osm);
295            Way otherWay = (Way) osm;
296            setNodes(otherWay.getNodes());
297        } finally {
298            writeUnlock(locked);
299        }
300    }
301
302    @Override
303    public String toString() {
304        String nodesDesc = isIncomplete() ? "(incomplete)" : ("nodes=" + Arrays.toString(nodes));
305        return "{Way id=" + getUniqueId() + " version=" + getVersion()+ ' ' + getFlagsAsString() + ' ' + nodesDesc + '}';
306    }
307
308    @Override
309    public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) {
310        if (!(other instanceof Way))
311            return false;
312        Way w = (Way) other;
313        if (getNodesCount() != w.getNodesCount()) return false;
314        if (!super.hasEqualSemanticAttributes(other, testInterestingTagsOnly))
315            return false;
316        for (int i = 0; i < getNodesCount(); i++) {
317            if (!getNode(i).hasEqualSemanticAttributes(w.getNode(i)))
318                return false;
319        }
320        return true;
321    }
322
323    /**
324     * Removes the given {@link Node} from this way. Ignored, if n is null.
325     * @param n The node to remove. Ignored, if null
326     * @since 1463
327     */
328    public void removeNode(Node n) {
329        checkDatasetNotReadOnly();
330        if (n == null || isIncomplete()) return;
331        boolean locked = writeLock();
332        try {
333            boolean closed = lastNode() == n && firstNode() == n;
334            int i;
335            List<Node> copy = getNodes();
336            while ((i = copy.indexOf(n)) >= 0) {
337                copy.remove(i);
338            }
339            i = copy.size();
340            if (closed && i > 2) {
341                copy.add(copy.get(0));
342            } else if (i >= 2 && i <= 3 && copy.get(0) == copy.get(i-1)) {
343                copy.remove(i-1);
344            }
345            setNodes(removeDouble(copy));
346            n.clearCachedStyle();
347        } finally {
348            writeUnlock(locked);
349        }
350    }
351
352    /**
353     * Removes the given set of {@link Node nodes} from this way. Ignored, if selection is null.
354     * @param selection The selection of nodes to remove. Ignored, if null
355     * @since 5408
356     */
357    public void removeNodes(Set<? extends Node> selection) {
358        checkDatasetNotReadOnly();
359        if (selection == null || isIncomplete()) return;
360        boolean locked = writeLock();
361        try {
362            boolean closed = isClosed() && selection.contains(lastNode());
363            List<Node> copy = new ArrayList<>();
364
365            for (Node n: nodes) {
366                if (!selection.contains(n)) {
367                    copy.add(n);
368                }
369            }
370
371            int i = copy.size();
372            if (closed && i > 2) {
373                copy.add(copy.get(0));
374            } else if (i >= 2 && i <= 3 && copy.get(0) == copy.get(i-1)) {
375                copy.remove(i-1);
376            }
377            setNodes(removeDouble(copy));
378            for (Node n : selection) {
379                n.clearCachedStyle();
380            }
381        } finally {
382            writeUnlock(locked);
383        }
384    }
385
386    /**
387     * Adds a node to the end of the list of nodes. Ignored, if n is null.
388     *
389     * @param n the node. Ignored, if null
390     * @throws IllegalStateException if this way is marked as incomplete. We can't add a node
391     * to an incomplete way
392     * @since 1313
393     */
394    public void addNode(Node n) {
395        checkDatasetNotReadOnly();
396        if (n == null) return;
397
398        boolean locked = writeLock();
399        try {
400            if (isIncomplete())
401                throw new IllegalStateException(tr("Cannot add node {0} to incomplete way {1}.", n.getId(), getId()));
402            clearCachedStyle();
403            n.addReferrer(this);
404            nodes = Utils.addInArrayCopy(nodes, n);
405            n.clearCachedStyle();
406            fireNodesChanged();
407        } finally {
408            writeUnlock(locked);
409        }
410    }
411
412    /**
413     * Adds a node at position offs.
414     *
415     * @param offs the offset
416     * @param n the node. Ignored, if null.
417     * @throws IllegalStateException if this way is marked as incomplete. We can't add a node
418     * to an incomplete way
419     * @throws IndexOutOfBoundsException if offs is out of bounds
420     * @since 1313
421     */
422    public void addNode(int offs, Node n) {
423        checkDatasetNotReadOnly();
424        if (n == null) return;
425
426        boolean locked = writeLock();
427        try {
428            if (isIncomplete())
429                throw new IllegalStateException(tr("Cannot add node {0} to incomplete way {1}.", n.getId(), getId()));
430
431            clearCachedStyle();
432            n.addReferrer(this);
433            Node[] newNodes = new Node[nodes.length + 1];
434            System.arraycopy(nodes, 0, newNodes, 0, offs);
435            System.arraycopy(nodes, offs, newNodes, offs + 1, nodes.length - offs);
436            newNodes[offs] = n;
437            nodes = newNodes;
438            n.clearCachedStyle();
439            fireNodesChanged();
440        } finally {
441            writeUnlock(locked);
442        }
443    }
444
445    @Override
446    public void setDeleted(boolean deleted) {
447        boolean locked = writeLock();
448        try {
449            for (Node n:nodes) {
450                if (deleted) {
451                    n.removeReferrer(this);
452                } else {
453                    n.addReferrer(this);
454                }
455                n.clearCachedStyle();
456            }
457            fireNodesChanged();
458            super.setDeleted(deleted);
459        } finally {
460            writeUnlock(locked);
461        }
462    }
463
464    @Override
465    public boolean isClosed() {
466        if (isIncomplete()) return false;
467
468        Node[] nodes = this.nodes;
469        return nodes.length >= 3 && nodes[nodes.length-1] == nodes[0];
470    }
471
472    /**
473     * Determines if this way denotes an area (closed way with at least three distinct nodes).
474     * @return {@code true} if this way is closed and contains at least three distinct nodes
475     * @see #isClosed
476     * @since 5490
477     */
478    public boolean isArea() {
479        if (this.nodes.length >= 4 && isClosed()) {
480            Node distinctNode = null;
481            for (int i = 1; i < nodes.length-1; i++) {
482                if (distinctNode == null && nodes[i] != nodes[0]) {
483                    distinctNode = nodes[i];
484                } else if (distinctNode != null && nodes[i] != nodes[0] && nodes[i] != distinctNode) {
485                    return true;
486                }
487            }
488        }
489        return false;
490    }
491
492    @Override
493    public Node lastNode() {
494        Node[] nodes = this.nodes;
495        if (isIncomplete() || nodes.length == 0) return null;
496        return nodes[nodes.length-1];
497    }
498
499    @Override
500    public Node firstNode() {
501        Node[] nodes = this.nodes;
502        if (isIncomplete() || nodes.length == 0) return null;
503        return nodes[0];
504    }
505
506    @Override
507    public boolean isFirstLastNode(INode n) {
508        Node[] nodes = this.nodes;
509        if (isIncomplete() || nodes.length == 0) return false;
510        return n == nodes[0] || n == nodes[nodes.length -1];
511    }
512
513    @Override
514    public boolean isInnerNode(INode n) {
515        Node[] nodes = this.nodes;
516        if (isIncomplete() || nodes.length <= 2) return false;
517        /* circular ways have only inner nodes, so return true for them! */
518        if (n == nodes[0] && n == nodes[nodes.length-1]) return true;
519        for (int i = 1; i < nodes.length - 1; ++i) {
520            if (nodes[i] == n) return true;
521        }
522        return false;
523    }
524
525    @Override
526    public OsmPrimitiveType getType() {
527        return OsmPrimitiveType.WAY;
528    }
529
530    @Override
531    public OsmPrimitiveType getDisplayType() {
532        return isClosed() ? OsmPrimitiveType.CLOSEDWAY : OsmPrimitiveType.WAY;
533    }
534
535    private void checkNodes() {
536        DataSet dataSet = getDataSet();
537        if (dataSet != null) {
538            Node[] nodes = this.nodes;
539            for (Node n: nodes) {
540                if (n.getDataSet() != dataSet)
541                    throw new DataIntegrityProblemException("Nodes in way must be in the same dataset",
542                            tr("Nodes in way must be in the same dataset"));
543                if (n.isDeleted())
544                    throw new DataIntegrityProblemException("Deleted node referenced: " + toString(),
545                            "<html>" + tr("Deleted node referenced by {0}",
546                                    DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(this)) + "</html>");
547            }
548            if (Config.getPref().getBoolean("debug.checkNullCoor", true)) {
549                for (Node n: nodes) {
550                    if (n.isVisible() && !n.isIncomplete() && !n.isLatLonKnown())
551                        throw new DataIntegrityProblemException("Complete visible node with null coordinates: " + toString(),
552                                "<html>" + tr("Complete node {0} with null coordinates in way {1}",
553                                DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(n),
554                                DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(this)) + "</html>");
555                }
556            }
557        }
558    }
559
560    private void fireNodesChanged() {
561        checkNodes();
562        if (getDataSet() != null) {
563            getDataSet().fireWayNodesChanged(this);
564        }
565    }
566
567    @Override
568    void setDataset(DataSet dataSet) {
569        super.setDataset(dataSet);
570        checkNodes();
571    }
572
573    @Override
574    public BBox getBBox() {
575        if (getDataSet() == null)
576            return new BBox(this);
577        if (bbox == null) {
578            bbox = new BBox(this);
579        }
580        return new BBox(bbox);
581    }
582
583    @Override
584    protected void addToBBox(BBox box, Set<PrimitiveId> visited) {
585        box.add(getBBox());
586    }
587
588    @Override
589    public void updatePosition() {
590        bbox = new BBox(this);
591        clearCachedStyle();
592    }
593
594    /**
595     * Replies true if this way has incomplete nodes, false otherwise.
596     * @return true if this way has incomplete nodes, false otherwise.
597     * @since 2587
598     */
599    public boolean hasIncompleteNodes() {
600        Node[] nodes = this.nodes;
601        for (Node node : nodes) {
602            if (node.isIncomplete())
603                return true;
604        }
605        return false;
606    }
607
608    /**
609     * Replies true if all nodes of the way have known lat/lon, false otherwise.
610     * @return true if all nodes of the way have known lat/lon, false otherwise
611     * @since 13033
612     */
613    public boolean hasOnlyLocatableNodes() {
614        Node[] nodes = this.nodes;
615        for (Node node : nodes) {
616            if (!node.isLatLonKnown())
617                return false;
618        }
619        return true;
620    }
621
622    @Override
623    public boolean isUsable() {
624        return super.isUsable() && !hasIncompleteNodes();
625    }
626
627    @Override
628    public boolean isDrawable() {
629        return super.isDrawable() && hasOnlyLocatableNodes();
630    }
631
632    /**
633     * Replies the length of the way, in metres, as computed by {@link LatLon#greatCircleDistance}.
634     * @return The length of the way, in metres
635     * @since 4138
636     */
637    public double getLength() {
638        double length = 0;
639        Node lastN = null;
640        for (Node n:nodes) {
641            if (lastN != null) {
642                LatLon lastNcoor = lastN.getCoor();
643                LatLon coor = n.getCoor();
644                if (lastNcoor != null && coor != null) {
645                    length += coor.greatCircleDistance(lastNcoor);
646                }
647            }
648            lastN = n;
649        }
650        return length;
651    }
652
653    /**
654     * Replies the length of the longest segment of the way, in metres, as computed by {@link LatLon#greatCircleDistance}.
655     * @return The length of the segment, in metres
656     * @since 8320
657     */
658    public double getLongestSegmentLength() {
659        double length = 0;
660        Node lastN = null;
661        for (Node n:nodes) {
662            if (lastN != null) {
663                LatLon lastNcoor = lastN.getCoor();
664                LatLon coor = n.getCoor();
665                if (lastNcoor != null && coor != null) {
666                    double l = coor.greatCircleDistance(lastNcoor);
667                    if (l > length) {
668                        length = l;
669                    }
670                }
671            }
672            lastN = n;
673        }
674        return length;
675    }
676
677    /**
678     * Tests if this way is a oneway.
679     * @return {@code 1} if the way is a oneway,
680     *         {@code -1} if the way is a reversed oneway,
681     *         {@code 0} otherwise.
682     * @since 5199
683     */
684    public int isOneway() {
685        String oneway = get("oneway");
686        if (oneway != null) {
687            if ("-1".equals(oneway)) {
688                return -1;
689            } else {
690                Boolean isOneway = OsmUtils.getOsmBoolean(oneway);
691                if (isOneway != null && isOneway) {
692                    return 1;
693                }
694            }
695        }
696        return 0;
697    }
698
699    /**
700     * Replies the first node of this way, respecting or not its oneway state.
701     * @param respectOneway If true and if this way is a reversed oneway, replies the last node. Otherwise, replies the first node.
702     * @return the first node of this way, according to {@code respectOneway} and its oneway state.
703     * @since 5199
704     */
705    public Node firstNode(boolean respectOneway) {
706        return !respectOneway || isOneway() != -1 ? firstNode() : lastNode();
707    }
708
709    /**
710     * Replies the last node of this way, respecting or not its oneway state.
711     * @param respectOneway If true and if this way is a reversed oneway, replies the first node. Otherwise, replies the last node.
712     * @return the last node of this way, according to {@code respectOneway} and its oneway state.
713     * @since 5199
714     */
715    public Node lastNode(boolean respectOneway) {
716        return !respectOneway || isOneway() != -1 ? lastNode() : firstNode();
717    }
718
719    @Override
720    public boolean concernsArea() {
721        return hasAreaTags();
722    }
723
724    @Override
725    public boolean isOutsideDownloadArea() {
726        for (final Node n : nodes) {
727            if (n.isOutsideDownloadArea()) {
728                return true;
729            }
730        }
731        return false;
732    }
733
734    @Override
735    protected void keysChangedImpl(Map<String, String> originalKeys) {
736        super.keysChangedImpl(originalKeys);
737        clearCachedNodeStyles();
738    }
739
740    /**
741     * Clears all cached styles for all nodes of this way. This should not be called from outside.
742     * @see Node#clearCachedStyle()
743     */
744    public void clearCachedNodeStyles() {
745        for (final Node n : nodes) {
746            n.clearCachedStyle();
747        }
748    }
749
750    /**
751     * Returns angles of vertices.
752     * @return angles of the way
753     * @since 13670
754     */
755    public synchronized List<Pair<Double, Node>> getAngles() {
756        List<Pair<Double, Node>> angles = new ArrayList<>();
757
758        for (int i = 1; i < nodes.length - 1; i++) {
759            Node n0 = nodes[i - 1];
760            Node n1 = nodes[i];
761            Node n2 = nodes[i + 1];
762
763            double angle = Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(
764                    n0.getEastNorth(), n1.getEastNorth(), n2.getEastNorth()));
765            angles.add(new Pair<>(angle, n1));
766        }
767
768        angles.add(new Pair<>(Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(
769                nodes[nodes.length - 2].getEastNorth(),
770                nodes[0].getEastNorth(),
771                nodes[1].getEastNorth())), nodes[0]));
772
773        return angles;
774    }
775}