001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.data.osm;
003    
004    import org.openstreetmap.josm.Main;
005    import org.openstreetmap.josm.data.coor.EastNorth;
006    import org.openstreetmap.josm.data.coor.LatLon;
007    import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
008    import org.openstreetmap.josm.data.osm.visitor.Visitor;
009    import org.openstreetmap.josm.data.projection.Projections;
010    
011    /**
012     * One node data, consisting of one world coordinate waypoint.
013     *
014     * @author imi
015     */
016    public final class Node extends OsmPrimitive implements INode {
017    
018        /*
019         * We "inline" lat/lon rather than using a LatLon-object => reduces memory footprint
020         */
021        private double lat = Double.NaN;
022        private double lon = Double.NaN;
023    
024        /*
025         * the cached projected coordinates
026         */
027        private double east = Double.NaN;
028        private double north = Double.NaN;
029    
030        private boolean isLatLonKnown() {
031            return !Double.isNaN(lat) && !Double.isNaN(lon);
032        }
033    
034        @Override
035        public final void setCoor(LatLon coor) {
036            updateCoor(coor, null);
037        }
038    
039        @Override
040        public final void setEastNorth(EastNorth eastNorth) {
041            updateCoor(null, eastNorth);
042        }
043    
044        private void updateCoor(LatLon coor, EastNorth eastNorth) {
045            if (getDataSet() != null) {
046                boolean locked = writeLock();
047                try {
048                    getDataSet().fireNodeMoved(this, coor, eastNorth);
049                } finally {
050                    writeUnlock(locked);
051                }
052            } else {
053                setCoorInternal(coor, eastNorth);
054            }
055        }
056    
057        @Override
058        public final LatLon getCoor() {
059            if (!isLatLonKnown()) return null;
060            return new LatLon(lat,lon);
061        }
062    
063        /**
064         * <p>Replies the projected east/north coordinates.</p>
065         * 
066         * <p>Uses the {@link Main#getProjection() global projection} to project the lan/lon-coordinates.
067         * Internally caches the projected coordinates.</p>
068         *
069         * <p><strong>Caveat:</strong> doesn't listen to projection changes. Clients must
070         * {@link #invalidateEastNorthCache() invalidate the internal cache}.</p>
071         * 
072         * <p>Replies {@code null} if this node doesn't know lat/lon-coordinates, i.e. because it is an incomplete node.
073         * 
074         * @return the east north coordinates or {@code null}
075         * @see #invalidateEastNorthCache()
076         * 
077         */
078        @Override
079        public final EastNorth getEastNorth() {
080            if (!isLatLonKnown()) return null;
081    
082            if (getDataSet() == null)
083                // there is no dataset that listens for projection changes
084                // and invalidates the cache, so we don't use the cache at all
085                return Projections.project(new LatLon(lat, lon));
086    
087            if (Double.isNaN(east) || Double.isNaN(north)) {
088                // projected coordinates haven't been calculated yet,
089                // so fill the cache of the projected node coordinates
090                EastNorth en = Projections.project(new LatLon(lat, lon));
091                this.east = en.east();
092                this.north = en.north();
093            }
094            return new EastNorth(east, north);
095        }
096    
097        /**
098         * To be used only by Dataset.reindexNode
099         */
100        protected void setCoorInternal(LatLon coor, EastNorth eastNorth) {
101            if (coor != null) {
102                this.lat = coor.lat();
103                this.lon = coor.lon();
104                invalidateEastNorthCache();
105            } else if (eastNorth != null) {
106                LatLon ll = Projections.inverseProject(eastNorth);
107                this.lat = ll.lat();
108                this.lon = ll.lon();
109                this.east = eastNorth.east();
110                this.north = eastNorth.north();
111            } else {
112                this.lat = Double.NaN;
113                this.lon = Double.NaN;
114                invalidateEastNorthCache();
115            }
116        }
117    
118        protected Node(long id, boolean allowNegative) {
119            super(id, allowNegative);
120        }
121    
122        /**
123         * Constructs a new local {@code Node} with id 0.
124         */
125        public Node() {
126            this(0, false);
127        }
128    
129        /**
130         * Constructs an incomplete {@code Node} object with the given id.
131         * @param id The id. Must be >= 0
132         * @throws IllegalArgumentException if id < 0
133         */
134        public Node(long id) throws IllegalArgumentException {
135            super(id, false);
136        }
137    
138        /**
139         * Constructs a new {@code Node} with the given id and version.
140         * @param id The id. Must be >= 0
141         * @param version The version
142         * @throws IllegalArgumentException if id < 0
143         */
144        public Node(long id, int version) throws IllegalArgumentException {
145            super(id, version, false);
146        }
147    
148        /**
149         * Constructs an identical clone of the argument.
150         * @param clone The node to clone
151         * @param clearId If true, set version to 0 and id to new unique value
152         */
153        public Node(Node clone, boolean clearId) {
154            super(clone.getUniqueId(), true /* allow negative IDs */);
155            cloneFrom(clone);
156            if (clearId) {
157                clearOsmId();
158            }
159        }
160    
161        /**
162         * Constructs an identical clone of the argument (including the id).
163         * @param clone The node to clone, including its id
164         */
165        public Node(Node clone) {
166            this(clone, false);
167        }
168    
169        /**
170         * Constructs a new {@code Node} with the given lat/lon with id 0.
171         * @param latlon The {@link LatLon} coordinates
172         */
173        public Node(LatLon latlon) {
174            super(0, false);
175            setCoor(latlon);
176        }
177    
178        /**
179         * Constructs a new {@code Node} with the given east/north with id 0.
180         * @param eastNorth The {@link EastNorth} coordinates
181         */
182        public Node(EastNorth eastNorth) {
183            super(0, false);
184            setEastNorth(eastNorth);
185        }
186    
187        @Override
188        void setDataset(DataSet dataSet) {
189            super.setDataset(dataSet);
190            if (!isIncomplete() && isVisible() && (getCoor() == null || getEastNorth() == null))
191                throw new DataIntegrityProblemException("Complete node with null coordinates: " + toString() + get3892DebugInfo());
192        }
193    
194        @Override
195        public void visit(Visitor visitor) {
196            visitor.visit(this);
197        }
198    
199        @Override
200        public void visit(PrimitiveVisitor visitor) {
201            visitor.visit(this);
202        }
203    
204        @Override
205        public void cloneFrom(OsmPrimitive osm) {
206            boolean locked = writeLock();
207            try {
208                super.cloneFrom(osm);
209                setCoor(((Node)osm).getCoor());
210            } finally {
211                writeUnlock(locked);
212            }
213        }
214    
215        /**
216         * Merges the technical and semantical attributes from <code>other</code> onto this.
217         *
218         * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code>
219         * have an assigend OSM id, the IDs have to be the same.
220         *
221         * @param other the other primitive. Must not be null.
222         * @throws IllegalArgumentException thrown if other is null.
223         * @throws DataIntegrityProblemException thrown if either this is new and other is not, or other is new and this is not
224         * @throws DataIntegrityProblemException thrown if other is new and other.getId() != this.getId()
225         */
226        @Override
227        public void mergeFrom(OsmPrimitive other) {
228            boolean locked = writeLock();
229            try {
230                super.mergeFrom(other);
231                if (!other.isIncomplete()) {
232                    setCoor(((Node)other).getCoor());
233                }
234            } finally {
235                writeUnlock(locked);
236            }
237        }
238    
239        @Override public void load(PrimitiveData data) {
240            boolean locked = writeLock();
241            try {
242                super.load(data);
243                setCoor(((NodeData)data).getCoor());
244            } finally {
245                writeUnlock(locked);
246            }
247        }
248    
249        @Override public NodeData save() {
250            NodeData data = new NodeData();
251            saveCommonAttributes(data);
252            if (!isIncomplete()) {
253                data.setCoor(getCoor());
254            }
255            return data;
256        }
257    
258        @Override
259        public String toString() {
260            String coorDesc = isLatLonKnown() ? "lat="+lat+",lon="+lon : "";
261            return "{Node id=" + getUniqueId() + " version=" + getVersion() + " " + getFlagsAsString() + " "  + coorDesc+"}";
262        }
263    
264        @Override
265        public boolean hasEqualSemanticAttributes(OsmPrimitive other) {
266            if (other == null || ! (other instanceof Node) )
267                return false;
268            if (! super.hasEqualSemanticAttributes(other))
269                return false;
270            Node n = (Node)other;
271            LatLon coor = getCoor();
272            LatLon otherCoor = n.getCoor();
273            if (coor == null && otherCoor == null)
274                return true;
275            else if (coor != null && otherCoor != null)
276                return coor.equalsEpsilon(otherCoor);
277            else
278                return false;
279        }
280    
281        @Override
282        public int compareTo(OsmPrimitive o) {
283            return o instanceof Node ? Long.valueOf(getUniqueId()).compareTo(o.getUniqueId()) : 1;
284        }
285    
286        @Override
287        public String getDisplayName(NameFormatter formatter) {
288            return formatter.format(this);
289        }
290    
291        @Override
292        public OsmPrimitiveType getType() {
293            return OsmPrimitiveType.NODE;
294        }
295    
296        @Override
297        public BBox getBBox() {
298            return new BBox(this);
299        }
300    
301        @Override
302        public void updatePosition() {
303        }
304        
305        @Override
306        public boolean isDrawable() {
307            // Not possible to draw a node without coordinates.
308            return super.isDrawable() && isLatLonKnown();
309        }
310    
311        /**
312         * Check whether this node connects 2 ways.
313         * 
314         * @return true if isReferredByWays(2) returns true
315         * @see #isReferredByWays(int) 
316         */
317        public boolean isConnectionNode() {
318            return isReferredByWays(2);
319        }
320    
321        /**
322         * Get debug info for bug #3892.
323         * @return debug info for bug #3892.
324         * @deprecated This method will be remove by the end of 2012 if no report appears.
325         */
326        public String get3892DebugInfo() {
327            StringBuilder builder = new StringBuilder();
328            builder.append("Unexpected error. Please report it to http://josm.openstreetmap.de/ticket/3892\n");
329            builder.append(toString());
330            builder.append("\n");
331            if (isLatLonKnown()) {
332                builder.append("Coor is null\n");
333            } else {
334                builder.append(String.format("EastNorth: %s\n", getEastNorth()));
335                builder.append(Main.getProjection());
336                builder.append("\n");
337            }
338    
339            return builder.toString();
340        }
341    
342        /**
343         * Invoke to invalidate the internal cache of projected east/north coordinates.
344         * Coordinates are reprojected on demand when the {@link #getEastNorth()} is invoked
345         * next time.
346         */
347        public void invalidateEastNorthCache() {
348            this.east = Double.NaN;
349            this.north = Double.NaN;
350        }
351    }