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

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

see #11924 - Java 9 - JDK-6850612 deprecates Class.newInstance() ==> replace it by Class.getConstructor().newInstance()

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