source: josm/trunk/src/org/openstreetmap/josm/io/OsmReader.java@ 2470

Last change on this file since 2470 was 2470, checked in by Gubaer, 14 years ago

fixed #3975: Loading of data breaking off at objects with visible=false

  • Property svn:eol-style set to native
File size: 24.2 KB
Line 
1package org.openstreetmap.josm.io;
2
3import static org.openstreetmap.josm.tools.I18n.tr;
4
5import java.io.InputStream;
6import java.io.InputStreamReader;
7import java.util.ArrayList;
8import java.util.Collection;
9import java.util.Date;
10import java.util.HashMap;
11import java.util.LinkedList;
12import java.util.List;
13import java.util.Map;
14import java.util.logging.Logger;
15
16import javax.xml.parsers.ParserConfigurationException;
17import javax.xml.parsers.SAXParserFactory;
18
19import org.openstreetmap.josm.data.Bounds;
20import org.openstreetmap.josm.data.coor.LatLon;
21import org.openstreetmap.josm.data.osm.DataSet;
22import org.openstreetmap.josm.data.osm.DataSource;
23import org.openstreetmap.josm.data.osm.Node;
24import org.openstreetmap.josm.data.osm.OsmPrimitive;
25import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
26import org.openstreetmap.josm.data.osm.Relation;
27import org.openstreetmap.josm.data.osm.RelationMember;
28import org.openstreetmap.josm.data.osm.User;
29import org.openstreetmap.josm.data.osm.Way;
30import org.openstreetmap.josm.gui.progress.ProgressMonitor;
31import org.openstreetmap.josm.tools.DateUtils;
32import org.xml.sax.Attributes;
33import org.xml.sax.InputSource;
34import org.xml.sax.Locator;
35import org.xml.sax.SAXException;
36import org.xml.sax.helpers.DefaultHandler;
37
38/**
39 * Parser for the Osm Api. Read from an input stream and construct a dataset out of it.
40 *
41 */
42public class OsmReader {
43 static private final Logger logger = Logger.getLogger(OsmReader.class.getName());
44
45 /**
46 * The dataset to add parsed objects to.
47 */
48 private DataSet ds = new DataSet();
49
50 /**
51 * Replies the parsed data set
52 *
53 * @return the parsed data set
54 */
55 public DataSet getDataSet() {
56 return ds;
57 }
58
59 /** the map from external ids to read OsmPrimitives. External ids are
60 * longs too, but in contrast to internal ids negative values are used
61 * to identify primitives unknown to the OSM server
62 *
63 * The keys are strings composed as follows
64 * <ul>
65 * <li>"n" + id for nodes</li>
66 * <li>"w" + id for nodes</li>
67 * <li>"r" + id for nodes</li>
68 * </ul>
69 */
70 private Map<String, OsmPrimitive> externalIdMap = new HashMap<String, OsmPrimitive>();
71
72
73 /**
74 * constructor (for private use only)
75 *
76 * @see #parseDataSet(InputStream, DataSet, ProgressMonitor)
77 * @see #parseDataSetOsm(InputStream, DataSet, ProgressMonitor)
78 */
79 private OsmReader() {
80 externalIdMap = new HashMap<String, OsmPrimitive>();
81 }
82
83 private static class OsmPrimitiveData {
84 public long id = 0;
85 public boolean modified = false;
86 public boolean deleted = false;
87 public Date timestamp = new Date();
88 public User user = null;
89 public boolean visible = true;
90 public int version = 0;
91 public LatLon latlon = new LatLon(0,0);
92 private OsmPrimitive primitive;
93
94 public void copyTo(OsmPrimitive osm) {
95 // It's not necessary to call clearOsmId for id < 0. The same thing is done by parameterless constructor
96 if (id > 0) {
97 osm.setOsmId(id, version);
98 }
99 osm.setDeleted(deleted);
100 osm.setModified(modified | deleted);
101 osm.setTimestamp(timestamp);
102 osm.setUser(user);
103 if (id > 0) {
104 // ignore visible attribute for objects not yet known to the server
105 //
106 osm.setVisible(visible);
107 }
108 osm.mappaintStyle = null;
109 }
110
111 public Node createNode() {
112 Node node = new Node();
113 node.setCoor(latlon);
114 copyTo(node);
115 primitive = node;
116 return node;
117 }
118
119 public Way createWay() {
120 Way way = new Way();
121 copyTo(way);
122 primitive = way;
123 return way;
124 }
125 public Relation createRelation() {
126 Relation relation= new Relation();
127 copyTo(relation);
128 primitive = relation;
129 return relation;
130 }
131
132 public void rememberTag(String key, String value) {
133 primitive.put(key, value);
134 }
135 }
136
137 /**
138 * Used as a temporary storage for relation members, before they
139 * are resolved into pointers to real objects.
140 */
141 private static class RelationMemberData {
142 public String type;
143 public long id;
144 public String role;
145 }
146
147 /**
148 * Data structure for the remaining way objects
149 */
150 private Map<Long, Collection<Long>> ways = new HashMap<Long, Collection<Long>>();
151
152 /**
153 * Data structure for relation objects
154 */
155 private Map<Long, Collection<RelationMemberData>> relations = new HashMap<Long, Collection<RelationMemberData>>();
156
157 private class Parser extends DefaultHandler {
158 private Locator locator;
159
160 @Override
161 public void setDocumentLocator(Locator locator) {
162 this.locator = locator;
163 }
164
165 protected void throwException(String msg) throws OsmDataParsingException{
166 throw new OsmDataParsingException(msg).rememberLocation(locator);
167 }
168 /**
169 * The current osm primitive to be read.
170 */
171 private OsmPrimitiveData current;
172 private String generator;
173
174
175 @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
176 if (qName.equals("osm")) {
177 if (atts == null) {
178 throwException(tr("Missing mandatory attribute ''{0}'' of XML element {1}.", "version", "osm"));
179 }
180 String v = atts.getValue("version");
181 if (v == null) {
182 throwException(tr("Missing mandatory attribute ''{0}''.", "version"));
183 }
184 if (!(v.equals("0.5") || v.equals("0.6"))) {
185 throwException(tr("Unsupported version: {0}", v));
186 }
187 // save generator attribute for later use when creating DataSource objects
188 generator = atts.getValue("generator");
189 ds.setVersion(v);
190
191 } else if (qName.equals("bounds")) {
192 // new style bounds.
193 String minlon = atts.getValue("minlon");
194 String minlat = atts.getValue("minlat");
195 String maxlon = atts.getValue("maxlon");
196 String maxlat = atts.getValue("maxlat");
197 String origin = atts.getValue("origin");
198 if (minlon != null && maxlon != null && minlat != null && maxlat != null) {
199 if (origin == null) {
200 origin = generator;
201 }
202 Bounds bounds = new Bounds(
203 new LatLon(Double.parseDouble(minlat), Double.parseDouble(minlon)),
204 new LatLon(Double.parseDouble(maxlat), Double.parseDouble(maxlon)));
205 DataSource src = new DataSource(bounds, origin);
206 ds.dataSources.add(src);
207 } else {
208 throwException(tr(
209 "Missing manadatory attributes on element ''bounds''. Got minlon=''{0}'',minlat=''{1}'',maxlon=''{3}'',maxlat=''{4}'', origin=''{5}''.",
210 minlon, minlat, maxlon, maxlat, origin
211 ));
212 }
213
214 // ---- PARSING NODES AND WAYS ----
215
216 } else if (qName.equals("node")) {
217 current = new OsmPrimitiveData();
218 current.latlon = new LatLon(getDouble(atts, "lat"), getDouble(atts, "lon"));
219 readCommon(atts, current);
220 Node n = current.createNode();
221 externalIdMap.put("n"+current.id, n);
222 } else if (qName.equals("way")) {
223 current = new OsmPrimitiveData();
224 readCommon(atts, current);
225 Way w = current.createWay();
226 externalIdMap.put("w"+current.id, w);
227 ways.put(current.id, new ArrayList<Long>());
228 } else if (qName.equals("nd")) {
229 Collection<Long> list = ways.get(current.id);
230 if (list == null) {
231 throwException(
232 tr("Found XML element <nd> not as direct child of element <way>.")
233 );
234 }
235 if (atts.getValue("ref") == null) {
236 throwException(
237 tr("Missing mandatory attribute ''{0}'' on <nd> of way {1}.", "ref", current.id)
238 );
239 }
240 long id = getLong(atts, "ref");
241 if (id == 0) {
242 throwException(
243 tr("Illegal value of attribute ''ref'' of element <nd>. Got {0}.", id)
244 );
245 }
246 list.add(id);
247
248 // ---- PARSING RELATIONS ----
249
250 } else if (qName.equals("relation")) {
251 current = new OsmPrimitiveData();
252 readCommon(atts, current);
253 Relation r = current.createRelation();
254 externalIdMap.put("r"+current.id, r );
255 relations.put(current.id, new LinkedList<RelationMemberData>());
256 } else if (qName.equals("member")) {
257 Collection<RelationMemberData> list = relations.get(current.id);
258 if (list == null) {
259 throwException(
260 tr("Found XML element <member> not as direct child of element <relation>.")
261 );
262 }
263 RelationMemberData emd = new RelationMemberData();
264 String value = atts.getValue("ref");
265 if (value == null) {
266 throwException(tr("Missing attribute ''ref'' on member in relation {0}.",current.id));
267 }
268 try {
269 emd.id = Long.parseLong(value);
270 } catch(NumberFormatException e) {
271 throwException(tr("Illegal value for attribute ''ref'' on member in relation {0}. Got {1}", Long.toString(current.id),value));
272 }
273 value = atts.getValue("type");
274 if (value == null) {
275 throwException(tr("Missing attribute ''type'' on member {0} in relation {1}.", Long.toString(emd.id), Long.toString(current.id)));
276 }
277 if (! (value.equals("way") || value.equals("node") || value.equals("relation"))) {
278 throwException(tr("Illegal value for attribute ''type'' on member {0} in relation {1}. Got {2}.", Long.toString(emd.id), Long.toString(current.id), value));
279 }
280 emd.type= value;
281 value = atts.getValue("role");
282 emd.role = value;
283
284 if (emd.id == 0) {
285 throwException(tr("Incomplete <member> specification with ref=0"));
286 }
287
288 list.add(emd);
289
290 // ---- PARSING TAGS (applicable to all objects) ----
291
292 } else if (qName.equals("tag")) {
293 String key = atts.getValue("k");
294 String value = atts.getValue("v");
295 current.rememberTag(key, value);
296 } else {
297 System.out.println(tr("Undefined element ''{0}'' found in input stream. Skipping.", qName));
298 }
299 }
300
301 private double getDouble(Attributes atts, String value) {
302 return Double.parseDouble(atts.getValue(value));
303 }
304
305 private User createUser(String uid, String name) throws SAXException {
306 if (uid == null) {
307 if (name == null)
308 return null;
309 return User.createLocalUser(name);
310 }
311 try {
312 long id = Long.parseLong(uid);
313 return User.createOsmUser(id, name);
314 } catch(NumberFormatException e) {
315 throwException(tr("Illegal value for attribute ''uid''. Got ''{0}''.", uid));
316 }
317 return null;
318 }
319 /**
320 * Read out the common attributes from atts and put them into this.current.
321 */
322 void readCommon(Attributes atts, OsmPrimitiveData current) throws SAXException {
323 current.id = getLong(atts, "id");
324 if (current.id == 0) {
325 throwException(tr("Illegal object with ID=0."));
326 }
327
328 String time = atts.getValue("timestamp");
329 if (time != null && time.length() != 0) {
330 current.timestamp = DateUtils.fromString(time);
331 }
332
333 // user attribute added in 0.4 API
334 String user = atts.getValue("user");
335 // uid attribute added in 0.6 API
336 String uid = atts.getValue("uid");
337 current.user = createUser(uid, user);
338
339 // visible attribute added in 0.4 API
340 String visible = atts.getValue("visible");
341 if (visible != null) {
342 current.visible = Boolean.parseBoolean(visible);
343 }
344
345 String version = atts.getValue("version");
346 current.version = 0;
347 if (version != null) {
348 try {
349 current.version = Integer.parseInt(version);
350 } catch(NumberFormatException e) {
351 throwException(tr("Illegal value for attribute ''version'' on OSM primitive with ID {0}. Got {1}.", Long.toString(current.id), version));
352 }
353 if (ds.getVersion().equals("0.6")){
354 if (current.version <= 0 && current.id > 0) {
355 throwException(tr("Illegal value for attribute ''version'' on OSM primitive with ID {0}. Got {1}.", Long.toString(current.id), version));
356 } else if (current.version < 0 && current.id <=0) {
357 System.out.println(tr("WARNING: Normalizing value of attribute ''version'' of element {0} to {2}, API version is ''{3}''. Got {1}.", current.id, current.version, 0, "0.6"));
358 current.version = 0;
359 }
360 } else if (ds.getVersion().equals("0.5")) {
361 if (current.version <= 0 && current.id > 0) {
362 System.out.println(tr("WARNING: Normalizing value of attribute ''version'' of element {0} to {2}, API version is ''{3}''. Got {1}.", current.id, current.version, 1, "0.5"));
363 current.version = 1;
364 } else if (current.version < 0 && current.id <=0) {
365 System.out.println(tr("WARNING: Normalizing value of attribute ''version'' of element {0} to {2}, API version is ''{3}''. Got {1}.", current.id, current.version, 0, "0.5"));
366 current.version = 0;
367 }
368 } else {
369 // should not happen. API version has been checked before
370 throwException(tr("Unknown or unsupported API version. Got {0}.", ds.getVersion()));
371 }
372 } else {
373 // version expected for OSM primitives with an id assigned by the server (id > 0), since API 0.6
374 //
375 if (current.id > 0 && ds.getVersion() != null && ds.getVersion().equals("0.6")) {
376 throwException(tr("Missing attribute ''version'' on OSM primitive with ID {0}.", Long.toString(current.id)));
377 } else if (current.id > 0 && ds.getVersion() != null && ds.getVersion().equals("0.5")) {
378 // default version in 0.5 files for existing primitives
379 System.out.println(tr("WARNING: Normalizing value of attribute ''version'' of element {0} to {2}, API version is ''{3}''. Got {1}.", current.id, current.version, 1, "0.5"));
380 current.version= 1;
381 } else if (current.id <= 0 && ds.getVersion() != null && ds.getVersion().equals("0.5")) {
382 // default version in 0.5 files for new primitives, no warning necessary. This is
383 // (was) legal in API 0.5
384 current.version= 0;
385 }
386 }
387
388 String action = atts.getValue("action");
389 if (action == null)
390 return;
391 if (action.equals("delete")) {
392 current.deleted = true;
393 } else if (action.startsWith("modify")) {
394 current.modified = true;
395 }
396 }
397
398 private long getLong(Attributes atts, String name) throws SAXException {
399 String value = atts.getValue(name);
400 if (value == null) {
401 throwException(tr("Missing required attribute ''{0}''.",name));
402 }
403 try {
404 return Long.parseLong(value);
405 } catch(NumberFormatException e) {
406 throwException(tr("Illegal long value for attribute ''{0}''. Got ''{1}''.",name, value));
407 }
408 return 0; // should not happen
409 }
410 }
411
412
413 /**
414 * Processes the ways after parsing. Rebuilds the list of nodes of each way and
415 * adds the way to the dataset
416 *
417 * @throws IllegalDataException thrown if a data integrity problem is detected
418 */
419 protected void processWaysAfterParsing() throws IllegalDataException{
420 for (Long externalWayId: ways.keySet()) {
421 Way w = (Way)externalIdMap.get("w" + externalWayId);
422 boolean incomplete = false;
423 List<Node> wayNodes = new ArrayList<Node>();
424 for (long id : ways.get(externalWayId)) {
425 Node n = (Node)externalIdMap.get("n" +id);
426 if (n == null) {
427 if (id <= 0)
428 throw new IllegalDataException (
429 tr(
430 "Way with external ID ''{0}'' includes missing node with external ID ''{1}''.",
431 externalWayId,
432 id
433 )
434 );
435 // create an incomplete node if necessary
436 //
437 n = (Node)ds.getPrimitiveById(id,OsmPrimitiveType.NODE);
438 if (n == null) {
439 n = new Node(id);
440 n.incomplete = true;
441 ds.addPrimitive(n);
442 }
443 incomplete = true;
444 }
445 wayNodes.add(n);
446 }
447 w.setNodes(wayNodes);
448 if (incomplete) {
449 logger.warning(tr("Marked way {0} with {1} nodes incomplete because at least one node was missing in the " +
450 "loaded data and is therefore incomplete too.", externalWayId, w.getNodesCount()));
451 w.incomplete = true;
452 ds.addPrimitive(w);
453 } else {
454 w.incomplete = false;
455 ds.addPrimitive(w);
456 }
457 }
458 }
459
460 /**
461 * Processes the parsed nodes after parsing. Just adds them to
462 * the dataset
463 *
464 */
465 protected void processNodesAfterParsing() {
466 for (OsmPrimitive primitive: externalIdMap.values()) {
467 if (primitive instanceof Node) {
468 this.ds.addPrimitive(primitive);
469 }
470 }
471 }
472
473 /**
474 * Completes the parsed relations with its members.
475 *
476 * @throws IllegalDataException thrown if a data integrity problem is detected, i.e. if a
477 * relation member refers to a local primitive which wasn't available in the data
478 *
479 */
480 private void processRelationsAfterParsing() throws IllegalDataException {
481 for (Long externalRelationId : relations.keySet()) {
482 Relation relation = (Relation) externalIdMap.get("r" +externalRelationId);
483 List<RelationMember> relationMembers = new ArrayList<RelationMember>();
484 for (RelationMemberData rm : relations.get(externalRelationId)) {
485 OsmPrimitive primitive = null;
486
487 // lookup the member from the map of already created primitives
488 //
489 if (rm.type.equals("node")) {
490 primitive = externalIdMap.get("n" + rm.id);
491 } else if (rm.type.equals("way")) {
492 primitive = externalIdMap.get("w" + rm.id);
493 } else if (rm.type.equals("relation")) {
494 primitive = externalIdMap.get("r" + rm.id);
495 } else
496 throw new IllegalDataException(
497 tr("Unknown relation member type ''{0}'' in relation with external id ''{1}''.", rm.type,externalRelationId)
498 );
499
500 if (primitive == null) {
501 if (rm.id <= 0)
502 // relation member refers to a primitive with a negative id which was not
503 // found in the data. This is always a data integrity problem and we abort
504 // with an exception
505 //
506 throw new IllegalDataException(
507 tr(
508 "Relation with external id ''{0}'' refers to a missing primitive with external id ''{1}''.",
509 externalRelationId,
510 rm.id
511 )
512 );
513
514 // member refers to OSM primitive which was not present in the parsed data
515 // -> create a new incomplete primitive and add it to the dataset
516 //
517 if (rm.type.equals("node")) {
518 primitive = new Node(rm.id);
519 } else if (rm.type.equals("way")) {
520 primitive = new Way(rm.id);
521 } else if (rm.type.equals("relation")) {
522 primitive = new Relation(rm.id);
523 } else {
524 // can't happen, we've been testing for valid member types
525 // at the beginning of this method
526 //
527 }
528 ds.addPrimitive(primitive);
529
530 if (rm.type.equals("node")) {
531 externalIdMap.put("n" + rm.id, primitive);
532 } else if (rm.type.equals("way")) {
533 externalIdMap.put("w" + rm.id, primitive);
534 } else if (rm.type.equals("relation")) {
535 externalIdMap.put("r" + rm.id, primitive);
536 }
537
538 }
539 relationMembers.add(new RelationMember(rm.role, primitive));
540 }
541 relation.setMembers(relationMembers);
542 ds.addPrimitive(relation);
543 }
544 }
545
546 /**
547 * Parse the given input source and return the dataset.
548 *
549 * @param source the source input stream
550 * @param progressMonitor the progress monitor
551 *
552 * @return the dataset with the parsed data
553 * @throws IllegalDataException thrown if the an error was found while parsing the data from the source
554 */
555 public static DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
556 OsmReader reader = new OsmReader();
557 try {
558 progressMonitor.beginTask(tr("Prepare OSM data...", 2));
559 progressMonitor.subTask(tr("Parsing OSM data..."));
560 InputSource inputSource = new InputSource(new InputStreamReader(source, "UTF-8"));
561 SAXParserFactory.newInstance().newSAXParser().parse(inputSource, reader.new Parser());
562 progressMonitor.worked(1);
563
564 progressMonitor.subTask(tr("Preparing data set..."));
565 reader.processNodesAfterParsing();
566 reader.processWaysAfterParsing();
567 reader.processRelationsAfterParsing();
568 progressMonitor.worked(1);
569 return reader.getDataSet();
570 } catch(IllegalDataException e) {
571 throw e;
572 } catch(ParserConfigurationException e) {
573 throw new IllegalDataException(e.getMessage(), e);
574 } catch(SAXException e) {
575 throw new IllegalDataException(e.getMessage(), e);
576 } catch(Exception e) {
577 throw new IllegalDataException(e);
578 } finally {
579 progressMonitor.finishTask();
580 }
581 }
582}
Note: See TracBrowser for help on using the repository browser.