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