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

Last change on this file since 2862 was 2852, checked in by mjulius, 14 years ago

fix messages for io

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