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

Last change on this file since 2512 was 2512, checked in by stoecker, 14 years ago

i18n updated, fixed files to reduce problems when applying patches, fix #4017

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