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

Last change on this file since 2070 was 2070, checked in by Gubaer, 15 years ago

new: rewrite of CombineWay action
new: conflict resolution dialog for CombineWay, including conflicts for different relation memberships
cleanup: cleanup in OsmReader, reduces memory footprint and reduces parsing time
cleanup: made most of the public fields in OsmPrimitive @deprecated, added accessors and changed the code
cleanup: replaced usages of @deprecated constructors for ExtendedDialog
fixed #3208: Combine ways brokes relation order

WARNING: this changeset touches a lot of code all over the code base. "latest" might become slightly unstable in the next days. Also experience incompatibility issues with plugins in the next few days.

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