source: josm/trunk/src/org/openstreetmap/josm/Main.java@ 8017

Last change on this file since 8017 was 8015, checked in by bastiK, 9 years ago

fixed #10989 - I18n display support for major Asian scripts (Tamil, Bengali, ...)

adds more scripts for Windows, including Tibet, Khmer, Lao, Mongolian, Myanmar
Only works if corresponding font is installed, so basically starting
with a certain Windows version. See source code for details.

Advanced pref font.extended-unicode.added-items renamed to
font.extended-unicode.extra-items, type is now list-of-maps.

  • Property svn:eol-style set to native
File size: 61.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BorderLayout;
7import java.awt.Component;
8import java.awt.GridBagConstraints;
9import java.awt.GridBagLayout;
10import java.awt.Window;
11import java.awt.event.ComponentEvent;
12import java.awt.event.ComponentListener;
13import java.awt.event.KeyEvent;
14import java.awt.event.WindowAdapter;
15import java.awt.event.WindowEvent;
16import java.io.File;
17import java.lang.ref.WeakReference;
18import java.net.URI;
19import java.net.URISyntaxException;
20import java.net.URL;
21import java.text.MessageFormat;
22import java.util.ArrayList;
23import java.util.Arrays;
24import java.util.Collection;
25import java.util.Collections;
26import java.util.HashMap;
27import java.util.HashSet;
28import java.util.Iterator;
29import java.util.List;
30import java.util.Map;
31import java.util.Objects;
32import java.util.Set;
33import java.util.StringTokenizer;
34import java.util.concurrent.Callable;
35import java.util.concurrent.ExecutorService;
36import java.util.concurrent.Executors;
37import java.util.concurrent.Future;
38import java.util.logging.Handler;
39import java.util.logging.Level;
40import java.util.logging.LogRecord;
41import java.util.logging.Logger;
42
43import javax.swing.Action;
44import javax.swing.InputMap;
45import javax.swing.JComponent;
46import javax.swing.JFrame;
47import javax.swing.JOptionPane;
48import javax.swing.JPanel;
49import javax.swing.JTextArea;
50import javax.swing.KeyStroke;
51import javax.swing.LookAndFeel;
52import javax.swing.UIManager;
53import javax.swing.UnsupportedLookAndFeelException;
54
55import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
56import org.openstreetmap.josm.actions.JosmAction;
57import org.openstreetmap.josm.actions.OpenFileAction;
58import org.openstreetmap.josm.actions.downloadtasks.DownloadGpsTask;
59import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
60import org.openstreetmap.josm.actions.downloadtasks.DownloadTask;
61import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
62import org.openstreetmap.josm.actions.mapmode.DrawAction;
63import org.openstreetmap.josm.actions.mapmode.MapMode;
64import org.openstreetmap.josm.actions.search.SearchAction;
65import org.openstreetmap.josm.data.Bounds;
66import org.openstreetmap.josm.data.Preferences;
67import org.openstreetmap.josm.data.ProjectionBounds;
68import org.openstreetmap.josm.data.UndoRedoHandler;
69import org.openstreetmap.josm.data.ViewportData;
70import org.openstreetmap.josm.data.coor.CoordinateFormat;
71import org.openstreetmap.josm.data.coor.LatLon;
72import org.openstreetmap.josm.data.osm.DataSet;
73import org.openstreetmap.josm.data.osm.OsmPrimitive;
74import org.openstreetmap.josm.data.osm.PrimitiveDeepCopy;
75import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
76import org.openstreetmap.josm.data.projection.Projection;
77import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
78import org.openstreetmap.josm.data.validation.OsmValidator;
79import org.openstreetmap.josm.gui.GettingStarted;
80import org.openstreetmap.josm.gui.MainApplication.Option;
81import org.openstreetmap.josm.gui.MainMenu;
82import org.openstreetmap.josm.gui.MapFrame;
83import org.openstreetmap.josm.gui.MapFrameListener;
84import org.openstreetmap.josm.gui.MapView;
85import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
86import org.openstreetmap.josm.gui.help.HelpUtil;
87import org.openstreetmap.josm.gui.io.SaveLayersDialog;
88import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
89import org.openstreetmap.josm.gui.layer.Layer;
90import org.openstreetmap.josm.gui.layer.OsmDataLayer;
91import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener;
92import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
93import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference;
94import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference;
95import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
96import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
97import org.openstreetmap.josm.gui.progress.ProgressMonitorExecutor;
98import org.openstreetmap.josm.gui.tagging.TaggingPresets;
99import org.openstreetmap.josm.gui.util.RedirectInputMap;
100import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
101import org.openstreetmap.josm.io.FileWatcher;
102import org.openstreetmap.josm.io.OnlineResource;
103import org.openstreetmap.josm.io.OsmApi;
104import org.openstreetmap.josm.plugins.PluginHandler;
105import org.openstreetmap.josm.tools.CheckParameterUtil;
106import org.openstreetmap.josm.tools.I18n;
107import org.openstreetmap.josm.tools.ImageProvider;
108import org.openstreetmap.josm.tools.OpenBrowser;
109import org.openstreetmap.josm.tools.OsmUrlToBounds;
110import org.openstreetmap.josm.tools.PlatformHook;
111import org.openstreetmap.josm.tools.PlatformHookOsx;
112import org.openstreetmap.josm.tools.PlatformHookUnixoid;
113import org.openstreetmap.josm.tools.PlatformHookWindows;
114import org.openstreetmap.josm.tools.Shortcut;
115import org.openstreetmap.josm.tools.Utils;
116import org.openstreetmap.josm.tools.WindowGeometry;
117
118/**
119 * Abstract class holding various static global variables and methods used in large parts of JOSM application.
120 * @since 98
121 */
122public abstract class Main {
123
124 /**
125 * The JOSM website URL.
126 * @since 6897 (was public from 6143 to 6896)
127 */
128 private static final String JOSM_WEBSITE = "https://josm.openstreetmap.de";
129
130 /**
131 * The OSM website URL.
132 * @since 6897 (was public from 6453 to 6896)
133 */
134 private static final String OSM_WEBSITE = "https://www.openstreetmap.org";
135
136 /**
137 * Replies true if JOSM currently displays a map view. False, if it doesn't, i.e. if
138 * it only shows the MOTD panel.
139 *
140 * @return <code>true</code> if JOSM currently displays a map view
141 */
142 public static boolean isDisplayingMapView() {
143 if (map == null) return false;
144 if (map.mapView == null) return false;
145 return true;
146 }
147
148 /**
149 * Global parent component for all dialogs and message boxes
150 */
151 public static Component parent;
152
153 /**
154 * Global application.
155 */
156 public static Main main;
157
158 /**
159 * Command-line arguments used to run the application.
160 */
161 public static String[] commandLineArgs;
162
163 /**
164 * The worker thread slave. This is for executing all long and intensive
165 * calculations. The executed runnables are guaranteed to be executed separately
166 * and sequential.
167 */
168 public static final ExecutorService worker = new ProgressMonitorExecutor();
169
170 /**
171 * Global application preferences
172 */
173 public static Preferences pref;
174
175 /**
176 * The global paste buffer.
177 */
178 public static final PrimitiveDeepCopy pasteBuffer = new PrimitiveDeepCopy();
179
180 /**
181 * The layer source from which {@link Main#pasteBuffer} data comes from.
182 */
183 public static Layer pasteSource;
184
185 /**
186 * The MapFrame. Use {@link Main#setMapFrame} to set or clear it.
187 */
188 public static MapFrame map;
189
190 /**
191 * The toolbar preference control to register new actions.
192 */
193 public static ToolbarPreferences toolbar;
194
195 /**
196 * The commands undo/redo handler.
197 */
198 public final UndoRedoHandler undoRedo = new UndoRedoHandler();
199
200 /**
201 * The progress monitor being currently displayed.
202 */
203 public static PleaseWaitProgressMonitor currentProgressMonitor;
204
205 /**
206 * The main menu bar at top of screen.
207 */
208 public MainMenu menu;
209
210 /**
211 * The data validation handler.
212 */
213 public OsmValidator validator;
214
215 /**
216 * The file watcher service.
217 */
218 public static final FileWatcher fileWatcher = new FileWatcher();
219
220 /**
221 * The MOTD Layer.
222 */
223 private GettingStarted gettingStarted = new GettingStarted();
224
225 private static final Collection<MapFrameListener> mapFrameListeners = new ArrayList<>();
226
227 protected static final Map<String, Throwable> NETWORK_ERRORS = new HashMap<>();
228
229 // First lines of last 5 error and warning messages, used for bug reports
230 private static final List<String> ERRORS_AND_WARNINGS = Collections.<String>synchronizedList(new ArrayList<String>());
231
232 private static final Set<OnlineResource> OFFLINE_RESOURCES = new HashSet<>();
233
234 /**
235 * Logging level (5 = trace, 4 = debug, 3 = info, 2 = warn, 1 = error, 0 = none).
236 * @since 6248
237 */
238 public static int logLevel = 3;
239
240 private static void rememberWarnErrorMsg(String msg) {
241 // Only remember first line of message
242 int idx = msg.indexOf('\n');
243 if (idx > 0) {
244 ERRORS_AND_WARNINGS.add(msg.substring(0, idx));
245 } else {
246 ERRORS_AND_WARNINGS.add(msg);
247 }
248 // Only keep 5 lines to avoid memory leak and incomplete stacktraces in bug reports
249 while (ERRORS_AND_WARNINGS.size() > 5) {
250 ERRORS_AND_WARNINGS.remove(0);
251 }
252 }
253
254 /**
255 * Replies the first lines of last 10 error and warning messages, used for bug reports
256 * @return the first lines of last 10 error and warning messages
257 * @since 7420
258 */
259 public static final Collection<String> getLastErrorAndWarnings() {
260 return Collections.unmodifiableList(ERRORS_AND_WARNINGS);
261 }
262
263 /**
264 * Prints an error message if logging is on.
265 * @param msg The message to print.
266 * @since 6248
267 */
268 public static void error(String msg) {
269 if (logLevel < 1)
270 return;
271 if (msg != null && !msg.isEmpty()) {
272 System.err.println(tr("ERROR: {0}", msg));
273 rememberWarnErrorMsg("E: "+msg);
274 }
275 }
276
277 /**
278 * Prints a warning message if logging is on.
279 * @param msg The message to print.
280 */
281 public static void warn(String msg) {
282 if (logLevel < 2)
283 return;
284 if (msg != null && !msg.isEmpty()) {
285 System.err.println(tr("WARNING: {0}", msg));
286 rememberWarnErrorMsg("W: "+msg);
287 }
288 }
289
290 /**
291 * Prints an informational message if logging is on.
292 * @param msg The message to print.
293 */
294 public static void info(String msg) {
295 if (logLevel < 3)
296 return;
297 if (msg != null && !msg.isEmpty()) {
298 System.out.println(tr("INFO: {0}", msg));
299 }
300 }
301
302 /**
303 * Prints a debug message if logging is on.
304 * @param msg The message to print.
305 */
306 public static void debug(String msg) {
307 if (logLevel < 4)
308 return;
309 if (msg != null && !msg.isEmpty()) {
310 System.out.println(tr("DEBUG: {0}", msg));
311 }
312 }
313
314 /**
315 * Prints a trace message if logging is on.
316 * @param msg The message to print.
317 */
318 public static void trace(String msg) {
319 if (logLevel < 5)
320 return;
321 if (msg != null && !msg.isEmpty()) {
322 System.out.print("TRACE: ");
323 System.out.println(msg);
324 }
325 }
326
327 /**
328 * Determines if debug log level is enabled.
329 * Useful to avoid costly construction of debug messages when not enabled.
330 * @return {@code true} if log level is at least debug, {@code false} otherwise
331 * @since 6852
332 */
333 public static boolean isDebugEnabled() {
334 return logLevel >= 4;
335 }
336
337 /**
338 * Determines if trace log level is enabled.
339 * Useful to avoid costly construction of trace messages when not enabled.
340 * @return {@code true} if log level is at least trace, {@code false} otherwise
341 * @since 6852
342 */
343 public static boolean isTraceEnabled() {
344 return logLevel >= 5;
345 }
346
347 /**
348 * Prints a formatted error message if logging is on. Calls {@link MessageFormat#format}
349 * function to format text.
350 * @param msg The formatted message to print.
351 * @param objects The objects to insert into format string.
352 * @since 6248
353 */
354 public static void error(String msg, Object... objects) {
355 error(MessageFormat.format(msg, objects));
356 }
357
358 /**
359 * Prints a formatted warning message if logging is on. Calls {@link MessageFormat#format}
360 * function to format text.
361 * @param msg The formatted message to print.
362 * @param objects The objects to insert into format string.
363 */
364 public static void warn(String msg, Object... objects) {
365 warn(MessageFormat.format(msg, objects));
366 }
367
368 /**
369 * Prints a formatted informational message if logging is on. Calls {@link MessageFormat#format}
370 * function to format text.
371 * @param msg The formatted message to print.
372 * @param objects The objects to insert into format string.
373 */
374 public static void info(String msg, Object... objects) {
375 info(MessageFormat.format(msg, objects));
376 }
377
378 /**
379 * Prints a formatted debug message if logging is on. Calls {@link MessageFormat#format}
380 * function to format text.
381 * @param msg The formatted message to print.
382 * @param objects The objects to insert into format string.
383 */
384 public static void debug(String msg, Object... objects) {
385 debug(MessageFormat.format(msg, objects));
386 }
387
388 /**
389 * Prints a formatted trace message if logging is on. Calls {@link MessageFormat#format}
390 * function to format text.
391 * @param msg The formatted message to print.
392 * @param objects The objects to insert into format string.
393 */
394 public static void trace(String msg, Object... objects) {
395 trace(MessageFormat.format(msg, objects));
396 }
397
398 /**
399 * Prints an error message for the given Throwable.
400 * @param t The throwable object causing the error
401 * @since 6248
402 */
403 public static void error(Throwable t) {
404 error(t, true);
405 }
406
407 /**
408 * Prints a warning message for the given Throwable.
409 * @param t The throwable object causing the error
410 * @since 6248
411 */
412 public static void warn(Throwable t) {
413 warn(t, true);
414 }
415
416 /**
417 * Prints an error message for the given Throwable.
418 * @param t The throwable object causing the error
419 * @param stackTrace {@code true}, if the stacktrace should be displayed
420 * @since 6642
421 */
422 public static void error(Throwable t, boolean stackTrace) {
423 error(getErrorMessage(t));
424 if (stackTrace) {
425 t.printStackTrace();
426 }
427 }
428
429 /**
430 * Prints a warning message for the given Throwable.
431 * @param t The throwable object causing the error
432 * @param stackTrace {@code true}, if the stacktrace should be displayed
433 * @since 6642
434 */
435 public static void warn(Throwable t, boolean stackTrace) {
436 warn(getErrorMessage(t));
437 if (stackTrace) {
438 t.printStackTrace();
439 }
440 }
441
442 /**
443 * Returns a human-readable message of error, also usable for developers.
444 * @param t The error
445 * @return The human-readable error message
446 * @since 6642
447 */
448 public static String getErrorMessage(Throwable t) {
449 if (t == null) {
450 return null;
451 }
452 StringBuilder sb = new StringBuilder(t.getClass().getName());
453 String msg = t.getMessage();
454 if (msg != null) {
455 sb.append(": ").append(msg.trim());
456 }
457 Throwable cause = t.getCause();
458 if (cause != null && !cause.equals(t)) {
459 sb.append(". ").append(tr("Cause: ")).append(getErrorMessage(cause));
460 }
461 return sb.toString();
462 }
463
464 /**
465 * Platform specific code goes in here.
466 * Plugins may replace it, however, some hooks will be called before any plugins have been loeaded.
467 * So if you need to hook into those early ones, split your class and send the one with the early hooks
468 * to the JOSM team for inclusion.
469 */
470 public static PlatformHook platform;
471
472 /**
473 * Whether or not the java vm is openjdk
474 * We use this to work around openjdk bugs
475 */
476 public static boolean isOpenjdk;
477
478 /**
479 * Initializes {@code Main.pref} in normal application context.
480 * @since 6471
481 */
482 public static void initApplicationPreferences() {
483 Main.pref = new Preferences();
484 }
485
486 /**
487 * Set or clear (if passed <code>null</code>) the map.
488 * @param map The map to set {@link Main#map} to. Can be null.
489 */
490 public final void setMapFrame(final MapFrame map) {
491 MapFrame old = Main.map;
492 panel.setVisible(false);
493 panel.removeAll();
494 if (map != null) {
495 map.fillPanel(panel);
496 } else {
497 old.destroy();
498 panel.add(gettingStarted, BorderLayout.CENTER);
499 }
500 panel.setVisible(true);
501 redoUndoListener.commandChanged(0,0);
502
503 Main.map = map;
504
505 for (MapFrameListener listener : mapFrameListeners ) {
506 listener.mapFrameInitialized(old, map);
507 }
508 if (map == null && currentProgressMonitor != null) {
509 currentProgressMonitor.showForegroundDialog();
510 }
511 }
512
513 /**
514 * Remove the specified layer from the map. If it is the last layer,
515 * remove the map as well.
516 * @param layer The layer to remove
517 */
518 public final synchronized void removeLayer(final Layer layer) {
519 if (map != null) {
520 map.mapView.removeLayer(layer);
521 if (isDisplayingMapView() && map.mapView.getAllLayers().isEmpty()) {
522 setMapFrame(null);
523 }
524 }
525 }
526
527 private static InitStatusListener initListener = null;
528
529 public static interface InitStatusListener {
530
531 void updateStatus(String event);
532 }
533
534 public static void setInitStatusListener(InitStatusListener listener) {
535 initListener = listener;
536 }
537
538 /**
539 * Constructs new {@code Main} object. A lot of global variables are initialized here.
540 */
541 public Main() {
542 main = this;
543 isOpenjdk = System.getProperty("java.vm.name").toUpperCase().indexOf("OPENJDK") != -1;
544
545 if (initListener != null) {
546 initListener.updateStatus(tr("Executing platform startup hook"));
547 }
548 platform.startupHook();
549
550 if (initListener != null) {
551 initListener.updateStatus(tr("Building main menu"));
552 }
553 contentPanePrivate.add(panel, BorderLayout.CENTER);
554 panel.add(gettingStarted, BorderLayout.CENTER);
555 menu = new MainMenu();
556
557 undoRedo.addCommandQueueListener(redoUndoListener);
558
559 // creating toolbar
560 contentPanePrivate.add(toolbar.control, BorderLayout.NORTH);
561
562 registerActionShortcut(menu.help, Shortcut.registerShortcut("system:help", tr("Help"),
563 KeyEvent.VK_F1, Shortcut.DIRECT));
564
565 // contains several initialization tasks to be executed (in parallel) by a ExecutorService
566 List<Callable<Void>> tasks = new ArrayList<>();
567
568 tasks.add(new InitializationTask(tr("Initializing OSM API")) {
569
570 @Override
571 public void initialize() throws Exception {
572 // We try to establish an API connection early, so that any API
573 // capabilities are already known to the editor instance. However
574 // if it goes wrong that's not critical at this stage.
575 try {
576 OsmApi.getOsmApi().initialize(null, true);
577 } catch (Exception e) {
578 Main.warn(getErrorMessage(Utils.getRootCause(e)));
579 }
580 }
581 });
582
583 tasks.add(new InitializationTask(tr("Initializing validator")) {
584
585 @Override
586 public void initialize() throws Exception {
587 validator = new OsmValidator();
588 MapView.addLayerChangeListener(validator);
589 }
590 });
591
592 tasks.add(new InitializationTask(tr("Initializing presets")) {
593
594 @Override
595 public void initialize() throws Exception {
596 TaggingPresets.initialize();
597 }
598 });
599
600 tasks.add(new InitializationTask(tr("Initializing map styles")) {
601
602 @Override
603 public void initialize() throws Exception {
604 MapPaintPreference.initialize();
605 }
606 });
607
608 tasks.add(new InitializationTask(tr("Loading imagery preferences")) {
609
610 @Override
611 public void initialize() throws Exception {
612 ImageryPreference.initialize();
613 }
614 });
615
616 try {
617 for (Future<Void> i : Executors.newFixedThreadPool(
618 Runtime.getRuntime().availableProcessors()).invokeAll(tasks)) {
619 i.get();
620 }
621 } catch (Exception ex) {
622 throw new RuntimeException(ex);
623 }
624
625 // hooks for the jmapviewer component
626 FeatureAdapter.registerBrowserAdapter(new FeatureAdapter.BrowserAdapter() {
627 @Override
628 public void openLink(String url) {
629 OpenBrowser.displayUrl(url);
630 }
631 });
632 FeatureAdapter.registerTranslationAdapter(I18n.getTranslationAdapter());
633 FeatureAdapter.registerLoggingAdapter(new FeatureAdapter.LoggingAdapter() {
634 @Override
635 public Logger getLogger(String name) {
636 Logger logger = Logger.getAnonymousLogger();
637 logger.setUseParentHandlers(false);
638 logger.setLevel(Level.ALL);
639 if (logger.getHandlers().length == 0) {
640 logger.addHandler(new Handler() {
641 @Override
642 public void publish(LogRecord record) {
643 String msg = MessageFormat.format(record.getMessage(), record.getParameters());
644 if (record.getLevel().intValue() >= Level.SEVERE.intValue()) {
645 Main.error(msg);
646 } else if (record.getLevel().intValue() >= Level.WARNING.intValue()) {
647 Main.warn(msg);
648 } else if (record.getLevel().intValue() >= Level.INFO.intValue()) {
649 Main.info(msg);
650 } else if (record.getLevel().intValue() >= Level.FINE.intValue()) {
651 Main.debug(msg);
652 } else {
653 Main.trace(msg);
654 }
655 }
656
657 @Override
658 public void flush() {
659 }
660
661 @Override
662 public void close() throws SecurityException {
663 }
664 });
665 }
666 return logger;
667 }
668 });
669
670 if (initListener != null) {
671 initListener.updateStatus(tr("Updating user interface"));
672 }
673
674 toolbar.refreshToolbarControl();
675
676 toolbar.control.updateUI();
677 contentPanePrivate.updateUI();
678 }
679
680 private abstract class InitializationTask implements Callable<Void> {
681
682 private final String name;
683
684 protected InitializationTask(String name) {
685 this.name = name;
686 }
687
688 public abstract void initialize() throws Exception;
689
690 @Override
691 public Void call() throws Exception {
692 if (initListener != null) {
693 initListener.updateStatus(name);
694 }
695 final long startTime = System.currentTimeMillis();
696 initialize();
697 if (isDebugEnabled()) {
698 final long elapsedTime = System.currentTimeMillis() - startTime;
699 Main.debug(tr("{0} completed in {1}", name, Utils.getDurationString(elapsedTime)));
700 }
701 return null;
702 }
703 }
704
705 /**
706 * Add a new layer to the map.
707 *
708 * If no map exists, create one.
709 *
710 * @param layer the layer
711 *
712 * @see #addLayer(Layer, ProjectionBounds)
713 * @see #addLayer(Layer, ViewportData)
714 */
715 public final void addLayer(final Layer layer) {
716 BoundingXYVisitor v = new BoundingXYVisitor();
717 layer.visitBoundingBox(v);
718 addLayer(layer, v.getBounds());
719 }
720
721 /**
722 * Add a new layer to the map.
723 *
724 * If no map exists, create one.
725 *
726 * @param layer the layer
727 * @param bounds the bounds of the layer (target zoom area); can be null, then
728 * the viewport isn't changed
729 */
730 public final synchronized void addLayer(final Layer layer, ProjectionBounds bounds) {
731 addLayer(layer, bounds == null ? null : new ViewportData(bounds));
732 }
733
734 /**
735 * Add a new layer to the map.
736 *
737 * If no map exists, create one.
738 *
739 * @param layer the layer
740 * @param viewport the viewport to zoom to; can be null, then the viewport
741 * isn't changed
742 */
743 public final synchronized void addLayer(final Layer layer, ViewportData viewport) {
744 boolean noMap = map == null;
745 if (noMap) {
746 createMapFrame(layer, viewport);
747 }
748 layer.hookUpMapView();
749 map.mapView.addLayer(layer);
750 if (noMap) {
751 Main.map.setVisible(true);
752 } else if (viewport != null) {
753 Main.map.mapView.zoomTo(viewport);
754 }
755 }
756
757 public synchronized void createMapFrame(Layer firstLayer, ViewportData viewportData) {
758 MapFrame mapFrame = new MapFrame(contentPanePrivate, viewportData);
759 setMapFrame(mapFrame);
760 if (firstLayer != null) {
761 mapFrame.selectMapMode((MapMode)mapFrame.getDefaultButtonAction(), firstLayer);
762 }
763 mapFrame.initializeDialogsPane();
764 // bootstrapping problem: make sure the layer list dialog is going to
765 // listen to change events of the very first layer
766 //
767 if (firstLayer != null) {
768 firstLayer.addPropertyChangeListener(LayerListDialog.getInstance().getModel());
769 }
770 }
771
772 /**
773 * Replies <code>true</code> if there is an edit layer
774 *
775 * @return <code>true</code> if there is an edit layer
776 */
777 public boolean hasEditLayer() {
778 if (getEditLayer() == null) return false;
779 return true;
780 }
781
782 /**
783 * Replies the current edit layer
784 *
785 * @return the current edit layer. <code>null</code>, if no current edit layer exists
786 */
787 public OsmDataLayer getEditLayer() {
788 if (!isDisplayingMapView()) return null;
789 return map.mapView.getEditLayer();
790 }
791
792 /**
793 * Replies the current data set.
794 *
795 * @return the current data set. <code>null</code>, if no current data set exists
796 */
797 public DataSet getCurrentDataSet() {
798 if (!hasEditLayer()) return null;
799 return getEditLayer().data;
800 }
801
802 /**
803 * Replies the current selected primitives, from a end-user point of view.
804 * It is not always technically the same collection of primitives than {@link DataSet#getSelected()}.
805 * Indeed, if the user is currently in drawing mode, only the way currently being drawn is returned,
806 * see {@link DrawAction#getInProgressSelection()}.
807 *
808 * @return The current selected primitives, from a end-user point of view. Can be {@code null}.
809 * @since 6546
810 */
811 public Collection<OsmPrimitive> getInProgressSelection() {
812 if (map != null && map.mapMode instanceof DrawAction) {
813 return ((DrawAction) map.mapMode).getInProgressSelection();
814 } else {
815 DataSet ds = getCurrentDataSet();
816 if (ds == null) return null;
817 return ds.getSelected();
818 }
819 }
820
821 /**
822 * Returns the currently active layer
823 *
824 * @return the currently active layer. <code>null</code>, if currently no active layer exists
825 */
826 public Layer getActiveLayer() {
827 if (!isDisplayingMapView()) return null;
828 return map.mapView.getActiveLayer();
829 }
830
831 protected static final JPanel contentPanePrivate = new JPanel(new BorderLayout());
832
833 public static void redirectToMainContentPane(JComponent source) {
834 RedirectInputMap.redirect(source, contentPanePrivate);
835 }
836
837 public static void registerActionShortcut(JosmAction action) {
838 registerActionShortcut(action, action.getShortcut());
839 }
840
841 public static void registerActionShortcut(Action action, Shortcut shortcut) {
842 KeyStroke keyStroke = shortcut.getKeyStroke();
843 if (keyStroke == null)
844 return;
845
846 InputMap inputMap = contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
847 Object existing = inputMap.get(keyStroke);
848 if (existing != null && !existing.equals(action)) {
849 info(String.format("Keystroke %s is already assigned to %s, will be overridden by %s", keyStroke, existing, action));
850 }
851 inputMap.put(keyStroke, action);
852
853 contentPanePrivate.getActionMap().put(action, action);
854 }
855
856 public static void unregisterShortcut(Shortcut shortcut) {
857 contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).remove(shortcut.getKeyStroke());
858 }
859
860 public static void unregisterActionShortcut(JosmAction action) {
861 unregisterActionShortcut(action, action.getShortcut());
862 }
863
864 public static void unregisterActionShortcut(Action action, Shortcut shortcut) {
865 unregisterShortcut(shortcut);
866 contentPanePrivate.getActionMap().remove(action);
867 }
868
869 /**
870 * Replies the registered action for the given shortcut
871 * @param shortcut The shortcut to look for
872 * @return the registered action for the given shortcut
873 * @since 5696
874 */
875 public static Action getRegisteredActionShortcut(Shortcut shortcut) {
876 KeyStroke keyStroke = shortcut.getKeyStroke();
877 if (keyStroke == null)
878 return null;
879 Object action = contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).get(keyStroke);
880 if (action instanceof Action)
881 return (Action) action;
882 return null;
883 }
884
885 ///////////////////////////////////////////////////////////////////////////
886 // Implementation part
887 ///////////////////////////////////////////////////////////////////////////
888
889 /**
890 * Global panel.
891 */
892 public static final JPanel panel = new JPanel(new BorderLayout());
893
894 protected static WindowGeometry geometry;
895 protected static int windowState = JFrame.NORMAL;
896
897 private final CommandQueueListener redoUndoListener = new CommandQueueListener(){
898 @Override
899 public void commandChanged(final int queueSize, final int redoSize) {
900 menu.undo.setEnabled(queueSize > 0);
901 menu.redo.setEnabled(redoSize > 0);
902 }
903 };
904
905 /**
906 * Should be called before the main constructor to setup some parameter stuff
907 * @param args The parsed argument list.
908 */
909 public static void preConstructorInit(Map<Option, Collection<String>> args) {
910 ProjectionPreference.setProjection();
911
912 try {
913 String defaultlaf = platform.getDefaultStyle();
914 String laf = Main.pref.get("laf", defaultlaf);
915 try {
916 UIManager.setLookAndFeel(laf);
917 }
918 catch (final NoClassDefFoundError | ClassNotFoundException e) {
919 // Try to find look and feel in plugin classloaders
920 Class<?> klass = null;
921 for (ClassLoader cl : PluginHandler.getResourceClassLoaders()) {
922 try {
923 klass = cl.loadClass(laf);
924 break;
925 } catch (ClassNotFoundException ex) {
926 // Do nothing
927 }
928 }
929 if (klass != null && LookAndFeel.class.isAssignableFrom(klass)) {
930 try {
931 UIManager.setLookAndFeel((LookAndFeel) klass.newInstance());
932 } catch (Exception ex) {
933 warn("Cannot set Look and Feel: " + laf + ": "+ex.getMessage());
934 }
935 } else {
936 info("Look and Feel not found: " + laf);
937 Main.pref.put("laf", defaultlaf);
938 }
939 }
940 catch (final UnsupportedLookAndFeelException e) {
941 info("Look and Feel not supported: " + laf);
942 Main.pref.put("laf", defaultlaf);
943 }
944 toolbar = new ToolbarPreferences();
945 contentPanePrivate.updateUI();
946 panel.updateUI();
947 } catch (final Exception e) {
948 error(e);
949 }
950 UIManager.put("OptionPane.okIcon", ImageProvider.get("ok"));
951 UIManager.put("OptionPane.yesIcon", UIManager.get("OptionPane.okIcon"));
952 UIManager.put("OptionPane.cancelIcon", ImageProvider.get("cancel"));
953 UIManager.put("OptionPane.noIcon", UIManager.get("OptionPane.cancelIcon"));
954
955 I18n.translateJavaInternalMessages();
956
957 // init default coordinate format
958 //
959 try {
960 CoordinateFormat.setCoordinateFormat(CoordinateFormat.valueOf(Main.pref.get("coordinates")));
961 } catch (IllegalArgumentException iae) {
962 CoordinateFormat.setCoordinateFormat(CoordinateFormat.DECIMAL_DEGREES);
963 }
964
965 geometry = WindowGeometry.mainWindow("gui.geometry",
966 (args.containsKey(Option.GEOMETRY) ? args.get(Option.GEOMETRY).iterator().next() : null),
967 !args.containsKey(Option.NO_MAXIMIZE) && Main.pref.getBoolean("gui.maximized", false));
968 }
969
970 protected static void postConstructorProcessCmdLine(Map<Option, Collection<String>> args) {
971 if (args.containsKey(Option.DOWNLOAD)) {
972 List<File> fileList = new ArrayList<>();
973 for (String s : args.get(Option.DOWNLOAD)) {
974 File f = null;
975 switch(paramType(s)) {
976 case httpUrl:
977 downloadFromParamHttp(false, s);
978 break;
979 case bounds:
980 downloadFromParamBounds(false, s);
981 break;
982 case fileUrl:
983 try {
984 f = new File(new URI(s));
985 } catch (URISyntaxException e) {
986 JOptionPane.showMessageDialog(
987 Main.parent,
988 tr("Ignoring malformed file URL: \"{0}\"", s),
989 tr("Warning"),
990 JOptionPane.WARNING_MESSAGE
991 );
992 }
993 if (f!=null) {
994 fileList.add(f);
995 }
996 break;
997 case fileName:
998 f = new File(s);
999 fileList.add(f);
1000 break;
1001 }
1002 }
1003 if(!fileList.isEmpty())
1004 {
1005 OpenFileAction.openFiles(fileList, true);
1006 }
1007 }
1008 if (args.containsKey(Option.DOWNLOADGPS)) {
1009 for (String s : args.get(Option.DOWNLOADGPS)) {
1010 switch(paramType(s)) {
1011 case httpUrl:
1012 downloadFromParamHttp(true, s);
1013 break;
1014 case bounds:
1015 downloadFromParamBounds(true, s);
1016 break;
1017 case fileUrl:
1018 case fileName:
1019 JOptionPane.showMessageDialog(
1020 Main.parent,
1021 tr("Parameter \"downloadgps\" does not accept file names or file URLs"),
1022 tr("Warning"),
1023 JOptionPane.WARNING_MESSAGE
1024 );
1025 }
1026 }
1027 }
1028 if (args.containsKey(Option.SELECTION)) {
1029 for (String s : args.get(Option.SELECTION)) {
1030 SearchAction.search(s, SearchAction.SearchMode.add);
1031 }
1032 }
1033 }
1034
1035 /**
1036 * Asks user to perform "save layer" operations (save on disk and/or upload data to server) for all {@link AbstractModifiableLayer} before JOSM exits.
1037 * @return {@code true} if there was nothing to save, or if the user wants to proceed to save operations. {@code false} if the user cancels.
1038 * @since 2025
1039 */
1040 public static boolean saveUnsavedModifications() {
1041 if (!isDisplayingMapView()) return true;
1042 return saveUnsavedModifications(map.mapView.getLayersOfType(AbstractModifiableLayer.class), true);
1043 }
1044
1045 /**
1046 * Asks user to perform "save layer" operations (save on disk and/or upload data to server) before data layers deletion.
1047 *
1048 * @param selectedLayers The layers to check. Only instances of {@link AbstractModifiableLayer} are considered.
1049 * @param exit {@code true} if JOSM is exiting, {@code false} otherwise.
1050 * @return {@code true} if there was nothing to save, or if the user wants to proceed to save operations. {@code false} if the user cancels.
1051 * @since 5519
1052 */
1053 public static boolean saveUnsavedModifications(Iterable<? extends Layer> selectedLayers, boolean exit) {
1054 SaveLayersDialog dialog = new SaveLayersDialog(parent);
1055 List<AbstractModifiableLayer> layersWithUnmodifiedChanges = new ArrayList<>();
1056 for (Layer l: selectedLayers) {
1057 if (!(l instanceof AbstractModifiableLayer)) {
1058 continue;
1059 }
1060 AbstractModifiableLayer odl = (AbstractModifiableLayer)l;
1061 if ((odl.requiresSaveToFile() || (odl.requiresUploadToServer() && !odl.isUploadDiscouraged())) && odl.isModified()) {
1062 layersWithUnmodifiedChanges.add(odl);
1063 }
1064 }
1065 if (exit) {
1066 dialog.prepareForSavingAndUpdatingLayersBeforeExit();
1067 } else {
1068 dialog.prepareForSavingAndUpdatingLayersBeforeDelete();
1069 }
1070 if (!layersWithUnmodifiedChanges.isEmpty()) {
1071 dialog.getModel().populate(layersWithUnmodifiedChanges);
1072 dialog.setVisible(true);
1073 switch(dialog.getUserAction()) {
1074 case PROCEED: return true;
1075 case CANCEL:
1076 default: return false;
1077 }
1078 }
1079
1080 return true;
1081 }
1082
1083 /**
1084 * Closes JOSM and optionally terminates the Java Virtual Machine (JVM). If there are some unsaved data layers, asks first for user confirmation.
1085 * @param exit If {@code true}, the JVM is terminated by running {@link System#exit} with a given return code.
1086 * @param exitCode The return code
1087 * @return {@code true} if JOSM has been closed, {@code false} if the user has cancelled the operation.
1088 * @since 3378
1089 */
1090 public static boolean exitJosm(boolean exit, int exitCode) {
1091 if (Main.saveUnsavedModifications()) {
1092 geometry.remember("gui.geometry");
1093 if (map != null) {
1094 map.rememberToggleDialogWidth();
1095 }
1096 pref.put("gui.maximized", (windowState & JFrame.MAXIMIZED_BOTH) != 0);
1097 // Remove all layers because somebody may rely on layerRemoved events (like AutosaveTask)
1098 if (Main.isDisplayingMapView()) {
1099 Collection<Layer> layers = new ArrayList<>(Main.map.mapView.getAllLayers());
1100 for (Layer l: layers) {
1101 Main.main.removeLayer(l);
1102 }
1103 }
1104 if (exit) {
1105 System.exit(exitCode);
1106 }
1107 return true;
1108 }
1109 return false;
1110 }
1111
1112 /**
1113 * The type of a command line parameter, to be used in switch statements.
1114 * @see #paramType
1115 */
1116 private enum DownloadParamType { httpUrl, fileUrl, bounds, fileName }
1117
1118 /**
1119 * Guess the type of a parameter string specified on the command line with --download= or --downloadgps.
1120 * @param s A parameter string
1121 * @return The guessed parameter type
1122 */
1123 private static DownloadParamType paramType(String s) {
1124 if(s.startsWith("http:") || s.startsWith("https:")) return DownloadParamType.httpUrl;
1125 if(s.startsWith("file:")) return DownloadParamType.fileUrl;
1126 String coorPattern = "\\s*[+-]?[0-9]+(\\.[0-9]+)?\\s*";
1127 if(s.matches(coorPattern+"(,"+coorPattern+"){3}")) return DownloadParamType.bounds;
1128 // everything else must be a file name
1129 return DownloadParamType.fileName;
1130 }
1131
1132 /**
1133 * Download area specified on the command line as OSM URL.
1134 * @param rawGps Flag to download raw GPS tracks
1135 * @param s The URL parameter
1136 */
1137 private static void downloadFromParamHttp(final boolean rawGps, String s) {
1138 final Bounds b = OsmUrlToBounds.parse(s);
1139 if (b == null) {
1140 JOptionPane.showMessageDialog(
1141 Main.parent,
1142 tr("Ignoring malformed URL: \"{0}\"", s),
1143 tr("Warning"),
1144 JOptionPane.WARNING_MESSAGE
1145 );
1146 } else {
1147 downloadFromParamBounds(rawGps, b);
1148 }
1149 }
1150
1151 /**
1152 * Download area specified on the command line as bounds string.
1153 * @param rawGps Flag to download raw GPS tracks
1154 * @param s The bounds parameter
1155 */
1156 private static void downloadFromParamBounds(final boolean rawGps, String s) {
1157 final StringTokenizer st = new StringTokenizer(s, ",");
1158 if (st.countTokens() == 4) {
1159 Bounds b = new Bounds(
1160 new LatLon(Double.parseDouble(st.nextToken()),Double.parseDouble(st.nextToken())),
1161 new LatLon(Double.parseDouble(st.nextToken()),Double.parseDouble(st.nextToken()))
1162 );
1163 downloadFromParamBounds(rawGps, b);
1164 }
1165 }
1166
1167 /**
1168 * Download area specified as Bounds value.
1169 * @param rawGps Flag to download raw GPS tracks
1170 * @param b The bounds value
1171 * @see #downloadFromParamBounds(boolean, String)
1172 * @see #downloadFromParamHttp
1173 */
1174 private static void downloadFromParamBounds(final boolean rawGps, Bounds b) {
1175 DownloadTask task = rawGps ? new DownloadGpsTask() : new DownloadOsmTask();
1176 // asynchronously launch the download task ...
1177 Future<?> future = task.download(true, b, null);
1178 // ... and the continuation when the download is finished (this will wait for the download to finish)
1179 Main.worker.execute(new PostDownloadHandler(task, future));
1180 }
1181
1182 /**
1183 * Identifies the current operating system family and initializes the platform hook accordingly.
1184 * @since 1849
1185 */
1186 public static void determinePlatformHook() {
1187 String os = System.getProperty("os.name");
1188 if (os == null) {
1189 warn("Your operating system has no name, so I'm guessing its some kind of *nix.");
1190 platform = new PlatformHookUnixoid();
1191 } else if (os.toLowerCase().startsWith("windows")) {
1192 platform = new PlatformHookWindows();
1193 } else if ("Linux".equals(os) || "Solaris".equals(os) ||
1194 "SunOS".equals(os) || "AIX".equals(os) ||
1195 "FreeBSD".equals(os) || "NetBSD".equals(os) || "OpenBSD".equals(os)) {
1196 platform = new PlatformHookUnixoid();
1197 } else if (os.toLowerCase().startsWith("mac os x")) {
1198 platform = new PlatformHookOsx();
1199 } else {
1200 warn("I don't know your operating system '"+os+"', so I'm guessing its some kind of *nix.");
1201 platform = new PlatformHookUnixoid();
1202 }
1203 }
1204
1205 private static class WindowPositionSizeListener extends WindowAdapter implements
1206 ComponentListener {
1207 @Override
1208 public void windowStateChanged(WindowEvent e) {
1209 Main.windowState = e.getNewState();
1210 }
1211
1212 @Override
1213 public void componentHidden(ComponentEvent e) {
1214 }
1215
1216 @Override
1217 public void componentMoved(ComponentEvent e) {
1218 handleComponentEvent(e);
1219 }
1220
1221 @Override
1222 public void componentResized(ComponentEvent e) {
1223 handleComponentEvent(e);
1224 }
1225
1226 @Override
1227 public void componentShown(ComponentEvent e) {
1228 }
1229
1230 private void handleComponentEvent(ComponentEvent e) {
1231 Component c = e.getComponent();
1232 if (c instanceof JFrame && c.isVisible()) {
1233 if(Main.windowState == JFrame.NORMAL) {
1234 Main.geometry = new WindowGeometry((JFrame) c);
1235 } else {
1236 Main.geometry.fixScreen((JFrame) c);
1237 }
1238 }
1239 }
1240 }
1241
1242 protected static void addListener() {
1243 parent.addComponentListener(new WindowPositionSizeListener());
1244 ((JFrame)parent).addWindowStateListener(new WindowPositionSizeListener());
1245 }
1246
1247 /**
1248 * Determines if JOSM currently runs with Java 8 or later.
1249 * @return {@code true} if the current JVM is at least Java 8, {@code false} otherwise
1250 * @since 7894
1251 */
1252 public static boolean isJava8orLater() {
1253 String version = System.getProperty("java.version");
1254 return version != null && !version.matches("^(1\\.)?[7].*");
1255 }
1256
1257 /**
1258 * Checks that JOSM is at least running with Java 7.
1259 * @since 7001
1260 */
1261 public static void checkJavaVersion() {
1262 String version = System.getProperty("java.version");
1263 if (version != null) {
1264 if (version.matches("^(1\\.)?[789].*"))
1265 return;
1266 if (version.matches("^(1\\.)?[56].*")) {
1267 JMultilineLabel ho = new JMultilineLabel("<html>"+
1268 tr("<h2>JOSM requires Java version {0}.</h2>"+
1269 "Detected Java version: {1}.<br>"+
1270 "You can <ul><li>update your Java (JRE) or</li>"+
1271 "<li>use an earlier (Java {2} compatible) version of JOSM.</li></ul>"+
1272 "More Info:", "7", version, "6")+"</html>");
1273 JTextArea link = new JTextArea(HelpUtil.getWikiBaseHelpUrl()+"/Help/SystemRequirements");
1274 link.setEditable(false);
1275 link.setBackground(panel.getBackground());
1276 JPanel panel = new JPanel(new GridBagLayout());
1277 GridBagConstraints gbc = new GridBagConstraints();
1278 gbc.gridwidth = GridBagConstraints.REMAINDER;
1279 gbc.anchor = GridBagConstraints.WEST;
1280 gbc.weightx = 1.0;
1281 panel.add(ho, gbc);
1282 panel.add(link, gbc);
1283 final String EXIT = tr("Exit JOSM");
1284 final String CONTINUE = tr("Continue, try anyway");
1285 int ret = JOptionPane.showOptionDialog(null, panel, tr("Error"), JOptionPane.YES_NO_OPTION,
1286 JOptionPane.ERROR_MESSAGE, null, new String[] {EXIT, CONTINUE}, EXIT);
1287 if (ret == 0) {
1288 System.exit(0);
1289 }
1290 return;
1291 }
1292 }
1293 error("Could not recognize Java Version: "+version);
1294 }
1295
1296 /* ----------------------------------------------------------------------------------------- */
1297 /* projection handling - Main is a registry for a single, global projection instance */
1298 /* */
1299 /* TODO: For historical reasons the registry is implemented by Main. An alternative approach */
1300 /* would be a singleton org.openstreetmap.josm.data.projection.ProjectionRegistry class. */
1301 /* ----------------------------------------------------------------------------------------- */
1302 /**
1303 * The projection method used.
1304 * use {@link #getProjection()} and {@link #setProjection(Projection)} for access.
1305 * Use {@link #setProjection(Projection)} in order to trigger a projection change event.
1306 */
1307 private static Projection proj;
1308
1309 /**
1310 * Replies the current projection.
1311 *
1312 * @return the currently active projection
1313 */
1314 public static Projection getProjection() {
1315 return proj;
1316 }
1317
1318 /**
1319 * Sets the current projection
1320 *
1321 * @param p the projection
1322 */
1323 public static void setProjection(Projection p) {
1324 CheckParameterUtil.ensureParameterNotNull(p);
1325 Projection oldValue = proj;
1326 Bounds b = isDisplayingMapView() ? map.mapView.getRealBounds() : null;
1327 proj = p;
1328 fireProjectionChanged(oldValue, proj, b);
1329 }
1330
1331 /*
1332 * Keep WeakReferences to the listeners. This relieves clients from the burden of
1333 * explicitly removing the listeners and allows us to transparently register every
1334 * created dataset as projection change listener.
1335 */
1336 private static final List<WeakReference<ProjectionChangeListener>> listeners = new ArrayList<>();
1337
1338 private static void fireProjectionChanged(Projection oldValue, Projection newValue, Bounds oldBounds) {
1339 if (newValue == null ^ oldValue == null
1340 || (newValue != null && oldValue != null && !Objects.equals(newValue.toCode(), oldValue.toCode()))) {
1341
1342 synchronized(Main.class) {
1343 Iterator<WeakReference<ProjectionChangeListener>> it = listeners.iterator();
1344 while (it.hasNext()){
1345 WeakReference<ProjectionChangeListener> wr = it.next();
1346 ProjectionChangeListener listener = wr.get();
1347 if (listener == null) {
1348 it.remove();
1349 continue;
1350 }
1351 listener.projectionChanged(oldValue, newValue);
1352 }
1353 }
1354 if (newValue != null && oldBounds != null) {
1355 Main.map.mapView.zoomTo(oldBounds);
1356 }
1357 /* TODO - remove layers with fixed projection */
1358 }
1359 }
1360
1361 /**
1362 * Register a projection change listener.
1363 *
1364 * @param listener the listener. Ignored if <code>null</code>.
1365 */
1366 public static void addProjectionChangeListener(ProjectionChangeListener listener) {
1367 if (listener == null) return;
1368 synchronized (Main.class) {
1369 for (WeakReference<ProjectionChangeListener> wr : listeners) {
1370 // already registered ? => abort
1371 if (wr.get() == listener) return;
1372 }
1373 listeners.add(new WeakReference<>(listener));
1374 }
1375 }
1376
1377 /**
1378 * Removes a projection change listener.
1379 *
1380 * @param listener the listener. Ignored if <code>null</code>.
1381 */
1382 public static void removeProjectionChangeListener(ProjectionChangeListener listener) {
1383 if (listener == null) return;
1384 synchronized(Main.class){
1385 Iterator<WeakReference<ProjectionChangeListener>> it = listeners.iterator();
1386 while (it.hasNext()){
1387 WeakReference<ProjectionChangeListener> wr = it.next();
1388 // remove the listener - and any other listener which got garbage
1389 // collected in the meantime
1390 if (wr.get() == null || wr.get() == listener) {
1391 it.remove();
1392 }
1393 }
1394 }
1395 }
1396
1397 /**
1398 * Listener for window switch events.
1399 *
1400 * These are events, when the user activates a window of another application
1401 * or comes back to JOSM. Window switches from one JOSM window to another
1402 * are not reported.
1403 */
1404 public static interface WindowSwitchListener {
1405 /**
1406 * Called when the user activates a window of another application.
1407 */
1408 void toOtherApplication();
1409 /**
1410 * Called when the user comes from a window of another application
1411 * back to JOSM.
1412 */
1413 void fromOtherApplication();
1414 }
1415
1416 private static final List<WeakReference<WindowSwitchListener>> windowSwitchListeners = new ArrayList<>();
1417
1418 /**
1419 * Register a window switch listener.
1420 *
1421 * @param listener the listener. Ignored if <code>null</code>.
1422 */
1423 public static void addWindowSwitchListener(WindowSwitchListener listener) {
1424 if (listener == null) return;
1425 synchronized (Main.class) {
1426 for (WeakReference<WindowSwitchListener> wr : windowSwitchListeners) {
1427 // already registered ? => abort
1428 if (wr.get() == listener) return;
1429 }
1430 boolean wasEmpty = windowSwitchListeners.isEmpty();
1431 windowSwitchListeners.add(new WeakReference<>(listener));
1432 if (wasEmpty) {
1433 // The following call will have no effect, when there is no window
1434 // at the time. Therefore, MasterWindowListener.setup() will also be
1435 // called, as soon as the main window is shown.
1436 MasterWindowListener.setup();
1437 }
1438 }
1439 }
1440
1441 /**
1442 * Removes a window switch listener.
1443 *
1444 * @param listener the listener. Ignored if <code>null</code>.
1445 */
1446 public static void removeWindowSwitchListener(WindowSwitchListener listener) {
1447 if (listener == null) return;
1448 synchronized (Main.class){
1449 Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator();
1450 while (it.hasNext()){
1451 WeakReference<WindowSwitchListener> wr = it.next();
1452 // remove the listener - and any other listener which got garbage
1453 // collected in the meantime
1454 if (wr.get() == null || wr.get() == listener) {
1455 it.remove();
1456 }
1457 }
1458 if (windowSwitchListeners.isEmpty()) {
1459 MasterWindowListener.teardown();
1460 }
1461 }
1462 }
1463
1464 /**
1465 * WindowListener, that is registered on all Windows of the application.
1466 *
1467 * Its purpose is to notify WindowSwitchListeners, that the user switches to
1468 * another application, e.g. a browser, or back to JOSM.
1469 *
1470 * When changing from JOSM to another application and back (e.g. two times
1471 * alt+tab), the active Window within JOSM may be different.
1472 * Therefore, we need to register listeners to <strong>all</strong> (visible)
1473 * Windows in JOSM, and it does not suffice to monitor the one that was
1474 * deactivated last.
1475 *
1476 * This class is only "active" on demand, i.e. when there is at least one
1477 * WindowSwitchListener registered.
1478 */
1479 protected static class MasterWindowListener extends WindowAdapter {
1480
1481 private static MasterWindowListener INSTANCE;
1482
1483 public static MasterWindowListener getInstance() {
1484 if (INSTANCE == null) {
1485 INSTANCE = new MasterWindowListener();
1486 }
1487 return INSTANCE;
1488 }
1489
1490 /**
1491 * Register listeners to all non-hidden windows.
1492 *
1493 * Windows that are created later, will be cared for in {@link #windowDeactivated(WindowEvent)}.
1494 */
1495 public static void setup() {
1496 if (!windowSwitchListeners.isEmpty()) {
1497 for (Window w : Window.getWindows()) {
1498 if (w.isShowing()) {
1499 if (!Arrays.asList(w.getWindowListeners()).contains(getInstance())) {
1500 w.addWindowListener(getInstance());
1501 }
1502 }
1503 }
1504 }
1505 }
1506
1507 /**
1508 * Unregister all listeners.
1509 */
1510 public static void teardown() {
1511 for (Window w : Window.getWindows()) {
1512 w.removeWindowListener(getInstance());
1513 }
1514 }
1515
1516 @Override
1517 public void windowActivated(WindowEvent e) {
1518 if (e.getOppositeWindow() == null) { // we come from a window of a different application
1519 // fire WindowSwitchListeners
1520 synchronized (Main.class) {
1521 Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator();
1522 while (it.hasNext()){
1523 WeakReference<WindowSwitchListener> wr = it.next();
1524 WindowSwitchListener listener = wr.get();
1525 if (listener == null) {
1526 it.remove();
1527 continue;
1528 }
1529 listener.fromOtherApplication();
1530 }
1531 }
1532 }
1533 }
1534
1535 @Override
1536 public void windowDeactivated(WindowEvent e) {
1537 // set up windows that have been created in the meantime
1538 for (Window w : Window.getWindows()) {
1539 if (!w.isShowing()) {
1540 w.removeWindowListener(getInstance());
1541 } else {
1542 if (!Arrays.asList(w.getWindowListeners()).contains(getInstance())) {
1543 w.addWindowListener(getInstance());
1544 }
1545 }
1546 }
1547 if (e.getOppositeWindow() == null) { // we go to a window of a different application
1548 // fire WindowSwitchListeners
1549 synchronized (Main.class) {
1550 Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator();
1551 while (it.hasNext()){
1552 WeakReference<WindowSwitchListener> wr = it.next();
1553 WindowSwitchListener listener = wr.get();
1554 if (listener == null) {
1555 it.remove();
1556 continue;
1557 }
1558 listener.toOtherApplication();
1559 }
1560 }
1561 }
1562 }
1563 }
1564
1565 /**
1566 * Registers a new {@code MapFrameListener} that will be notified of MapFrame changes
1567 * @param listener The MapFrameListener
1568 * @return {@code true} if the listeners collection changed as a result of the call
1569 * @since 5957
1570 */
1571 public static boolean addMapFrameListener(MapFrameListener listener) {
1572 return listener != null ? mapFrameListeners.add(listener) : false;
1573 }
1574
1575 /**
1576 * Unregisters the given {@code MapFrameListener} from MapFrame changes
1577 * @param listener The MapFrameListener
1578 * @return {@code true} if the listeners collection changed as a result of the call
1579 * @since 5957
1580 */
1581 public static boolean removeMapFrameListener(MapFrameListener listener) {
1582 return listener != null ? mapFrameListeners.remove(listener) : false;
1583 }
1584
1585 /**
1586 * Adds a new network error that occur to give a hint about broken Internet connection.
1587 * Do not use this method for errors known for sure thrown because of a bad proxy configuration.
1588 *
1589 * @param url The accessed URL that caused the error
1590 * @param t The network error
1591 * @return The previous error associated to the given resource, if any. Can be {@code null}
1592 * @since 6642
1593 */
1594 public static Throwable addNetworkError(URL url, Throwable t) {
1595 if (url != null && t != null) {
1596 Throwable old = addNetworkError(url.toExternalForm(), t);
1597 if (old != null) {
1598 Main.warn("Already here "+old);
1599 }
1600 return old;
1601 }
1602 return null;
1603 }
1604
1605 /**
1606 * Adds a new network error that occur to give a hint about broken Internet connection.
1607 * Do not use this method for errors known for sure thrown because of a bad proxy configuration.
1608 *
1609 * @param url The accessed URL that caused the error
1610 * @param t The network error
1611 * @return The previous error associated to the given resource, if any. Can be {@code null}
1612 * @since 6642
1613 */
1614 public static Throwable addNetworkError(String url, Throwable t) {
1615 if (url != null && t != null) {
1616 return NETWORK_ERRORS.put(url, t);
1617 }
1618 return null;
1619 }
1620
1621 /**
1622 * Returns the network errors that occured until now.
1623 * @return the network errors that occured until now, indexed by URL
1624 * @since 6639
1625 */
1626 public static Map<String, Throwable> getNetworkErrors() {
1627 return new HashMap<>(NETWORK_ERRORS);
1628 }
1629
1630 /**
1631 * Returns the JOSM website URL.
1632 * @return the josm website URL
1633 * @since 6897
1634 */
1635 public static String getJOSMWebsite() {
1636 if (Main.pref != null)
1637 return Main.pref.get("josm.url", JOSM_WEBSITE);
1638 return JOSM_WEBSITE;
1639 }
1640
1641 /**
1642 * Returns the JOSM XML URL.
1643 * @return the josm XML URL
1644 * @since 6897
1645 */
1646 public static String getXMLBase() {
1647 // Always return HTTP (issues reported with HTTPS)
1648 return "http://josm.openstreetmap.de";
1649 }
1650
1651 /**
1652 * Returns the OSM website URL.
1653 * @return the OSM website URL
1654 * @since 6897
1655 */
1656 public static String getOSMWebsite() {
1657 if (Main.pref != null)
1658 return Main.pref.get("osm.url", OSM_WEBSITE);
1659 return OSM_WEBSITE;
1660 }
1661
1662 /**
1663 * Replies the base URL for browsing information about a primitive.
1664 * @return the base URL, i.e. https://www.openstreetmap.org
1665 * @since 7678
1666 */
1667 public static String getBaseBrowseUrl() {
1668 if (Main.pref != null)
1669 return Main.pref.get("osm-browse.url", getOSMWebsite());
1670 return getOSMWebsite();
1671 }
1672
1673 /**
1674 * Replies the base URL for browsing information about a user.
1675 * @return the base URL, i.e. https://www.openstreetmap.org/user
1676 * @since 7678
1677 */
1678 public static String getBaseUserUrl() {
1679 if (Main.pref != null)
1680 return Main.pref.get("osm-user.url", getOSMWebsite() + "/user");
1681 return getOSMWebsite() + "/user";
1682 }
1683
1684 /**
1685 * Determines if we are currently running on OSX.
1686 * @return {@code true} if we are currently running on OSX
1687 * @since 6957
1688 */
1689 public static boolean isPlatformOsx() {
1690 return Main.platform instanceof PlatformHookOsx;
1691 }
1692
1693 /**
1694 * Determines if we are currently running on Windows.
1695 * @return {@code true} if we are currently running on Windows
1696 * @since 7335
1697 */
1698 public static boolean isPlatformWindows() {
1699 return Main.platform instanceof PlatformHookWindows;
1700 }
1701
1702 /**
1703 * Determines if the given online resource is currently offline.
1704 * @param r the online resource
1705 * @return {@code true} if {@code r} is offline and should not be accessed
1706 * @since 7434
1707 */
1708 public static boolean isOffline(OnlineResource r) {
1709 return OFFLINE_RESOURCES.contains(r) || OFFLINE_RESOURCES.contains(OnlineResource.ALL);
1710 }
1711
1712 /**
1713 * Sets the given online resource to offline state.
1714 * @param r the online resource
1715 * @return {@code true} if {@code r} was not already offline
1716 * @since 7434
1717 */
1718 public static boolean setOffline(OnlineResource r) {
1719 return OFFLINE_RESOURCES.add(r);
1720 }
1721
1722 /**
1723 * Replies the set of online resources currently offline.
1724 * @return the set of online resources currently offline
1725 * @since 7434
1726 */
1727 public static Set<OnlineResource> getOfflineResources() {
1728 return new HashSet<>(OFFLINE_RESOURCES);
1729 }
1730}
Note: See TracBrowser for help on using the repository browser.