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        return Arrays.stream(nodes).anyMatch(Node::isIncomplete);
592    }
593
594    /**
595     * Replies true if all nodes of the way have known lat/lon, false otherwise.
596     * @return true if all nodes of the way have known lat/lon, false otherwise
597     * @since 13033
598     */
599    public boolean hasOnlyLocatableNodes() {
600        return Arrays.stream(nodes).allMatch(Node::isLatLonKnown);
601    }
602
603    @Override
604    public boolean isUsable() {
605        return super.isUsable() && !hasIncompleteNodes();
606    }
607
608    @Override
609    public boolean isDrawable() {
610        return super.isDrawable() && hasOnlyLocatableNodes();
611    }
612
613    /**
614     * Replies the length of the way, in metres, as computed by {@link LatLon#greatCircleDistance}.
615     * @return The length of the way, in metres
616     * @since 4138
617     */
618    public double getLength() {
619        double length = 0;
620        Node lastN = null;
621        for (Node n:nodes) {
622            if (lastN != null) {
623                LatLon lastNcoor = lastN.getCoor();
624                LatLon coor = n.getCoor();
625                if (lastNcoor != null && coor != null) {
626                    length += coor.greatCircleDistance(lastNcoor);
627                }
628            }
629            lastN = n;
630        }
631        return length;
632    }
633
634    /**
635     * Replies the length of the longest segment of the way, in metres, as computed by {@link LatLon#greatCircleDistance}.
636     * @return The length of the segment, in metres
637     * @since 8320
638     */
639    public double getLongestSegmentLength() {
640        double length = 0;
641        Node lastN = null;
642        for (Node n:nodes) {
643            if (lastN != null) {
644                LatLon lastNcoor = lastN.getCoor();
645                LatLon coor = n.getCoor();
646                if (lastNcoor != null && coor != null) {
647                    double l = coor.greatCircleDistance(lastNcoor);
648                    if (l > length) {
649                        length = l;
650                    }
651                }
652            }
653            lastN = n;
654        }
655        return length;
656    }
657
658    /**
659     * Tests if this way is a oneway.
660     * @return {@code 1} if the way is a oneway,
661     *         {@code -1} if the way is a reversed oneway,
662     *         {@code 0} otherwise.
663     * @since 5199
664     */
665    public int isOneway() {
666        String oneway = get("oneway");
667        if (oneway != null) {
668            if ("-1".equals(oneway)) {
669                return -1;
670            } else {
671                Boolean isOneway = OsmUtils.getOsmBoolean(oneway);
672                if (isOneway != null && isOneway) {
673                    return 1;
674                }
675            }
676        }
677        return 0;
678    }
679
680    /**
681     * Replies the first node of this way, respecting or not its oneway state.
682     * @param respectOneway If true and if this way is a reversed oneway, replies the last node. Otherwise, replies the first node.
683     * @return the first node of this way, according to {@code respectOneway} and its oneway state.
684     * @since 5199
685     */
686    public Node firstNode(boolean respectOneway) {
687        return !respectOneway || isOneway() != -1 ? firstNode() : lastNode();
688    }
689
690    /**
691     * Replies the last node of this way, respecting or not its oneway state.
692     * @param respectOneway If true and if this way is a reversed oneway, replies the first node. Otherwise, replies the last node.
693     * @return the last node of this way, according to {@code respectOneway} and its oneway state.
694     * @since 5199
695     */
696    public Node lastNode(boolean respectOneway) {
697        return !respectOneway || isOneway() != -1 ? lastNode() : firstNode();
698    }
699
700    @Override
701    public boolean concernsArea() {
702        return hasAreaTags();
703    }
704
705    @Override
706    public boolean isOutsideDownloadArea() {
707        return Arrays.stream(nodes).anyMatch(Node::isOutsideDownloadArea);
708    }
709
710    @Override
711    protected void keysChangedImpl(Map<String, String> originalKeys) {
712        super.keysChangedImpl(originalKeys);
713        clearCachedNodeStyles();
714    }
715
716    /**
717     * Clears all cached styles for all nodes of this way. This should not be called from outside.
718     * @see Node#clearCachedStyle()
719     */
720    public void clearCachedNodeStyles() {
721        for (final Node n : nodes) {
722            n.clearCachedStyle();
723        }
724    }
725
726    /**
727     * Returns angles of vertices.
728     * @return angles of the way
729     * @since 13670
730     */
731    public synchronized List<Pair<Double, Node>> getAngles() {
732        List<Pair<Double, Node>> angles = new ArrayList<>();
733
734        for (int i = 1; i < nodes.length - 1; i++) {
735            Node n0 = nodes[i - 1];
736            Node n1 = nodes[i];
737            Node n2 = nodes[i + 1];
738
739            double angle = Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(
740                    n0.getEastNorth(), n1.getEastNorth(), n2.getEastNorth()));
741            angles.add(new Pair<>(angle, n1));
742        }
743
744        angles.add(new Pair<>(Geometry.getNormalizedAngleInDegrees(Geometry.getCornerAngle(
745                nodes[nodes.length - 2].getEastNorth(),
746                nodes[0].getEastNorth(),
747                nodes[1].getEastNorth())), nodes[0]));
748
749        return angles;
750    }
751}