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