001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.preferences.imagery; 003 004import static org.openstreetmap.josm.tools.I18n.marktr; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.awt.Color; 008import java.awt.Component; 009import java.awt.Dimension; 010import java.awt.FlowLayout; 011import java.awt.Font; 012import java.awt.GraphicsEnvironment; 013import java.awt.GridBagConstraints; 014import java.awt.GridBagLayout; 015import java.awt.event.ActionEvent; 016import java.awt.event.MouseAdapter; 017import java.awt.event.MouseEvent; 018import java.io.IOException; 019import java.net.MalformedURLException; 020import java.net.URL; 021import java.util.ArrayList; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Objects; 027import java.util.Set; 028import java.util.stream.Collectors; 029 030import javax.swing.AbstractAction; 031import javax.swing.BorderFactory; 032import javax.swing.Box; 033import javax.swing.JButton; 034import javax.swing.JLabel; 035import javax.swing.JOptionPane; 036import javax.swing.JPanel; 037import javax.swing.JScrollPane; 038import javax.swing.JSeparator; 039import javax.swing.JTabbedPane; 040import javax.swing.JTable; 041import javax.swing.JToolBar; 042import javax.swing.UIManager; 043import javax.swing.event.ListSelectionEvent; 044import javax.swing.event.ListSelectionListener; 045import javax.swing.table.DefaultTableCellRenderer; 046import javax.swing.table.DefaultTableModel; 047import javax.swing.table.TableColumnModel; 048 049import org.openstreetmap.gui.jmapviewer.Coordinate; 050import org.openstreetmap.gui.jmapviewer.JMapViewer; 051import org.openstreetmap.gui.jmapviewer.MapPolygonImpl; 052import org.openstreetmap.gui.jmapviewer.MapRectangleImpl; 053import org.openstreetmap.gui.jmapviewer.interfaces.MapPolygon; 054import org.openstreetmap.gui.jmapviewer.interfaces.MapRectangle; 055import org.openstreetmap.josm.data.coor.EastNorth; 056import org.openstreetmap.josm.data.imagery.ImageryInfo; 057import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds; 058import org.openstreetmap.josm.data.imagery.ImageryLayerInfo; 059import org.openstreetmap.josm.data.imagery.OffsetBookmark; 060import org.openstreetmap.josm.data.imagery.Shape; 061import org.openstreetmap.josm.data.preferences.NamedColorProperty; 062import org.openstreetmap.josm.data.projection.ProjectionRegistry; 063import org.openstreetmap.josm.gui.MainApplication; 064import org.openstreetmap.josm.gui.bbox.SlippyMapBBoxChooser; 065import org.openstreetmap.josm.gui.download.DownloadDialog; 066import org.openstreetmap.josm.gui.help.HelpUtil; 067import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting; 068import org.openstreetmap.josm.gui.preferences.PreferenceSetting; 069import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory; 070import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane; 071import org.openstreetmap.josm.gui.util.GuiHelper; 072import org.openstreetmap.josm.gui.widgets.HtmlPanel; 073import org.openstreetmap.josm.gui.widgets.JosmEditorPane; 074import org.openstreetmap.josm.spi.preferences.Config; 075import org.openstreetmap.josm.tools.GBC; 076import org.openstreetmap.josm.tools.ImageProvider; 077import org.openstreetmap.josm.tools.LanguageInfo; 078import org.openstreetmap.josm.tools.Logging; 079 080/** 081 * Imagery preferences, including imagery providers, settings and offsets. 082 * @since 3715 083 */ 084public final class ImageryPreference extends DefaultTabPreferenceSetting { 085 086 private ImageryProvidersPanel imageryProviders; 087 private ImageryLayerInfo layerInfo; 088 089 private final CommonSettingsPanel commonSettings = new CommonSettingsPanel(); 090 private final WMSSettingsPanel wmsSettings = new WMSSettingsPanel(); 091 private final TMSSettingsPanel tmsSettings = new TMSSettingsPanel(); 092 private final CacheSettingsPanel cacheSettingsPanel = new CacheSettingsPanel(); 093 094 /** 095 * Factory used to create a new {@code ImageryPreference}. 096 */ 097 public static class Factory implements PreferenceSettingFactory { 098 @Override 099 public PreferenceSetting createPreferenceSetting() { 100 return new ImageryPreference(); 101 } 102 } 103 104 private ImageryPreference() { 105 super(/* ICON(preferences/) */ "imagery", tr("Imagery preferences"), 106 tr("Modify list of imagery layers displayed in the Imagery menu"), 107 false, new JTabbedPane()); 108 } 109 110 private static void addSettingsSection(final JPanel p, String name, JPanel section) { 111 addSettingsSection(p, name, section, GBC.eol()); 112 } 113 114 private static void addSettingsSection(final JPanel p, String name, JPanel section, GBC gbc) { 115 final JLabel lbl = new JLabel(name); 116 lbl.setFont(lbl.getFont().deriveFont(Font.BOLD)); 117 lbl.setLabelFor(section); 118 p.add(lbl, GBC.std()); 119 p.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 0)); 120 p.add(section, gbc.insets(20, 5, 0, 10)); 121 } 122 123 private Component buildSettingsPanel() { 124 final JPanel p = new JPanel(new GridBagLayout()); 125 p.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 126 127 addSettingsSection(p, tr("Common Settings"), commonSettings); 128 addSettingsSection(p, tr("WMS Settings"), wmsSettings, 129 GBC.eol().fill(GBC.HORIZONTAL)); 130 addSettingsSection(p, tr("TMS Settings"), tmsSettings, 131 GBC.eol().fill(GBC.HORIZONTAL)); 132 133 p.add(new JPanel(), GBC.eol().fill(GBC.BOTH)); 134 return GuiHelper.setDefaultIncrement(new JScrollPane(p)); 135 } 136 137 @Override 138 public void addGui(final PreferenceTabbedPane gui) { 139 JPanel p = gui.createPreferenceTab(this); 140 JTabbedPane pane = getTabPane(); 141 layerInfo = new ImageryLayerInfo(ImageryLayerInfo.instance); 142 imageryProviders = new ImageryProvidersPanel(gui, layerInfo); 143 pane.addTab(tr("Imagery providers"), imageryProviders); 144 pane.addTab(tr("Settings"), buildSettingsPanel()); 145 pane.addTab(tr("Offset bookmarks"), new OffsetBookmarksPanel(gui)); 146 pane.addTab(tr("Cache"), cacheSettingsPanel); 147 loadSettings(); 148 p.add(pane, GBC.std().fill(GBC.BOTH)); 149 } 150 151 /** 152 * Returns the imagery providers panel. 153 * @return The imagery providers panel. 154 */ 155 public ImageryProvidersPanel getProvidersPanel() { 156 return imageryProviders; 157 } 158 159 private void loadSettings() { 160 commonSettings.loadSettings(); 161 wmsSettings.loadSettings(); 162 tmsSettings.loadSettings(); 163 cacheSettingsPanel.loadSettings(); 164 } 165 166 @Override 167 public boolean ok() { 168 layerInfo.save(); 169 ImageryLayerInfo.instance.clear(); 170 ImageryLayerInfo.instance.load(false); 171 MainApplication.getMenu().imageryMenu.refreshOffsetMenu(); 172 OffsetBookmark.saveBookmarks(); 173 174 if (!GraphicsEnvironment.isHeadless()) { 175 DownloadDialog.getInstance().refreshTileSources(); 176 } 177 178 boolean commonRestartRequired = commonSettings.saveSettings(); 179 boolean wmsRestartRequired = wmsSettings.saveSettings(); 180 boolean tmsRestartRequired = tmsSettings.saveSettings(); 181 boolean cacheRestartRequired = cacheSettingsPanel.saveSettings(); 182 183 return commonRestartRequired || wmsRestartRequired || tmsRestartRequired || cacheRestartRequired; 184 } 185 186 /** 187 * Updates a server URL in the preferences dialog. Used by plugins. 188 * 189 * @param server 190 * The server name 191 * @param url 192 * The server URL 193 */ 194 public void setServerUrl(String server, String url) { 195 for (int i = 0; i < imageryProviders.activeModel.getRowCount(); i++) { 196 if (server.equals(imageryProviders.activeModel.getValueAt(i, 0).toString())) { 197 imageryProviders.activeModel.setValueAt(url, i, 1); 198 return; 199 } 200 } 201 imageryProviders.activeModel.addRow(new String[] {server, url}); 202 } 203 204 /** 205 * Gets a server URL in the preferences dialog. Used by plugins. 206 * 207 * @param server The server name 208 * @return The server URL 209 */ 210 public String getServerUrl(String server) { 211 for (int i = 0; i < imageryProviders.activeModel.getRowCount(); i++) { 212 if (server.equals(imageryProviders.activeModel.getValueAt(i, 0).toString())) 213 return imageryProviders.activeModel.getValueAt(i, 1).toString(); 214 } 215 return null; 216 } 217 218 /** 219 * A panel displaying imagery providers. 220 */ 221 public static class ImageryProvidersPanel extends JPanel { 222 // Public JTables and JMapViewer 223 /** The table of active providers **/ 224 public final JTable activeTable; 225 /** The table of default providers **/ 226 public final JTable defaultTable; 227 /** The selection listener synchronizing map display with table of default providers **/ 228 private final transient DefListSelectionListener defaultTableListener; 229 /** The map displaying imagery bounds of selected default providers **/ 230 public final JMapViewer defaultMap; 231 232 // Public models 233 /** The model of active providers **/ 234 public final ImageryLayerTableModel activeModel; 235 /** The model of default providers **/ 236 public final ImageryDefaultLayerTableModel defaultModel; 237 238 // Public JToolbars 239 /** The toolbar on the right of active providers **/ 240 public final JToolBar activeToolbar; 241 /** The toolbar on the middle of the panel **/ 242 public final JToolBar middleToolbar; 243 /** The toolbar on the right of default providers **/ 244 public final JToolBar defaultToolbar; 245 246 // Private members 247 private final PreferenceTabbedPane gui; 248 private final transient ImageryLayerInfo layerInfo; 249 250 /** 251 * class to render the URL information of Imagery source 252 * @since 8065 253 */ 254 private static class ImageryURLTableCellRenderer extends DefaultTableCellRenderer { 255 256 private static final NamedColorProperty IMAGERY_BACKGROUND_COLOR = new NamedColorProperty( 257 marktr("Imagery Background: Default"), 258 new Color(200, 255, 200)); 259 260 private final transient List<ImageryInfo> layers; 261 262 ImageryURLTableCellRenderer(List<ImageryInfo> layers) { 263 this.layers = layers; 264 } 265 266 @Override 267 public Component getTableCellRendererComponent(JTable table, Object value, boolean 268 isSelected, boolean hasFocus, int row, int column) { 269 JLabel label = (JLabel) super.getTableCellRendererComponent( 270 table, value, isSelected, hasFocus, row, column); 271 GuiHelper.setBackgroundReadable(label, UIManager.getColor("Table.background")); 272 if (value != null) { // Fix #8159 273 String t = value.toString(); 274 for (ImageryInfo l : layers) { 275 if (l.getExtendedUrl().equals(t)) { 276 GuiHelper.setBackgroundReadable(label, IMAGERY_BACKGROUND_COLOR.get()); 277 break; 278 } 279 } 280 label.setToolTipText((String) value); 281 } 282 return label; 283 } 284 } 285 286 /** 287 * class to render the name information of Imagery source 288 * @since 8064 289 */ 290 private static class ImageryNameTableCellRenderer extends DefaultTableCellRenderer { 291 @Override 292 public Component getTableCellRendererComponent(JTable table, Object value, boolean 293 isSelected, boolean hasFocus, int row, int column) { 294 ImageryInfo info = (ImageryInfo) value; 295 JLabel label = (JLabel) super.getTableCellRendererComponent( 296 table, info == null ? null : info.getName(), isSelected, hasFocus, row, column); 297 GuiHelper.setBackgroundReadable(label, UIManager.getColor("Table.background")); 298 if (info != null) { 299 label.setToolTipText(info.getToolTipText()); 300 } 301 return label; 302 } 303 } 304 305 /** 306 * Constructs a new {@code ImageryProvidersPanel}. 307 * @param gui The parent preference tab pane 308 * @param layerInfoArg The list of imagery entries to display 309 */ 310 public ImageryProvidersPanel(final PreferenceTabbedPane gui, ImageryLayerInfo layerInfoArg) { 311 super(new GridBagLayout()); 312 this.gui = gui; 313 this.layerInfo = layerInfoArg; 314 this.activeModel = new ImageryLayerTableModel(); 315 316 activeTable = new JTable(activeModel) { 317 @Override 318 public String getToolTipText(MouseEvent e) { 319 java.awt.Point p = e.getPoint(); 320 try { 321 return activeModel.getValueAt(rowAtPoint(p), columnAtPoint(p)).toString(); 322 } catch (ArrayIndexOutOfBoundsException ex) { 323 Logging.debug(ex); 324 return null; 325 } 326 } 327 }; 328 activeTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); 329 330 defaultModel = new ImageryDefaultLayerTableModel(); 331 defaultTable = new JTable(defaultModel); 332 333 defaultModel.addTableModelListener(e -> activeTable.repaint()); 334 activeModel.addTableModelListener(e -> defaultTable.repaint()); 335 336 TableColumnModel mod = defaultTable.getColumnModel(); 337 mod.getColumn(2).setPreferredWidth(800); 338 mod.getColumn(2).setCellRenderer(new ImageryURLTableCellRenderer(layerInfo.getLayers())); 339 mod.getColumn(1).setPreferredWidth(400); 340 mod.getColumn(1).setCellRenderer(new ImageryNameTableCellRenderer()); 341 mod.getColumn(0).setPreferredWidth(50); 342 343 mod = activeTable.getColumnModel(); 344 mod.getColumn(1).setPreferredWidth(800); 345 mod.getColumn(1).setCellRenderer(new ImageryURLTableCellRenderer(layerInfo.getAllDefaultLayers())); 346 mod.getColumn(0).setPreferredWidth(200); 347 348 RemoveEntryAction remove = new RemoveEntryAction(); 349 activeTable.getSelectionModel().addListSelectionListener(remove); 350 351 add(new JLabel(tr("Available default entries:")), GBC.std().insets(5, 5, 0, 0)); 352 add(new JLabel(tr("Boundaries of selected imagery entries:")), GBC.eol().insets(5, 5, 0, 0)); 353 354 // Add default item list 355 JScrollPane scrolldef = new JScrollPane(defaultTable); 356 scrolldef.setPreferredSize(new Dimension(200, 200)); 357 add(scrolldef, GBC.std().insets(0, 5, 0, 0).fill(GridBagConstraints.BOTH).weight(1.0, 0.6).insets(5, 0, 0, 0)); 358 359 // Add default item map 360 defaultMap = new JMapViewer(); 361 defaultMap.setTileSource(SlippyMapBBoxChooser.DefaultOsmTileSourceProvider.get()); // for attribution 362 defaultMap.addMouseListener(new MouseAdapter() { 363 @Override 364 public void mouseClicked(MouseEvent e) { 365 if (e.getButton() == MouseEvent.BUTTON1) { 366 defaultMap.getAttribution().handleAttribution(e.getPoint(), true); 367 } 368 } 369 }); 370 defaultMap.setZoomControlsVisible(false); 371 defaultMap.setMinimumSize(new Dimension(100, 200)); 372 add(defaultMap, GBC.std().insets(5, 5, 0, 0).fill(GridBagConstraints.BOTH).weight(0.33, 0.6).insets(5, 0, 0, 0)); 373 374 defaultTableListener = new DefListSelectionListener(); 375 defaultTable.getSelectionModel().addListSelectionListener(defaultTableListener); 376 377 defaultToolbar = new JToolBar(JToolBar.VERTICAL); 378 defaultToolbar.setFloatable(false); 379 defaultToolbar.setBorderPainted(false); 380 defaultToolbar.setOpaque(false); 381 defaultToolbar.add(new ReloadAction()); 382 add(defaultToolbar, GBC.eol().anchor(GBC.SOUTH).insets(0, 0, 5, 0)); 383 384 HtmlPanel help = new HtmlPanel(tr("New default entries can be added in the <a href=\"{0}\">Wiki</a>.", 385 Config.getUrls().getJOSMWebsite()+"/wiki/Maps")); 386 help.enableClickableHyperlinks(); 387 add(help, GBC.eol().insets(10, 0, 0, 0).fill(GBC.HORIZONTAL)); 388 389 ActivateAction activate = new ActivateAction(); 390 defaultTable.getSelectionModel().addListSelectionListener(activate); 391 JButton btnActivate = new JButton(activate); 392 393 middleToolbar = new JToolBar(JToolBar.HORIZONTAL); 394 middleToolbar.setFloatable(false); 395 middleToolbar.setBorderPainted(false); 396 middleToolbar.setOpaque(false); 397 middleToolbar.add(btnActivate); 398 add(middleToolbar, GBC.eol().anchor(GBC.CENTER).insets(5, 5, 5, 0)); 399 400 add(Box.createHorizontalGlue(), GBC.eol().fill(GridBagConstraints.HORIZONTAL)); 401 402 add(new JLabel(tr("Selected entries:")), GBC.eol().insets(5, 0, 0, 0)); 403 JScrollPane scroll = new JScrollPane(activeTable); 404 add(scroll, GBC.std().fill(GridBagConstraints.BOTH).span(GridBagConstraints.RELATIVE).weight(1.0, 0.4).insets(5, 0, 0, 5)); 405 scroll.setPreferredSize(new Dimension(200, 200)); 406 407 activeToolbar = new JToolBar(JToolBar.VERTICAL); 408 activeToolbar.setFloatable(false); 409 activeToolbar.setBorderPainted(false); 410 activeToolbar.setOpaque(false); 411 activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMS)); 412 activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.TMS)); 413 activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMTS)); 414 //activeToolbar.add(edit); TODO 415 activeToolbar.add(remove); 416 add(activeToolbar, GBC.eol().anchor(GBC.NORTH).insets(0, 0, 5, 5)); 417 } 418 419 // Listener of default providers list selection 420 private final class DefListSelectionListener implements ListSelectionListener { 421 // The current drawn rectangles and polygons 422 private final Map<Integer, MapRectangle> mapRectangles; 423 private final Map<Integer, List<MapPolygon>> mapPolygons; 424 425 private DefListSelectionListener() { 426 this.mapRectangles = new HashMap<>(); 427 this.mapPolygons = new HashMap<>(); 428 } 429 430 private void clearMap() { 431 defaultMap.removeAllMapRectangles(); 432 defaultMap.removeAllMapPolygons(); 433 mapRectangles.clear(); 434 mapPolygons.clear(); 435 } 436 437 @Override 438 public void valueChanged(ListSelectionEvent e) { 439 // First index can be set to -1 when the list is refreshed, so discard all map rectangles and polygons 440 if (e.getFirstIndex() == -1) { 441 clearMap(); 442 } else if (!e.getValueIsAdjusting()) { 443 // Only process complete (final) selection events 444 for (int i = e.getFirstIndex(); i <= e.getLastIndex(); i++) { 445 updateBoundsAndShapes(i); 446 } 447 // If needed, adjust map to show all map rectangles and polygons 448 if (!mapRectangles.isEmpty() || !mapPolygons.isEmpty()) { 449 defaultMap.setDisplayToFitMapElements(false, true, true); 450 defaultMap.zoomOut(); 451 } 452 } 453 } 454 455 private void updateBoundsAndShapes(int i) { 456 ImageryBounds bounds = defaultModel.getRow(i).getBounds(); 457 if (bounds != null) { 458 List<Shape> shapes = bounds.getShapes(); 459 if (shapes != null && !shapes.isEmpty()) { 460 if (defaultTable.getSelectionModel().isSelectedIndex(i)) { 461 if (!mapPolygons.containsKey(i)) { 462 List<MapPolygon> list = new ArrayList<>(); 463 mapPolygons.put(i, list); 464 // Add new map polygons 465 for (Shape shape : shapes) { 466 MapPolygon polygon = new MapPolygonImpl(shape.getPoints()); 467 list.add(polygon); 468 defaultMap.addMapPolygon(polygon); 469 } 470 } 471 } else if (mapPolygons.containsKey(i)) { 472 // Remove previously drawn map polygons 473 for (MapPolygon polygon : mapPolygons.get(i)) { 474 defaultMap.removeMapPolygon(polygon); 475 } 476 mapPolygons.remove(i); 477 } 478 // Only display bounds when no polygons (shapes) are defined for this provider 479 } else { 480 if (defaultTable.getSelectionModel().isSelectedIndex(i)) { 481 if (!mapRectangles.containsKey(i)) { 482 // Add new map rectangle 483 Coordinate topLeft = new Coordinate(bounds.getMaxLat(), bounds.getMinLon()); 484 Coordinate bottomRight = new Coordinate(bounds.getMinLat(), bounds.getMaxLon()); 485 MapRectangle rectangle = new MapRectangleImpl(topLeft, bottomRight); 486 mapRectangles.put(i, rectangle); 487 defaultMap.addMapRectangle(rectangle); 488 } 489 } else if (mapRectangles.containsKey(i)) { 490 // Remove previously drawn map rectangle 491 defaultMap.removeMapRectangle(mapRectangles.get(i)); 492 mapRectangles.remove(i); 493 } 494 } 495 } 496 } 497 } 498 499 private class NewEntryAction extends AbstractAction { 500 501 private final ImageryInfo.ImageryType type; 502 503 NewEntryAction(ImageryInfo.ImageryType type) { 504 putValue(NAME, type.toString()); 505 putValue(SHORT_DESCRIPTION, tr("Add a new {0} entry by entering the URL", type.toString())); 506 String icon = /* ICON(dialogs/) */ "add"; 507 switch (type) { 508 case WMS: 509 icon = /* ICON(dialogs/) */ "add_wms"; 510 break; 511 case TMS: 512 icon = /* ICON(dialogs/) */ "add_tms"; 513 break; 514 case WMTS: 515 icon = /* ICON(dialogs/) */ "add_wmts"; 516 break; 517 default: 518 break; 519 } 520 new ImageProvider("dialogs", icon).getResource().attachImageIcon(this, true); 521 this.type = type; 522 } 523 524 @Override 525 public void actionPerformed(ActionEvent evt) { 526 final AddImageryPanel p; 527 switch (type) { 528 case WMS: 529 p = new AddWMSLayerPanel(); 530 break; 531 case TMS: 532 p = new AddTMSLayerPanel(); 533 break; 534 case WMTS: 535 p = new AddWMTSLayerPanel(); 536 break; 537 default: 538 throw new IllegalStateException("Type " + type + " not supported"); 539 } 540 541 final AddImageryDialog addDialog = new AddImageryDialog(gui, p); 542 addDialog.showDialog(); 543 544 if (addDialog.getValue() == 1) { 545 try { 546 activeModel.addRow(p.getImageryInfo()); 547 } catch (IllegalArgumentException ex) { 548 if (ex.getMessage() == null || ex.getMessage().isEmpty()) 549 throw ex; 550 else { 551 JOptionPane.showMessageDialog(MainApplication.getMainFrame(), 552 ex.getMessage(), tr("Error"), 553 JOptionPane.ERROR_MESSAGE); 554 } 555 } 556 } 557 } 558 } 559 560 private class RemoveEntryAction extends AbstractAction implements ListSelectionListener { 561 562 /** 563 * Constructs a new {@code RemoveEntryAction}. 564 */ 565 RemoveEntryAction() { 566 putValue(NAME, tr("Remove")); 567 putValue(SHORT_DESCRIPTION, tr("Remove entry")); 568 new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this, true); 569 updateEnabledState(); 570 } 571 572 protected final void updateEnabledState() { 573 setEnabled(activeTable.getSelectedRowCount() > 0); 574 } 575 576 @Override 577 public void valueChanged(ListSelectionEvent e) { 578 updateEnabledState(); 579 } 580 581 @Override 582 public void actionPerformed(ActionEvent e) { 583 Integer i; 584 while ((i = activeTable.getSelectedRow()) != -1) { 585 activeModel.removeRow(i); 586 } 587 } 588 } 589 590 private class ActivateAction extends AbstractAction implements ListSelectionListener { 591 592 /** 593 * Constructs a new {@code ActivateAction}. 594 */ 595 ActivateAction() { 596 putValue(NAME, tr("Activate")); 597 putValue(SHORT_DESCRIPTION, tr("Copy selected default entries from the list above into the list below.")); 598 new ImageProvider("preferences", "activate-down").getResource().attachImageIcon(this, true); 599 } 600 601 protected void updateEnabledState() { 602 setEnabled(defaultTable.getSelectedRowCount() > 0); 603 } 604 605 @Override 606 public void valueChanged(ListSelectionEvent e) { 607 updateEnabledState(); 608 } 609 610 @Override 611 public void actionPerformed(ActionEvent e) { 612 int[] lines = defaultTable.getSelectedRows(); 613 if (lines.length == 0) { 614 JOptionPane.showMessageDialog( 615 gui, 616 tr("Please select at least one row to copy."), 617 tr("Information"), 618 JOptionPane.INFORMATION_MESSAGE); 619 return; 620 } 621 622 Set<String> acceptedEulas = new HashSet<>(); 623 624 outer: 625 for (int line : lines) { 626 ImageryInfo info = defaultModel.getRow(line); 627 628 // Check if an entry with exactly the same values already exists 629 for (int j = 0; j < activeModel.getRowCount(); j++) { 630 if (info.equalsBaseValues(activeModel.getRow(j))) { 631 // Select the already existing row so the user has 632 // some feedback in case an entry exists 633 activeTable.getSelectionModel().setSelectionInterval(j, j); 634 activeTable.scrollRectToVisible(activeTable.getCellRect(j, 0, true)); 635 continue outer; 636 } 637 } 638 639 String eulaURL = info.getEulaAcceptanceRequired(); 640 // If set and not already accepted, ask for EULA acceptance 641 if (eulaURL != null && !acceptedEulas.contains(eulaURL)) { 642 if (confirmEulaAcceptance(gui, eulaURL)) { 643 acceptedEulas.add(eulaURL); 644 } else { 645 continue outer; 646 } 647 } 648 649 activeModel.addRow(new ImageryInfo(info)); 650 int lastLine = activeModel.getRowCount() - 1; 651 activeTable.getSelectionModel().setSelectionInterval(lastLine, lastLine); 652 activeTable.scrollRectToVisible(activeTable.getCellRect(lastLine, 0, true)); 653 } 654 } 655 } 656 657 private class ReloadAction extends AbstractAction { 658 659 /** 660 * Constructs a new {@code ReloadAction}. 661 */ 662 ReloadAction() { 663 putValue(SHORT_DESCRIPTION, tr("Update default entries")); 664 new ImageProvider("dialogs", "refresh").getResource().attachImageIcon(this, true); 665 } 666 667 @Override 668 public void actionPerformed(ActionEvent evt) { 669 layerInfo.loadDefaults(true, MainApplication.worker, false); 670 defaultModel.fireTableDataChanged(); 671 defaultTable.getSelectionModel().clearSelection(); 672 defaultTableListener.clearMap(); 673 /* loading new file may change active layers */ 674 activeModel.fireTableDataChanged(); 675 } 676 } 677 678 /** 679 * The table model for imagery layer list 680 */ 681 public class ImageryLayerTableModel extends DefaultTableModel { 682 /** 683 * Constructs a new {@code ImageryLayerTableModel}. 684 */ 685 public ImageryLayerTableModel() { 686 setColumnIdentifiers(new String[] {tr("Menu Name"), tr("Imagery URL")}); 687 } 688 689 /** 690 * Returns the imagery info at the given row number. 691 * @param row The row number 692 * @return The imagery info at the given row number 693 */ 694 public ImageryInfo getRow(int row) { 695 return layerInfo.getLayers().get(row); 696 } 697 698 /** 699 * Adds a new imagery info as the last row. 700 * @param i The imagery info to add 701 */ 702 public void addRow(ImageryInfo i) { 703 layerInfo.add(i); 704 int p = getRowCount() - 1; 705 fireTableRowsInserted(p, p); 706 } 707 708 @Override 709 public void removeRow(int i) { 710 layerInfo.remove(getRow(i)); 711 fireTableRowsDeleted(i, i); 712 } 713 714 @Override 715 public int getRowCount() { 716 return layerInfo.getLayers().size(); 717 } 718 719 @Override 720 public Object getValueAt(int row, int column) { 721 ImageryInfo info = layerInfo.getLayers().get(row); 722 switch (column) { 723 case 0: 724 return info.getName(); 725 case 1: 726 return info.getExtendedUrl(); 727 default: 728 throw new ArrayIndexOutOfBoundsException(Integer.toString(column)); 729 } 730 } 731 732 @Override 733 public void setValueAt(Object o, int row, int column) { 734 if (layerInfo.getLayers().size() <= row) return; 735 ImageryInfo info = layerInfo.getLayers().get(row); 736 switch (column) { 737 case 0: 738 info.setName((String) o); 739 info.clearId(); 740 break; 741 case 1: 742 info.setExtendedUrl((String) o); 743 info.clearId(); 744 break; 745 default: 746 throw new ArrayIndexOutOfBoundsException(Integer.toString(column)); 747 } 748 } 749 } 750 751 /** 752 * The table model for the default imagery layer list 753 */ 754 public class ImageryDefaultLayerTableModel extends DefaultTableModel { 755 /** 756 * Constructs a new {@code ImageryDefaultLayerTableModel}. 757 */ 758 public ImageryDefaultLayerTableModel() { 759 setColumnIdentifiers(new String[]{"", tr("Menu Name (Default)"), tr("Imagery URL (Default)")}); 760 } 761 762 /** 763 * Returns the imagery info at the given row number. 764 * @param row The row number 765 * @return The imagery info at the given row number 766 */ 767 public ImageryInfo getRow(int row) { 768 return layerInfo.getAllDefaultLayers().get(row); 769 } 770 771 @Override 772 public int getRowCount() { 773 return layerInfo.getAllDefaultLayers().size(); 774 } 775 776 @Override 777 public Object getValueAt(int row, int column) { 778 ImageryInfo info = layerInfo.getAllDefaultLayers().get(row); 779 switch (column) { 780 case 0: 781 return info.getCountryCode(); 782 case 1: 783 return info; 784 case 2: 785 return info.getExtendedUrl(); 786 } 787 return null; 788 } 789 790 @Override 791 public boolean isCellEditable(int row, int column) { 792 return false; 793 } 794 } 795 796 private static boolean confirmEulaAcceptance(PreferenceTabbedPane gui, String eulaUrl) { 797 URL url; 798 try { 799 url = new URL(eulaUrl.replaceAll("\\{lang\\}", LanguageInfo.getWikiLanguagePrefix())); 800 JosmEditorPane htmlPane; 801 try { 802 htmlPane = new JosmEditorPane(url); 803 } catch (IOException e1) { 804 Logging.trace(e1); 805 // give a second chance with a default Locale 'en' 806 try { 807 url = new URL(eulaUrl.replaceAll("\\{lang\\}", "")); 808 htmlPane = new JosmEditorPane(url); 809 } catch (IOException e2) { 810 Logging.debug(e2); 811 JOptionPane.showMessageDialog(gui, tr("EULA license URL not available: {0}", eulaUrl)); 812 return false; 813 } 814 } 815 Box box = Box.createVerticalBox(); 816 htmlPane.setEditable(false); 817 JScrollPane scrollPane = new JScrollPane(htmlPane); 818 scrollPane.setPreferredSize(new Dimension(400, 400)); 819 box.add(scrollPane); 820 int option = JOptionPane.showConfirmDialog(MainApplication.getMainFrame(), box, tr("Please abort if you are not sure"), 821 JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); 822 if (option == JOptionPane.YES_OPTION) 823 return true; 824 } catch (MalformedURLException e2) { 825 JOptionPane.showMessageDialog(gui, tr("Malformed URL for the EULA licence: {0}", eulaUrl)); 826 } 827 return false; 828 } 829 } 830 831 static class OffsetBookmarksPanel extends JPanel { 832 private final OffsetsBookmarksModel model = new OffsetsBookmarksModel(); 833 834 /** 835 * Constructs a new {@code OffsetBookmarksPanel}. 836 * @param gui the preferences tab pane 837 */ 838 OffsetBookmarksPanel(final PreferenceTabbedPane gui) { 839 super(new GridBagLayout()); 840 final JTable list = new JTable(model) { 841 @Override 842 public String getToolTipText(MouseEvent e) { 843 java.awt.Point p = e.getPoint(); 844 return model.getValueAt(rowAtPoint(p), columnAtPoint(p)).toString(); 845 } 846 }; 847 JScrollPane scroll = new JScrollPane(list); 848 add(scroll, GBC.eol().fill(GridBagConstraints.BOTH)); 849 scroll.setPreferredSize(new Dimension(200, 200)); 850 851 TableColumnModel mod = list.getColumnModel(); 852 mod.getColumn(0).setPreferredWidth(150); 853 mod.getColumn(1).setPreferredWidth(200); 854 mod.getColumn(2).setPreferredWidth(300); 855 mod.getColumn(3).setPreferredWidth(150); 856 mod.getColumn(4).setPreferredWidth(150); 857 858 JPanel buttonPanel = new JPanel(new FlowLayout()); 859 860 JButton add = new JButton(tr("Add")); 861 buttonPanel.add(add, GBC.std().insets(0, 5, 0, 0)); 862 add.addActionListener(e -> model.addRow(new OffsetBookmark(ProjectionRegistry.getProjection().toCode(), "", "", "", 0, 0))); 863 864 JButton delete = new JButton(tr("Delete")); 865 buttonPanel.add(delete, GBC.std().insets(0, 5, 0, 0)); 866 delete.addActionListener(e -> { 867 if (list.getSelectedRow() == -1) { 868 JOptionPane.showMessageDialog(gui, tr("Please select the row to delete.")); 869 } else { 870 Integer i; 871 while ((i = list.getSelectedRow()) != -1) { 872 model.removeRow(i); 873 } 874 } 875 }); 876 877 add(buttonPanel, GBC.eol()); 878 } 879 880 /** 881 * The table model for imagery offsets list 882 */ 883 private static class OffsetsBookmarksModel extends DefaultTableModel { 884 885 /** 886 * Constructs a new {@code OffsetsBookmarksModel}. 887 */ 888 OffsetsBookmarksModel() { 889 setColumnIdentifiers(new String[] {tr("Projection"), tr("Layer"), tr("Name"), tr("Easting"), tr("Northing")}); 890 } 891 892 private static OffsetBookmark getRow(int row) { 893 return OffsetBookmark.getBookmarkByIndex(row); 894 } 895 896 private void addRow(OffsetBookmark i) { 897 OffsetBookmark.addBookmark(i); 898 int p = getRowCount() - 1; 899 fireTableRowsInserted(p, p); 900 } 901 902 @Override 903 public void removeRow(int i) { 904 OffsetBookmark.removeBookmark(getRow(i)); 905 fireTableRowsDeleted(i, i); 906 } 907 908 @Override 909 public int getRowCount() { 910 return OffsetBookmark.getBookmarksSize(); 911 } 912 913 @Override 914 public Object getValueAt(int row, int column) { 915 OffsetBookmark info = OffsetBookmark.getBookmarkByIndex(row); 916 switch (column) { 917 case 0: 918 if (info.getProjectionCode() == null) return ""; 919 return info.getProjectionCode(); 920 case 1: 921 return info.getImageryName(); 922 case 2: 923 return info.getName(); 924 case 3: 925 return info.getDisplacement().east(); 926 case 4: 927 return info.getDisplacement().north(); 928 default: 929 throw new ArrayIndexOutOfBoundsException(column); 930 } 931 } 932 933 @Override 934 public void setValueAt(Object o, int row, int column) { 935 OffsetBookmark info = OffsetBookmark.getBookmarkByIndex(row); 936 switch (column) { 937 case 1: 938 String name = o.toString(); 939 info.setImageryName(name); 940 List<ImageryInfo> layers = ImageryLayerInfo.instance.getLayers().stream() 941 .filter(l -> Objects.equals(name, l.getName())).collect(Collectors.toList()); 942 if (layers.size() == 1) { 943 info.setImageryId(layers.get(0).getId()); 944 } else { 945 Logging.warn("Not a single layer for the name '" + info.getImageryName() + "': " + layers); 946 } 947 break; 948 case 2: 949 info.setName(o.toString()); 950 break; 951 case 3: 952 double dx = Double.parseDouble((String) o); 953 info.setDisplacement(new EastNorth(dx, info.getDisplacement().north())); 954 break; 955 case 4: 956 double dy = Double.parseDouble((String) o); 957 info.setDisplacement(new EastNorth(info.getDisplacement().east(), dy)); 958 break; 959 default: 960 throw new ArrayIndexOutOfBoundsException(column); 961 } 962 } 963 964 @Override 965 public boolean isCellEditable(int row, int column) { 966 return column >= 1; 967 } 968 } 969 } 970 971 /** 972 * Initializes imagery preferences. 973 */ 974 public static void initialize() { 975 ImageryLayerInfo.instance.load(false); 976 OffsetBookmark.loadBookmarks(); 977 MainApplication.getMenu().imageryMenu.refreshImageryMenu(); 978 MainApplication.getMenu().imageryMenu.refreshOffsetMenu(); 979 } 980 981 @Override 982 public String getHelpContext() { 983 return HelpUtil.ht("/Preferences/Imagery"); 984 } 985}