Changeset 18817 in josm
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/org/openstreetmap/josm/io/GpxReader.java
r18396 r18817 7 7 import java.io.InputStream; 8 8 import java.io.Reader; 9 import java.time.DateTimeException;10 import java.util.ArrayList;11 import java.util.Collection;12 import java.util.HashMap;13 import java.util.LinkedList;14 import java.util.List;15 import java.util.Map;16 import java.util.Stack;17 9 18 10 import javax.xml.parsers.ParserConfigurationException; 19 11 20 import org.openstreetmap.josm.data.Bounds;21 import org.openstreetmap.josm.data.coor.LatLon;22 12 import org.openstreetmap.josm.data.gpx.GpxConstants; 23 13 import org.openstreetmap.josm.data.gpx.GpxData; 24 import org.openstreetmap.josm.data.gpx.GpxData.XMLNamespace;25 import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;26 import org.openstreetmap.josm.data.gpx.GpxLink;27 import org.openstreetmap.josm.data.gpx.GpxRoute;28 import org.openstreetmap.josm.data.gpx.GpxTrack;29 import org.openstreetmap.josm.data.gpx.GpxTrackSegment;30 import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;31 import org.openstreetmap.josm.data.gpx.WayPoint;32 14 import org.openstreetmap.josm.tools.Logging; 33 import org.openstreetmap.josm.tools.UncheckedParseException;34 15 import org.openstreetmap.josm.tools.Utils; 35 16 import org.openstreetmap.josm.tools.XmlUtils; 36 import org.openstreetmap.josm.tools.date.DateUtils;37 import org.xml.sax.Attributes;38 17 import org.xml.sax.InputSource; 39 18 import org.xml.sax.SAXException; 40 19 import org.xml.sax.SAXParseException; 41 import org.xml.sax.helpers.DefaultHandler;42 20 43 21 /** 44 22 * Read a gpx file. 45 * 23 * <p> 46 24 * Bounds are read, even if we calculate them, see {@link GpxData#recalculateBounds}.<br> 47 25 * Both GPX version 1.0 and 1.1 are supported. … … 51 29 public class GpxReader implements GpxConstants, IGpxReader { 52 30 53 private enum State {54 INIT,55 GPX,56 METADATA,57 WPT,58 RTE,59 TRK,60 EXT,61 AUTHOR,62 LINK,63 TRKSEG,64 COPYRIGHT65 }66 67 private String version;68 31 /** The resulting gpx data */ 69 32 private GpxData gpxData; 70 33 private final InputSource inputSource; 71 72 private class Parser extends DefaultHandler {73 74 private GpxData data;75 private Collection<IGpxTrackSegment> currentTrack;76 private Map<String, Object> currentTrackAttr;77 private Collection<WayPoint> currentTrackSeg;78 private GpxRoute currentRoute;79 private WayPoint currentWayPoint;80 81 private State currentState = State.INIT;82 83 private GpxLink currentLink;84 private GpxExtensionCollection currentExtensionCollection;85 private GpxExtensionCollection currentTrackExtensionCollection;86 private Stack<State> states;87 private final Stack<String[]> elements = new Stack<>();88 89 private StringBuilder accumulator = new StringBuilder();90 91 private boolean nokiaSportsTrackerBug;92 93 @Override94 public void startDocument() {95 accumulator = new StringBuilder();96 states = new Stack<>();97 data = new GpxData(true);98 currentExtensionCollection = new GpxExtensionCollection();99 currentTrackExtensionCollection = new GpxExtensionCollection();100 }101 102 @Override103 public void startPrefixMapping(String prefix, String uri) throws SAXException {104 data.getNamespaces().add(new XMLNamespace(prefix, uri));105 }106 107 private double parseCoord(Attributes atts, String key) {108 String val = atts.getValue(key);109 if (val != null) {110 return parseCoord(val);111 } else {112 // Some software do not respect GPX schema and use "minLat" / "minLon" instead of "minlat" / "minlon"113 return parseCoord(atts.getValue(key.replaceFirst("l", "L")));114 }115 }116 117 private double parseCoord(String s) {118 if (s != null) {119 try {120 return Double.parseDouble(s);121 } catch (NumberFormatException ex) {122 Logging.trace(ex);123 }124 }125 return Double.NaN;126 }127 128 private LatLon parseLatLon(Attributes atts) {129 return new LatLon(130 parseCoord(atts, "lat"),131 parseCoord(atts, "lon"));132 }133 134 @Override135 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {136 elements.push(new String[] {namespaceURI, localName, qName});137 switch(currentState) {138 case INIT:139 states.push(currentState);140 currentState = State.GPX;141 data.creator = atts.getValue("creator");142 version = atts.getValue("version");143 if (version != null && version.startsWith("1.0")) {144 version = "1.0";145 } else if (!"1.1".equals(version)) {146 // unknown version, assume 1.1147 version = "1.1";148 }149 String schemaLocation = atts.getValue(GpxConstants.XML_URI_XSD, "schemaLocation");150 if (schemaLocation != null) {151 String[] schemaLocations = schemaLocation.split(" ", -1);152 for (int i = 0; i < schemaLocations.length - 1; i += 2) {153 final String schemaURI = schemaLocations[i];154 final String schemaXSD = schemaLocations[i + 1];155 data.getNamespaces().stream().filter(xml -> xml.getURI().equals(schemaURI)).forEach(xml -> {156 xml.setLocation(schemaXSD);157 });158 }159 }160 break;161 case GPX:162 switch (localName) {163 case "metadata":164 states.push(currentState);165 currentState = State.METADATA;166 break;167 case "wpt":168 states.push(currentState);169 currentState = State.WPT;170 currentWayPoint = new WayPoint(parseLatLon(atts));171 break;172 case "rte":173 states.push(currentState);174 currentState = State.RTE;175 currentRoute = new GpxRoute();176 break;177 case "trk":178 states.push(currentState);179 currentState = State.TRK;180 currentTrack = new ArrayList<>();181 currentTrackAttr = new HashMap<>();182 break;183 case "extensions":184 states.push(currentState);185 currentState = State.EXT;186 break;187 case "gpx":188 if (atts.getValue("creator") != null && atts.getValue("creator").startsWith("Nokia Sports Tracker")) {189 nokiaSportsTrackerBug = true;190 }191 break;192 default: // Do nothing193 }194 break;195 case METADATA:196 switch (localName) {197 case "author":198 states.push(currentState);199 currentState = State.AUTHOR;200 break;201 case "extensions":202 states.push(currentState);203 currentState = State.EXT;204 break;205 case "copyright":206 states.push(currentState);207 currentState = State.COPYRIGHT;208 data.put(META_COPYRIGHT_AUTHOR, atts.getValue("author"));209 break;210 case "link":211 states.push(currentState);212 currentState = State.LINK;213 currentLink = new GpxLink(atts.getValue("href"));214 break;215 case "bounds":216 data.put(META_BOUNDS, new Bounds(217 parseCoord(atts, "minlat"),218 parseCoord(atts, "minlon"),219 parseCoord(atts, "maxlat"),220 parseCoord(atts, "maxlon")));221 break;222 default: // Do nothing223 }224 break;225 case AUTHOR:226 switch (localName) {227 case "link":228 states.push(currentState);229 currentState = State.LINK;230 currentLink = new GpxLink(atts.getValue("href"));231 break;232 case "email":233 data.put(META_AUTHOR_EMAIL, atts.getValue("id") + '@' + atts.getValue("domain"));234 break;235 default: // Do nothing236 }237 break;238 case TRK:239 switch (localName) {240 case "trkseg":241 states.push(currentState);242 currentState = State.TRKSEG;243 currentTrackSeg = new ArrayList<>();244 break;245 case "link":246 states.push(currentState);247 currentState = State.LINK;248 currentLink = new GpxLink(atts.getValue("href"));249 break;250 case "extensions":251 states.push(currentState);252 currentState = State.EXT;253 break;254 default: // Do nothing255 }256 break;257 case TRKSEG:258 switch (localName) {259 case "trkpt":260 states.push(currentState);261 currentState = State.WPT;262 currentWayPoint = new WayPoint(parseLatLon(atts));263 break;264 case "extensions":265 states.push(currentState);266 currentState = State.EXT;267 break;268 default: // Do nothing269 }270 break;271 case WPT:272 switch (localName) {273 case "link":274 states.push(currentState);275 currentState = State.LINK;276 currentLink = new GpxLink(atts.getValue("href"));277 break;278 case "extensions":279 states.push(currentState);280 currentState = State.EXT;281 break;282 default: // Do nothing283 }284 break;285 case RTE:286 switch (localName) {287 case "link":288 states.push(currentState);289 currentState = State.LINK;290 currentLink = new GpxLink(atts.getValue("href"));291 break;292 case "rtept":293 states.push(currentState);294 currentState = State.WPT;295 currentWayPoint = new WayPoint(parseLatLon(atts));296 break;297 case "extensions":298 states.push(currentState);299 currentState = State.EXT;300 break;301 default: // Do nothing302 }303 break;304 case EXT:305 if (states.lastElement() == State.TRK) {306 currentTrackExtensionCollection.openChild(namespaceURI, qName, atts);307 } else {308 currentExtensionCollection.openChild(namespaceURI, qName, atts);309 }310 break;311 default: // Do nothing312 }313 accumulator.setLength(0);314 }315 316 @Override317 public void characters(char[] ch, int start, int length) {318 /**319 * Remove illegal characters generated by the Nokia Sports Tracker device.320 * Don't do this crude substitution for all files, since it would destroy321 * certain unicode characters.322 */323 if (nokiaSportsTrackerBug) {324 for (int i = 0; i < ch.length; ++i) {325 if (ch[i] == 1) {326 ch[i] = 32;327 }328 }329 nokiaSportsTrackerBug = false;330 }331 332 accumulator.append(ch, start, length);333 }334 335 private Map<String, Object> getAttr() {336 switch (currentState) {337 case RTE: return currentRoute.attr;338 case METADATA: return data.attr;339 case WPT: return currentWayPoint.attr;340 case TRK: return currentTrackAttr;341 default: return null;342 }343 }344 345 @SuppressWarnings("unchecked")346 @Override347 public void endElement(String namespaceURI, String localName, String qName) {348 elements.pop();349 switch (currentState) {350 case GPX: // GPX 1.0351 case METADATA: // GPX 1.1352 switch (localName) {353 case "name":354 data.put(META_NAME, accumulator.toString());355 break;356 case "desc":357 data.put(META_DESC, accumulator.toString());358 break;359 case "time":360 data.put(META_TIME, accumulator.toString());361 break;362 case "keywords":363 data.put(META_KEYWORDS, accumulator.toString());364 break;365 case "author":366 if ("1.0".equals(version)) {367 // author is a string in 1.0, but complex element in 1.1368 data.put(META_AUTHOR_NAME, accumulator.toString());369 }370 break;371 case "email":372 if ("1.0".equals(version)) {373 data.put(META_AUTHOR_EMAIL, accumulator.toString());374 }375 break;376 case "url":377 case "urlname":378 data.put(localName, accumulator.toString());379 break;380 case "metadata":381 case "gpx":382 if ((currentState == State.METADATA && "metadata".equals(localName)) ||383 (currentState == State.GPX && "gpx".equals(localName))) {384 convertUrlToLink(data.attr);385 data.getExtensions().addAll(currentExtensionCollection);386 currentExtensionCollection.clear();387 currentState = states.pop();388 }389 break;390 case "bounds":391 // do nothing, has been parsed on startElement392 break;393 default:394 }395 break;396 case AUTHOR:397 switch (localName) {398 case "author":399 currentState = states.pop();400 break;401 case "name":402 data.put(META_AUTHOR_NAME, accumulator.toString());403 break;404 case "email":405 // do nothing, has been parsed on startElement406 break;407 case "link":408 data.put(META_AUTHOR_LINK, currentLink);409 break;410 default: // Do nothing411 }412 break;413 case COPYRIGHT:414 switch (localName) {415 case "copyright":416 currentState = states.pop();417 break;418 case "year":419 data.put(META_COPYRIGHT_YEAR, accumulator.toString());420 break;421 case "license":422 data.put(META_COPYRIGHT_LICENSE, accumulator.toString());423 break;424 default: // Do nothing425 }426 break;427 case LINK:428 switch (localName) {429 case "text":430 currentLink.text = accumulator.toString();431 break;432 case "type":433 currentLink.type = accumulator.toString();434 break;435 case "link":436 if (currentLink.uri == null && !accumulator.toString().isEmpty()) {437 currentLink = new GpxLink(accumulator.toString());438 }439 currentState = states.pop();440 break;441 default: // Do nothing442 }443 if (currentState == State.AUTHOR) {444 data.put(META_AUTHOR_LINK, currentLink);445 } else if (currentState != State.LINK) {446 Map<String, Object> attr = getAttr();447 if (attr != null && !attr.containsKey(META_LINKS)) {448 attr.put(META_LINKS, new LinkedList<GpxLink>());449 }450 if (attr != null)451 ((Collection<GpxLink>) attr.get(META_LINKS)).add(currentLink);452 }453 break;454 case WPT:455 switch (localName) {456 case "ele":457 case "magvar":458 case "name":459 case "src":460 case "geoidheight":461 case "type":462 case "sym":463 case "url":464 case "urlname":465 case "cmt":466 case "desc":467 case "fix":468 currentWayPoint.put(localName, accumulator.toString());469 break;470 case "hdop":471 case "vdop":472 case "pdop":473 try {474 currentWayPoint.put(localName, Float.valueOf(accumulator.toString()));475 } catch (NumberFormatException e) {476 currentWayPoint.put(localName, 0f);477 }478 break;479 case PT_TIME:480 try {481 currentWayPoint.setInstant(DateUtils.parseInstant(accumulator.toString()));482 } catch (UncheckedParseException | DateTimeException e) {483 Logging.error(e);484 }485 break;486 case "rtept":487 currentState = states.pop();488 convertUrlToLink(currentWayPoint.attr);489 currentRoute.routePoints.add(currentWayPoint);490 break;491 case "trkpt":492 currentState = states.pop();493 convertUrlToLink(currentWayPoint.attr);494 currentTrackSeg.add(currentWayPoint);495 break;496 case "wpt":497 currentState = states.pop();498 convertUrlToLink(currentWayPoint.attr);499 currentWayPoint.getExtensions().addAll(currentExtensionCollection);500 data.waypoints.add(currentWayPoint);501 currentExtensionCollection.clear();502 break;503 default: // Do nothing504 }505 break;506 case TRKSEG:507 if ("trkseg".equals(localName)) {508 currentState = states.pop();509 if (!currentTrackSeg.isEmpty()) {510 GpxTrackSegment seg = new GpxTrackSegment(currentTrackSeg);511 if (!currentExtensionCollection.isEmpty()) {512 seg.getExtensions().addAll(currentExtensionCollection);513 }514 currentTrack.add(seg);515 }516 currentExtensionCollection.clear();517 }518 break;519 case TRK:520 switch (localName) {521 case "trk":522 currentState = states.pop();523 convertUrlToLink(currentTrackAttr);524 GpxTrack trk = new GpxTrack(new ArrayList<>(currentTrack), currentTrackAttr);525 if (!currentTrackExtensionCollection.isEmpty()) {526 trk.getExtensions().addAll(currentTrackExtensionCollection);527 }528 data.addTrack(trk);529 currentTrackExtensionCollection.clear();530 break;531 case "name":532 case "cmt":533 case "desc":534 case "src":535 case "type":536 case "number":537 case "url":538 case "urlname":539 currentTrackAttr.put(localName, accumulator.toString());540 break;541 default: // Do nothing542 }543 break;544 case EXT:545 if ("extensions".equals(localName)) {546 currentState = states.pop();547 } else if (currentExtensionCollection != null) {548 String acc = accumulator.toString().trim();549 if (states.lastElement() == State.TRK) {550 currentTrackExtensionCollection.closeChild(qName, acc); //a segment inside the track can have an extension too551 } else {552 currentExtensionCollection.closeChild(qName, acc);553 }554 }555 break;556 default:557 switch (localName) {558 case "wpt":559 currentState = states.pop();560 break;561 case "rte":562 currentState = states.pop();563 convertUrlToLink(currentRoute.attr);564 data.addRoute(currentRoute);565 break;566 default: // Do nothing567 }568 }569 accumulator.setLength(0);570 }571 572 @Override573 public void endDocument() throws SAXException {574 if (!states.empty())575 throw new SAXException(tr("Parse error: invalid document structure for GPX document."));576 577 data.getExtensions().stream("josm", "from-server").findAny().ifPresent(ext -> {578 data.fromServer = "true".equals(ext.getValue());579 });580 581 data.getExtensions().stream("josm", "layerPreferences").forEach(prefs -> {582 prefs.getExtensions().stream("josm", "entry").forEach(prefEntry -> {583 Object key = prefEntry.get("key");584 Object val = prefEntry.get("value");585 if (key != null && val != null) {586 data.getLayerPrefs().put(key.toString(), val.toString());587 }588 });589 });590 data.endUpdate();591 gpxData = data;592 }593 594 /**595 * convert url/urlname to link element (GPX 1.0 -> GPX 1.1).596 * @param attr attributes597 */598 private void convertUrlToLink(Map<String, Object> attr) {599 String url = (String) attr.get("url");600 String urlname = (String) attr.get("urlname");601 if (url != null) {602 if (!attr.containsKey(META_LINKS)) {603 attr.put(META_LINKS, new LinkedList<GpxLink>());604 }605 GpxLink link = new GpxLink(url);606 link.text = urlname;607 @SuppressWarnings("unchecked")608 Collection<GpxLink> links = (Collection<GpxLink>) attr.get(META_LINKS);609 links.add(link);610 }611 }612 613 void tryToFinish() throws SAXException {614 List<String[]> remainingElements = new ArrayList<>(elements);615 for (int i = remainingElements.size() - 1; i >= 0; i--) {616 String[] e = remainingElements.get(i);617 endElement(e[0], e[1], e[2]);618 }619 endDocument();620 }621 }622 34 623 35 /** … … 646 58 @Override 647 59 public boolean parse(boolean tryToFinish) throws SAXException, IOException { 648 Parser parser = newParser();60 GpxParser parser = new GpxParser(); 649 61 try { 650 62 XmlUtils.parseSafeSAX(inputSource, parser); … … 663 75 message += '.'; 664 76 } 665 if (!Utils.isBlank(parser. data.creator)) {666 message += "\n" + tr("The file was created by \"{0}\".", parser. data.creator);77 if (!Utils.isBlank(parser.getData().creator)) { 78 message += "\n" + tr("The file was created by \"{0}\".", parser.getData().creator); 667 79 } 668 80 SAXException ex = new SAXException(message, e); 669 if (parser. data.isEmpty())81 if (parser.getData().isEmpty()) 670 82 throw ex; 671 83 Logging.warn(ex); … … 676 88 Logging.error(e); // broken SAXException chaining 677 89 throw new SAXException(e); 90 } finally { 91 if (parser.getData() != null) { 92 this.gpxData = parser.getData(); 93 } 678 94 } 679 95 } -
trunk/test/unit/org/openstreetmap/josm/io/GpxReaderTest.java
r18801 r18817 5 5 import static org.junit.jupiter.api.Assertions.assertThrows; 6 6 import static org.junit.jupiter.api.Assertions.assertTrue; 7 import static org.junit.jupiter.api.Assertions.fail; 7 8 8 9 import java.io.ByteArrayInputStream; … … 14 15 import java.util.Map; 15 16 17 import org.junit.jupiter.params.ParameterizedTest; 18 import org.junit.jupiter.params.provider.ValueSource; 16 19 import org.openstreetmap.josm.TestUtils; 17 20 import org.openstreetmap.josm.data.Bounds; … … 82 85 /** 83 86 * Tests invalid data. 84 * @throws Exception always SAXException85 87 */ 86 88 @Test 87 void testException() throws Exception{89 void testException() { 88 90 assertThrows(SAXException.class, 89 91 () -> new GpxReader(new ByteArrayInputStream("--foo--bar--".getBytes(StandardCharsets.UTF_8))).parse(true)); … … 100 102 GpxReaderTest.parseGpxData(TestUtils.getRegressionDataFile(15634, "drumlish.gpx")).getMetaBounds()); 101 103 } 104 105 @ParameterizedTest 106 @ValueSource(strings = { 107 "<gpx><wpt></wpt></gpx>", 108 }) 109 void testIncompleteLocations(String gpx) { 110 SAXException saxException = assertThrows(SAXException.class, 111 () -> new GpxReader(new ByteArrayInputStream(gpx.getBytes(StandardCharsets.UTF_8))).parse(true)); 112 final String type; 113 if ("<wpt>".regionMatches(0, gpx, 5, 4)) { 114 type = "wpt"; 115 } else { 116 fail("You need to add code to tell us what the exception for \"" + gpx + "\" should be"); 117 type = null; 118 } 119 assertEquals(type + " element does not have valid latitude and/or longitude.", saxException.getMessage()); 120 } 102 121 }
Note:
See TracChangeset
for help on using the changeset viewer.