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

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

checkstyle: enable relevant whitespace checks and fix them

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