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