source: josm/trunk/src/org/openstreetmap/josm/io/session/SessionReader.java@ 11073

Last change on this file since 11073 was 11043, checked in by Don-vip, 8 years ago

see #13683 - display proper error message instead of throwing a RuntimeException

  • Property svn:eol-style set to native
File size: 25.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.session;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.GraphicsEnvironment;
7import java.io.BufferedInputStream;
8import java.io.File;
9import java.io.FileInputStream;
10import java.io.FileNotFoundException;
11import java.io.IOException;
12import java.io.InputStream;
13import java.lang.reflect.InvocationTargetException;
14import java.net.URI;
15import java.net.URISyntaxException;
16import java.nio.charset.StandardCharsets;
17import java.util.ArrayList;
18import java.util.Collections;
19import java.util.Enumeration;
20import java.util.HashMap;
21import java.util.List;
22import java.util.Map;
23import java.util.Map.Entry;
24import java.util.TreeMap;
25import java.util.zip.ZipEntry;
26import java.util.zip.ZipException;
27import java.util.zip.ZipFile;
28
29import javax.swing.JOptionPane;
30import javax.swing.SwingUtilities;
31import javax.xml.parsers.ParserConfigurationException;
32
33import org.openstreetmap.josm.Main;
34import org.openstreetmap.josm.data.ViewportData;
35import org.openstreetmap.josm.data.coor.EastNorth;
36import org.openstreetmap.josm.data.coor.LatLon;
37import org.openstreetmap.josm.data.projection.Projection;
38import org.openstreetmap.josm.data.projection.Projections;
39import org.openstreetmap.josm.gui.ExtendedDialog;
40import org.openstreetmap.josm.gui.layer.Layer;
41import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
42import org.openstreetmap.josm.gui.progress.ProgressMonitor;
43import org.openstreetmap.josm.io.Compression;
44import org.openstreetmap.josm.io.IllegalDataException;
45import org.openstreetmap.josm.tools.MultiMap;
46import org.openstreetmap.josm.tools.Utils;
47import org.w3c.dom.Document;
48import org.w3c.dom.Element;
49import org.w3c.dom.Node;
50import org.w3c.dom.NodeList;
51import org.xml.sax.SAXException;
52
53/**
54 * Reads a .jos session file and loads the layers in the process.
55 * @since 4668
56 */
57public class SessionReader {
58
59 private static final Map<String, Class<? extends SessionLayerImporter>> sessionLayerImporters = new HashMap<>();
60
61 private URI sessionFileURI;
62 private boolean zip; // true, if session file is a .joz file; false if it is a .jos file
63 private ZipFile zipFile;
64 private List<Layer> layers = new ArrayList<>();
65 private int active = -1;
66 private final List<Runnable> postLoadTasks = new ArrayList<>();
67 private ViewportData viewport;
68
69 static {
70 registerSessionLayerImporter("osm-data", OsmDataSessionImporter.class);
71 registerSessionLayerImporter("imagery", ImagerySessionImporter.class);
72 registerSessionLayerImporter("tracks", GpxTracksSessionImporter.class);
73 registerSessionLayerImporter("geoimage", GeoImageSessionImporter.class);
74 registerSessionLayerImporter("markers", MarkerSessionImporter.class);
75 registerSessionLayerImporter("osm-notes", NoteSessionImporter.class);
76 }
77
78 /**
79 * Register a session layer importer.
80 *
81 * @param layerType layer type
82 * @param importer importer for this layer class
83 */
84 public static void registerSessionLayerImporter(String layerType, Class<? extends SessionLayerImporter> importer) {
85 sessionLayerImporters.put(layerType, importer);
86 }
87
88 /**
89 * Returns the session layer importer for the given layer type.
90 * @param layerType layer type to import
91 * @return session layer importer for the given layer
92 */
93 public static SessionLayerImporter getSessionLayerImporter(String layerType) {
94 Class<? extends SessionLayerImporter> importerClass = sessionLayerImporters.get(layerType);
95 if (importerClass == null)
96 return null;
97 SessionLayerImporter importer = null;
98 try {
99 importer = importerClass.getConstructor().newInstance();
100 } catch (ReflectiveOperationException e) {
101 throw new RuntimeException(e);
102 }
103 return importer;
104 }
105
106 /**
107 * @return list of layers that are later added to the mapview
108 */
109 public List<Layer> getLayers() {
110 return layers;
111 }
112
113 /**
114 * @return active layer, or {@code null} if not set
115 * @since 6271
116 */
117 public Layer getActive() {
118 // layers is in reverse order because of the way TreeMap is built
119 return (active >= 0 && active < layers.size()) ? layers.get(layers.size()-1-active) : null;
120 }
121
122 /**
123 * @return actions executed in EDT after layers have been added (message dialog, etc.)
124 */
125 public List<Runnable> getPostLoadTasks() {
126 return postLoadTasks;
127 }
128
129 /**
130 * Return the viewport (map position and scale).
131 * @return The viewport. Can be null when no viewport info is found in the file.
132 */
133 public ViewportData getViewport() {
134 return viewport;
135 }
136
137 /**
138 * A class that provides some context for the individual {@link SessionLayerImporter}
139 * when doing the import.
140 */
141 public class ImportSupport {
142
143 private final String layerName;
144 private final int layerIndex;
145 private final List<LayerDependency> layerDependencies;
146
147 /**
148 * Path of the file inside the zip archive.
149 * Used as alternative return value for getFile method.
150 */
151 private String inZipPath;
152
153 /**
154 * Constructs a new {@code ImportSupport}.
155 * @param layerName layer name
156 * @param layerIndex layer index
157 * @param layerDependencies layer dependencies
158 */
159 public ImportSupport(String layerName, int layerIndex, List<LayerDependency> layerDependencies) {
160 this.layerName = layerName;
161 this.layerIndex = layerIndex;
162 this.layerDependencies = layerDependencies;
163 }
164
165 /**
166 * Add a task, e.g. a message dialog, that should
167 * be executed in EDT after all layers have been added.
168 * @param task task to run in EDT
169 */
170 public void addPostLayersTask(Runnable task) {
171 postLoadTasks.add(task);
172 }
173
174 /**
175 * Return an InputStream for a URI from a .jos/.joz file.
176 *
177 * The following forms are supported:
178 *
179 * - absolute file (both .jos and .joz):
180 * "file:///home/user/data.osm"
181 * "file:/home/user/data.osm"
182 * "file:///C:/files/data.osm"
183 * "file:/C:/file/data.osm"
184 * "/home/user/data.osm"
185 * "C:\files\data.osm" (not a URI, but recognized by File constructor on Windows systems)
186 * - standalone .jos files:
187 * - relative uri:
188 * "save/data.osm"
189 * "../project2/data.osm"
190 * - for .joz files:
191 * - file inside zip archive:
192 * "layers/01/data.osm"
193 * - relativ to the .joz file:
194 * "../save/data.osm" ("../" steps out of the archive)
195 * @param uriStr URI as string
196 * @return the InputStream
197 *
198 * @throws IOException Thrown when no Stream can be opened for the given URI, e.g. when the linked file has been deleted.
199 */
200 public InputStream getInputStream(String uriStr) throws IOException {
201 File file = getFile(uriStr);
202 if (file != null) {
203 try {
204 return new BufferedInputStream(Compression.getUncompressedFileInputStream(file));
205 } catch (FileNotFoundException e) {
206 throw new IOException(tr("File ''{0}'' does not exist.", file.getPath()), e);
207 }
208 } else if (inZipPath != null) {
209 ZipEntry entry = zipFile.getEntry(inZipPath);
210 if (entry != null) {
211 return zipFile.getInputStream(entry);
212 }
213 }
214 throw new IOException(tr("Unable to locate file ''{0}''.", uriStr));
215 }
216
217 /**
218 * Return a File for a URI from a .jos/.joz file.
219 *
220 * Returns null if the URI points to a file inside the zip archive.
221 * In this case, inZipPath will be set to the corresponding path.
222 * @param uriStr the URI as string
223 * @return the resulting File
224 * @throws IOException if any I/O error occurs
225 */
226 public File getFile(String uriStr) throws IOException {
227 inZipPath = null;
228 try {
229 URI uri = new URI(uriStr);
230 if ("file".equals(uri.getScheme()))
231 // absolute path
232 return new File(uri);
233 else if (uri.getScheme() == null) {
234 // Check if this is an absolute path without 'file:' scheme part.
235 // At this point, (as an exception) platform dependent path separator will be recognized.
236 // (This form is discouraged, only for users that like to copy and paste a path manually.)
237 File file = new File(uriStr);
238 if (file.isAbsolute())
239 return file;
240 else {
241 // for relative paths, only forward slashes are permitted
242 if (isZip()) {
243 if (uri.getPath().startsWith("../")) {
244 // relative to session file - "../" step out of the archive
245 String relPath = uri.getPath().substring(3);
246 return new File(sessionFileURI.resolve(relPath));
247 } else {
248 // file inside zip archive
249 inZipPath = uriStr;
250 return null;
251 }
252 } else
253 return new File(sessionFileURI.resolve(uri));
254 }
255 } else
256 throw new IOException(tr("Unsupported scheme ''{0}'' in URI ''{1}''.", uri.getScheme(), uriStr));
257 } catch (URISyntaxException e) {
258 throw new IOException(e);
259 }
260 }
261
262 /**
263 * Determines if we are reading from a .joz file.
264 * @return {@code true} if we are reading from a .joz file, {@code false} otherwise
265 */
266 public boolean isZip() {
267 return zip;
268 }
269
270 /**
271 * Name of the layer that is currently imported.
272 * @return layer name
273 */
274 public String getLayerName() {
275 return layerName;
276 }
277
278 /**
279 * Index of the layer that is currently imported.
280 * @return layer index
281 */
282 public int getLayerIndex() {
283 return layerIndex;
284 }
285
286 /**
287 * Dependencies - maps the layer index to the importer of the given
288 * layer. All the dependent importers have loaded completely at this point.
289 * @return layer dependencies
290 */
291 public List<LayerDependency> getLayerDependencies() {
292 return layerDependencies;
293 }
294
295 @Override
296 public String toString() {
297 return "ImportSupport [layerName=" + layerName + ", layerIndex=" + layerIndex + ", layerDependencies="
298 + layerDependencies + ", inZipPath=" + inZipPath + ']';
299 }
300 }
301
302 public static class LayerDependency {
303 private final Integer index;
304 private final Layer layer;
305 private final SessionLayerImporter importer;
306
307 public LayerDependency(Integer index, Layer layer, SessionLayerImporter importer) {
308 this.index = index;
309 this.layer = layer;
310 this.importer = importer;
311 }
312
313 public SessionLayerImporter getImporter() {
314 return importer;
315 }
316
317 public Integer getIndex() {
318 return index;
319 }
320
321 public Layer getLayer() {
322 return layer;
323 }
324 }
325
326 private static void error(String msg) throws IllegalDataException {
327 throw new IllegalDataException(msg);
328 }
329
330 private void parseJos(Document doc, ProgressMonitor progressMonitor) throws IllegalDataException {
331 Element root = doc.getDocumentElement();
332 if (!"josm-session".equals(root.getTagName())) {
333 error(tr("Unexpected root element ''{0}'' in session file", root.getTagName()));
334 }
335 String version = root.getAttribute("version");
336 if (!"0.1".equals(version)) {
337 error(tr("Version ''{0}'' of session file is not supported. Expected: 0.1", version));
338 }
339
340 Element viewportEl = getElementByTagName(root, "viewport");
341 if (viewportEl != null) {
342 EastNorth center = null;
343 Element centerEl = getElementByTagName(viewportEl, "center");
344 if (centerEl != null && centerEl.hasAttribute("lat") && centerEl.hasAttribute("lon")) {
345 try {
346 LatLon centerLL = new LatLon(Double.parseDouble(centerEl.getAttribute("lat")),
347 Double.parseDouble(centerEl.getAttribute("lon")));
348 center = Projections.project(centerLL);
349 } catch (NumberFormatException ex) {
350 Main.warn(ex);
351 }
352 }
353 if (center != null) {
354 Element scaleEl = getElementByTagName(viewportEl, "scale");
355 if (scaleEl != null && scaleEl.hasAttribute("meter-per-pixel")) {
356 try {
357 double meterPerPixel = Double.parseDouble(scaleEl.getAttribute("meter-per-pixel"));
358 Projection proj = Main.getProjection();
359 // Get a "typical" distance in east/north units that
360 // corresponds to a couple of pixels. Shouldn't be too
361 // large, to keep it within projection bounds and
362 // not too small to avoid rounding errors.
363 double dist = 0.01 * proj.getDefaultZoomInPPD();
364 LatLon ll1 = proj.eastNorth2latlon(new EastNorth(center.east() - dist, center.north()));
365 LatLon ll2 = proj.eastNorth2latlon(new EastNorth(center.east() + dist, center.north()));
366 double meterPerEasting = ll1.greatCircleDistance(ll2) / dist / 2;
367 double scale = meterPerPixel / meterPerEasting; // unit: easting per pixel
368 viewport = new ViewportData(center, scale);
369 } catch (NumberFormatException ex) {
370 Main.warn(ex);
371 }
372 }
373 }
374 }
375
376 Element layersEl = getElementByTagName(root, "layers");
377 if (layersEl == null) return;
378
379 String activeAtt = layersEl.getAttribute("active");
380 try {
381 active = (activeAtt != null && !activeAtt.isEmpty()) ? Integer.parseInt(activeAtt)-1 : -1;
382 } catch (NumberFormatException e) {
383 Main.warn("Unsupported value for 'active' layer attribute. Ignoring it. Error was: "+e.getMessage());
384 active = -1;
385 }
386
387 MultiMap<Integer, Integer> deps = new MultiMap<>();
388 Map<Integer, Element> elems = new HashMap<>();
389
390 NodeList nodes = layersEl.getChildNodes();
391
392 for (int i = 0; i < nodes.getLength(); ++i) {
393 Node node = nodes.item(i);
394 if (node.getNodeType() == Node.ELEMENT_NODE) {
395 Element e = (Element) node;
396 if ("layer".equals(e.getTagName())) {
397 if (!e.hasAttribute("index")) {
398 error(tr("missing mandatory attribute ''index'' for element ''layer''"));
399 }
400 Integer idx = null;
401 try {
402 idx = Integer.valueOf(e.getAttribute("index"));
403 } catch (NumberFormatException ex) {
404 Main.warn(ex);
405 }
406 if (idx == null) {
407 error(tr("unexpected format of attribute ''index'' for element ''layer''"));
408 }
409 if (elems.containsKey(idx)) {
410 error(tr("attribute ''index'' ({0}) for element ''layer'' must be unique", Integer.toString(idx)));
411 }
412 elems.put(idx, e);
413
414 deps.putVoid(idx);
415 String depStr = e.getAttribute("depends");
416 if (depStr != null && !depStr.isEmpty()) {
417 for (String sd : depStr.split(",")) {
418 Integer d = null;
419 try {
420 d = Integer.valueOf(sd);
421 } catch (NumberFormatException ex) {
422 Main.warn(ex);
423 }
424 if (d != null) {
425 deps.put(idx, d);
426 }
427 }
428 }
429 }
430 }
431 }
432
433 List<Integer> sorted = Utils.topologicalSort(deps);
434 final Map<Integer, Layer> layersMap = new TreeMap<>(Collections.reverseOrder());
435 final Map<Integer, SessionLayerImporter> importers = new HashMap<>();
436 final Map<Integer, String> names = new HashMap<>();
437
438 progressMonitor.setTicksCount(sorted.size());
439 LAYER: for (int idx: sorted) {
440 Element e = elems.get(idx);
441 if (e == null) {
442 error(tr("missing layer with index {0}", idx));
443 return;
444 } else if (!e.hasAttribute("name")) {
445 error(tr("missing mandatory attribute ''name'' for element ''layer''"));
446 return;
447 }
448 String name = e.getAttribute("name");
449 names.put(idx, name);
450 if (!e.hasAttribute("type")) {
451 error(tr("missing mandatory attribute ''type'' for element ''layer''"));
452 return;
453 }
454 String type = e.getAttribute("type");
455 SessionLayerImporter imp = getSessionLayerImporter(type);
456 if (imp == null && !GraphicsEnvironment.isHeadless()) {
457 CancelOrContinueDialog dialog = new CancelOrContinueDialog();
458 dialog.show(
459 tr("Unable to load layer"),
460 tr("Cannot load layer of type ''{0}'' because no suitable importer was found.", type),
461 JOptionPane.WARNING_MESSAGE,
462 progressMonitor
463 );
464 if (dialog.isCancel()) {
465 progressMonitor.cancel();
466 return;
467 } else {
468 continue;
469 }
470 } else if (imp != null) {
471 importers.put(idx, imp);
472 List<LayerDependency> depsImp = new ArrayList<>();
473 for (int d : deps.get(idx)) {
474 SessionLayerImporter dImp = importers.get(d);
475 if (dImp == null) {
476 CancelOrContinueDialog dialog = new CancelOrContinueDialog();
477 dialog.show(
478 tr("Unable to load layer"),
479 tr("Cannot load layer {0} because it depends on layer {1} which has been skipped.", idx, d),
480 JOptionPane.WARNING_MESSAGE,
481 progressMonitor
482 );
483 if (dialog.isCancel()) {
484 progressMonitor.cancel();
485 return;
486 } else {
487 continue LAYER;
488 }
489 }
490 depsImp.add(new LayerDependency(d, layersMap.get(d), dImp));
491 }
492 ImportSupport support = new ImportSupport(name, idx, depsImp);
493 Layer layer = null;
494 Exception exception = null;
495 try {
496 layer = imp.load(e, support, progressMonitor.createSubTaskMonitor(1, false));
497 if (layer == null) {
498 throw new IllegalStateException("Importer " + imp + " returned null for " + support);
499 }
500 } catch (IllegalDataException | IllegalStateException | IOException ex) {
501 exception = ex;
502 }
503 if (exception != null) {
504 Main.error(exception);
505 if (!GraphicsEnvironment.isHeadless()) {
506 CancelOrContinueDialog dialog = new CancelOrContinueDialog();
507 dialog.show(
508 tr("Error loading layer"),
509 tr("<html>Could not load layer {0} ''{1}''.<br>Error is:<br>{2}</html>", idx, name, exception.getMessage()),
510 JOptionPane.ERROR_MESSAGE,
511 progressMonitor
512 );
513 if (dialog.isCancel()) {
514 progressMonitor.cancel();
515 return;
516 } else {
517 continue;
518 }
519 }
520 }
521
522 layersMap.put(idx, layer);
523 }
524 progressMonitor.worked(1);
525 }
526
527 layers = new ArrayList<>();
528 for (Entry<Integer, Layer> entry : layersMap.entrySet()) {
529 Layer layer = entry.getValue();
530 if (layer == null) {
531 continue;
532 }
533 Element el = elems.get(entry.getKey());
534 if (el.hasAttribute("visible")) {
535 layer.setVisible(Boolean.parseBoolean(el.getAttribute("visible")));
536 }
537 if (el.hasAttribute("opacity")) {
538 try {
539 double opacity = Double.parseDouble(el.getAttribute("opacity"));
540 layer.setOpacity(opacity);
541 } catch (NumberFormatException ex) {
542 Main.warn(ex);
543 }
544 }
545 layer.setName(names.get(entry.getKey()));
546 layers.add(layer);
547 }
548 }
549
550 /**
551 * Show Dialog when there is an error for one layer.
552 * Ask the user whether to cancel the complete session loading or just to skip this layer.
553 *
554 * This is expected to run in a worker thread (PleaseWaitRunnable), so invokeAndWait is
555 * needed to block the current thread and wait for the result of the modal dialog from EDT.
556 */
557 private static class CancelOrContinueDialog {
558
559 private boolean cancel;
560
561 public void show(final String title, final String message, final int icon, final ProgressMonitor progressMonitor) {
562 try {
563 SwingUtilities.invokeAndWait(() -> {
564 ExtendedDialog dlg = new ExtendedDialog(
565 Main.parent,
566 title,
567 new String[] {tr("Cancel"), tr("Skip layer and continue")}
568 );
569 dlg.setButtonIcons(new String[] {"cancel", "dialogs/next"});
570 dlg.setIcon(icon);
571 dlg.setContent(message);
572 dlg.showDialog();
573 cancel = dlg.getValue() != 2;
574 });
575 } catch (InvocationTargetException | InterruptedException ex) {
576 throw new RuntimeException(ex);
577 }
578 }
579
580 public boolean isCancel() {
581 return cancel;
582 }
583 }
584
585 /**
586 * Loads session from the given file.
587 * @param sessionFile session file to load
588 * @param zip {@code true} if it's a zipped session (.joz)
589 * @param progressMonitor progress monitor
590 * @throws IllegalDataException if invalid data is detected
591 * @throws IOException if any I/O error occurs
592 */
593 public void loadSession(File sessionFile, boolean zip, ProgressMonitor progressMonitor) throws IllegalDataException, IOException {
594 try (InputStream josIS = createInputStream(sessionFile, zip)) {
595 loadSession(josIS, sessionFile.toURI(), zip, progressMonitor != null ? progressMonitor : NullProgressMonitor.INSTANCE);
596 }
597 }
598
599 private InputStream createInputStream(File sessionFile, boolean zip) throws IOException, IllegalDataException {
600 if (zip) {
601 try {
602 zipFile = new ZipFile(sessionFile, StandardCharsets.UTF_8);
603 return getZipInputStream(zipFile);
604 } catch (ZipException ze) {
605 throw new IOException(ze);
606 }
607 } else {
608 try {
609 return new FileInputStream(sessionFile);
610 } catch (FileNotFoundException ex) {
611 throw new IOException(ex);
612 }
613 }
614 }
615
616 private static InputStream getZipInputStream(ZipFile zipFile) throws IOException, IllegalDataException {
617 ZipEntry josEntry = null;
618 Enumeration<? extends ZipEntry> entries = zipFile.entries();
619 while (entries.hasMoreElements()) {
620 ZipEntry entry = entries.nextElement();
621 if (Utils.hasExtension(entry.getName(), "jos")) {
622 josEntry = entry;
623 break;
624 }
625 }
626 if (josEntry == null) {
627 error(tr("expected .jos file inside .joz archive"));
628 }
629 return zipFile.getInputStream(josEntry);
630 }
631
632 private void loadSession(InputStream josIS, URI sessionFileURI, boolean zip, ProgressMonitor progressMonitor)
633 throws IOException, IllegalDataException {
634
635 this.sessionFileURI = sessionFileURI;
636 this.zip = zip;
637
638 try {
639 parseJos(Utils.parseSafeDOM(josIS), progressMonitor);
640 } catch (SAXException e) {
641 throw new IllegalDataException(e);
642 } catch (ParserConfigurationException e) {
643 throw new IOException(e);
644 }
645 }
646
647 private static Element getElementByTagName(Element root, String name) {
648 NodeList els = root.getElementsByTagName(name);
649 return els.getLength() > 0 ? (Element) els.item(0) : null;
650 }
651}
Note: See TracBrowser for help on using the repository browser.