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

Last change on this file since 7509 was 7326, checked in by Don-vip, 10 years ago

fix #10292 - allow to load a session with NMEA file + enhance reading/writing unit tests for sessions

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