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

Last change on this file since 2410 was 2410, checked in by jttt, 15 years ago

Add clearId parameter to primitives copy constructor, replace some clearOsmId calls, throw exception if id is set after primitive was added to the dataset

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