001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Color; 007import java.awt.Component; 008import java.awt.Dimension; 009import java.awt.Font; 010import java.awt.GraphicsEnvironment; 011import java.awt.event.ActionEvent; 012import java.awt.event.InputEvent; 013import java.awt.event.KeyEvent; 014import java.awt.event.MouseEvent; 015import java.beans.PropertyChangeEvent; 016import java.beans.PropertyChangeListener; 017import java.util.ArrayList; 018import java.util.Arrays; 019import java.util.Collections; 020import java.util.List; 021import java.util.Objects; 022import java.util.concurrent.CopyOnWriteArrayList; 023 024import javax.swing.AbstractAction; 025import javax.swing.DefaultCellEditor; 026import javax.swing.DefaultListSelectionModel; 027import javax.swing.DropMode; 028import javax.swing.Icon; 029import javax.swing.ImageIcon; 030import javax.swing.JCheckBox; 031import javax.swing.JComponent; 032import javax.swing.JLabel; 033import javax.swing.JTable; 034import javax.swing.KeyStroke; 035import javax.swing.ListSelectionModel; 036import javax.swing.UIManager; 037import javax.swing.event.ListDataEvent; 038import javax.swing.event.ListSelectionEvent; 039import javax.swing.table.AbstractTableModel; 040import javax.swing.table.DefaultTableCellRenderer; 041import javax.swing.table.TableCellRenderer; 042import javax.swing.table.TableModel; 043 044import org.openstreetmap.josm.Main; 045import org.openstreetmap.josm.actions.MergeLayerAction; 046import org.openstreetmap.josm.data.coor.EastNorth; 047import org.openstreetmap.josm.data.imagery.OffsetBookmark; 048import org.openstreetmap.josm.data.preferences.AbstractProperty; 049import org.openstreetmap.josm.gui.MainApplication; 050import org.openstreetmap.josm.gui.MapFrame; 051import org.openstreetmap.josm.gui.MapView; 052import org.openstreetmap.josm.gui.SideButton; 053import org.openstreetmap.josm.gui.dialogs.layer.ActivateLayerAction; 054import org.openstreetmap.josm.gui.dialogs.layer.DeleteLayerAction; 055import org.openstreetmap.josm.gui.dialogs.layer.DuplicateAction; 056import org.openstreetmap.josm.gui.dialogs.layer.LayerListTransferHandler; 057import org.openstreetmap.josm.gui.dialogs.layer.LayerVisibilityAction; 058import org.openstreetmap.josm.gui.dialogs.layer.MergeAction; 059import org.openstreetmap.josm.gui.dialogs.layer.MoveDownAction; 060import org.openstreetmap.josm.gui.dialogs.layer.MoveUpAction; 061import org.openstreetmap.josm.gui.dialogs.layer.ShowHideLayerAction; 062import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 063import org.openstreetmap.josm.gui.layer.JumpToMarkerActions; 064import org.openstreetmap.josm.gui.layer.Layer; 065import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 066import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 067import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 068import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 069import org.openstreetmap.josm.gui.layer.MainLayerManager; 070import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 071import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 072import org.openstreetmap.josm.gui.layer.NativeScaleLayer; 073import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeEvent; 074import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeListener; 075import org.openstreetmap.josm.gui.util.MultikeyActionsHandler; 076import org.openstreetmap.josm.gui.util.MultikeyShortcutAction.MultikeyInfo; 077import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField; 078import org.openstreetmap.josm.gui.widgets.JosmTextField; 079import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 080import org.openstreetmap.josm.gui.widgets.ScrollableTable; 081import org.openstreetmap.josm.spi.preferences.Config; 082import org.openstreetmap.josm.tools.ImageProvider; 083import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; 084import org.openstreetmap.josm.tools.InputMapUtils; 085import org.openstreetmap.josm.tools.Shortcut; 086 087/** 088 * This is a toggle dialog which displays the list of layers. Actions allow to 089 * change the ordering of the layers, to hide/show layers, to activate layers, 090 * and to delete layers. 091 * <p> 092 * Support for multiple {@link LayerListDialog} is currently not complete but intended for the future. 093 * @since 17 094 */ 095public class LayerListDialog extends ToggleDialog implements DisplaySettingsChangeListener { 096 /** the unique instance of the dialog */ 097 private static volatile LayerListDialog instance; 098 099 /** 100 * Creates the instance of the dialog. It's connected to the layer manager 101 * 102 * @param layerManager the layer manager 103 * @since 11885 (signature) 104 */ 105 public static void createInstance(MainLayerManager layerManager) { 106 if (instance != null) 107 throw new IllegalStateException("Dialog was already created"); 108 instance = new LayerListDialog(layerManager); 109 } 110 111 /** 112 * Replies the instance of the dialog 113 * 114 * @return the instance of the dialog 115 * @throws IllegalStateException if the dialog is not created yet 116 * @see #createInstance(MainLayerManager) 117 */ 118 public static LayerListDialog getInstance() { 119 if (instance == null) 120 throw new IllegalStateException("Dialog not created yet. Invoke createInstance() first"); 121 return instance; 122 } 123 124 /** the model for the layer list */ 125 private final LayerListModel model; 126 127 /** the list of layers (technically its a JTable, but appears like a list) */ 128 private final LayerList layerList; 129 130 private final ActivateLayerAction activateLayerAction; 131 private final ShowHideLayerAction showHideLayerAction; 132 133 //TODO This duplicates ShowHide actions functionality 134 /** stores which layer index to toggle and executes the ShowHide action if the layer is present */ 135 private final class ToggleLayerIndexVisibility extends AbstractAction { 136 private final int layerIndex; 137 138 ToggleLayerIndexVisibility(int layerIndex) { 139 this.layerIndex = layerIndex; 140 } 141 142 @Override 143 public void actionPerformed(ActionEvent e) { 144 final Layer l = model.getLayer(model.getRowCount() - layerIndex - 1); 145 if (l != null) { 146 l.toggleVisible(); 147 } 148 } 149 } 150 151 private final transient Shortcut[] visibilityToggleShortcuts = new Shortcut[10]; 152 private final ToggleLayerIndexVisibility[] visibilityToggleActions = new ToggleLayerIndexVisibility[10]; 153 154 /** 155 * The {@link MainLayerManager} this list is for. 156 */ 157 private final transient MainLayerManager layerManager; 158 159 /** 160 * registers (shortcut to toggle right hand side toggle dialogs)+(number keys) shortcuts 161 * to toggle the visibility of the first ten layers. 162 */ 163 private void createVisibilityToggleShortcuts() { 164 for (int i = 0; i < 10; i++) { 165 final int i1 = i + 1; 166 /* POSSIBLE SHORTCUTS: 1,2,3,4,5,6,7,8,9,0=10 */ 167 visibilityToggleShortcuts[i] = Shortcut.registerShortcut("subwindow:layers:toggleLayer" + i1, 168 tr("Toggle visibility of layer: {0}", i1), KeyEvent.VK_0 + (i1 % 10), Shortcut.ALT); 169 visibilityToggleActions[i] = new ToggleLayerIndexVisibility(i); 170 MainApplication.registerActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]); 171 } 172 } 173 174 /** 175 * Creates a layer list and attach it to the given layer manager. 176 * @param layerManager The layer manager this list is for 177 * @since 10467 178 */ 179 public LayerListDialog(MainLayerManager layerManager) { 180 super(tr("Layers"), "layerlist", tr("Open a list of all loaded layers."), 181 Shortcut.registerShortcut("subwindow:layers", tr("Toggle: {0}", tr("Layers")), KeyEvent.VK_L, 182 Shortcut.ALT_SHIFT), 100, true); 183 this.layerManager = layerManager; 184 185 // create the models 186 // 187 DefaultListSelectionModel selectionModel = new DefaultListSelectionModel(); 188 selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 189 model = new LayerListModel(layerManager, selectionModel); 190 191 // create the list control 192 // 193 layerList = new LayerList(model); 194 layerList.setSelectionModel(selectionModel); 195 layerList.addMouseListener(new PopupMenuHandler()); 196 layerList.setBackground(UIManager.getColor("Button.background")); 197 layerList.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); 198 layerList.putClientProperty("JTable.autoStartsEdit", Boolean.FALSE); 199 layerList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 200 layerList.setTableHeader(null); 201 layerList.setShowGrid(false); 202 layerList.setIntercellSpacing(new Dimension(0, 0)); 203 layerList.getColumnModel().getColumn(0).setCellRenderer(new ActiveLayerCellRenderer()); 204 layerList.getColumnModel().getColumn(0).setCellEditor(new DefaultCellEditor(new ActiveLayerCheckBox())); 205 layerList.getColumnModel().getColumn(0).setMaxWidth(12); 206 layerList.getColumnModel().getColumn(0).setPreferredWidth(12); 207 layerList.getColumnModel().getColumn(0).setResizable(false); 208 209 layerList.getColumnModel().getColumn(1).setCellRenderer(new NativeScaleLayerCellRenderer()); 210 layerList.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(new NativeScaleLayerCheckBox())); 211 layerList.getColumnModel().getColumn(1).setMaxWidth(12); 212 layerList.getColumnModel().getColumn(1).setPreferredWidth(12); 213 layerList.getColumnModel().getColumn(1).setResizable(false); 214 215 layerList.getColumnModel().getColumn(2).setCellRenderer(new OffsetLayerCellRenderer()); 216 layerList.getColumnModel().getColumn(2).setCellEditor(new DefaultCellEditor(new OffsetLayerCheckBox())); 217 layerList.getColumnModel().getColumn(2).setMaxWidth(16); 218 layerList.getColumnModel().getColumn(2).setPreferredWidth(16); 219 layerList.getColumnModel().getColumn(2).setResizable(false); 220 221 layerList.getColumnModel().getColumn(3).setCellRenderer(new LayerVisibleCellRenderer()); 222 layerList.getColumnModel().getColumn(3).setCellEditor(new LayerVisibleCellEditor(new LayerVisibleCheckBox())); 223 layerList.getColumnModel().getColumn(3).setMaxWidth(16); 224 layerList.getColumnModel().getColumn(3).setPreferredWidth(16); 225 layerList.getColumnModel().getColumn(3).setResizable(false); 226 227 layerList.getColumnModel().getColumn(4).setCellRenderer(new LayerNameCellRenderer()); 228 layerList.getColumnModel().getColumn(4).setCellEditor(new LayerNameCellEditor(new DisableShortcutsOnFocusGainedTextField())); 229 // Disable some default JTable shortcuts to use JOSM ones (see #5678, #10458) 230 for (KeyStroke ks : new KeyStroke[] { 231 KeyStroke.getKeyStroke(KeyEvent.VK_C, Main.platform.getMenuShortcutKeyMaskEx()), 232 KeyStroke.getKeyStroke(KeyEvent.VK_V, Main.platform.getMenuShortcutKeyMaskEx()), 233 KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_DOWN_MASK), 234 KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_DOWN_MASK), 235 KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_DOWN_MASK), 236 KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_DOWN_MASK), 237 KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.CTRL_DOWN_MASK), 238 KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK), 239 KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.CTRL_DOWN_MASK), 240 KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.CTRL_DOWN_MASK), 241 KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), 242 KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), 243 KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), 244 KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0), 245 }) { 246 layerList.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(ks, new Object()); 247 } 248 249 // init the model 250 // 251 model.populate(); 252 model.setSelectedLayer(layerManager.getActiveLayer()); 253 model.addLayerListModelListener( 254 new LayerListModelListener() { 255 @Override 256 public void makeVisible(int row, Layer layer) { 257 layerList.scrollToVisible(row, 0); 258 layerList.repaint(); 259 } 260 261 @Override 262 public void refresh() { 263 layerList.repaint(); 264 } 265 } 266 ); 267 268 // -- move up action 269 MoveUpAction moveUpAction = new MoveUpAction(model); 270 adaptTo(moveUpAction, model); 271 adaptTo(moveUpAction, selectionModel); 272 273 // -- move down action 274 MoveDownAction moveDownAction = new MoveDownAction(model); 275 adaptTo(moveDownAction, model); 276 adaptTo(moveDownAction, selectionModel); 277 278 // -- activate action 279 activateLayerAction = new ActivateLayerAction(model); 280 activateLayerAction.updateEnabledState(); 281 MultikeyActionsHandler.getInstance().addAction(activateLayerAction); 282 adaptTo(activateLayerAction, selectionModel); 283 284 JumpToMarkerActions.initialize(); 285 286 // -- show hide action 287 showHideLayerAction = new ShowHideLayerAction(model); 288 MultikeyActionsHandler.getInstance().addAction(showHideLayerAction); 289 adaptTo(showHideLayerAction, selectionModel); 290 291 LayerVisibilityAction visibilityAction = new LayerVisibilityAction(model); 292 adaptTo(visibilityAction, selectionModel); 293 SideButton visibilityButton = new SideButton(visibilityAction, false); 294 visibilityAction.setCorrespondingSideButton(visibilityButton); 295 296 // -- delete layer action 297 DeleteLayerAction deleteLayerAction = new DeleteLayerAction(model); 298 layerList.getActionMap().put("deleteLayer", deleteLayerAction); 299 adaptTo(deleteLayerAction, selectionModel); 300 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put( 301 KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete" 302 ); 303 getActionMap().put("delete", deleteLayerAction); 304 305 // Activate layer on Enter key press 306 InputMapUtils.addEnterAction(layerList, new AbstractAction() { 307 @Override 308 public void actionPerformed(ActionEvent e) { 309 activateLayerAction.actionPerformed(null); 310 layerList.requestFocus(); 311 } 312 }); 313 314 // Show/Activate layer on Enter key press 315 InputMapUtils.addSpacebarAction(layerList, showHideLayerAction); 316 317 createLayout(layerList, true, Arrays.asList( 318 new SideButton(moveUpAction, false), 319 new SideButton(moveDownAction, false), 320 new SideButton(activateLayerAction, false), 321 visibilityButton, 322 new SideButton(deleteLayerAction, false) 323 )); 324 325 createVisibilityToggleShortcuts(); 326 } 327 328 /** 329 * Gets the layer manager this dialog is for. 330 * @return The layer manager. 331 * @since 10288 332 */ 333 public MainLayerManager getLayerManager() { 334 return layerManager; 335 } 336 337 @Override 338 public void showNotify() { 339 layerManager.addActiveLayerChangeListener(activateLayerAction); 340 layerManager.addAndFireLayerChangeListener(model); 341 layerManager.addAndFireActiveLayerChangeListener(model); 342 model.populate(); 343 } 344 345 @Override 346 public void hideNotify() { 347 layerManager.removeAndFireLayerChangeListener(model); 348 layerManager.removeActiveLayerChangeListener(model); 349 layerManager.removeActiveLayerChangeListener(activateLayerAction); 350 } 351 352 /** 353 * Returns the layer list model. 354 * @return the layer list model 355 */ 356 public LayerListModel getModel() { 357 return model; 358 } 359 360 /** 361 * Wires <code>listener</code> to <code>listSelectionModel</code> in such a way, that 362 * <code>listener</code> receives a {@link IEnabledStateUpdating#updateEnabledState()} 363 * on every {@link ListSelectionEvent}. 364 * 365 * @param listener the listener 366 * @param listSelectionModel the source emitting {@link ListSelectionEvent}s 367 */ 368 protected void adaptTo(final IEnabledStateUpdating listener, ListSelectionModel listSelectionModel) { 369 listSelectionModel.addListSelectionListener(e -> listener.updateEnabledState()); 370 } 371 372 /** 373 * Wires <code>listener</code> to <code>listModel</code> in such a way, that 374 * <code>listener</code> receives a {@link IEnabledStateUpdating#updateEnabledState()} 375 * on every {@link ListDataEvent}. 376 * 377 * @param listener the listener 378 * @param listModel the source emitting {@link ListDataEvent}s 379 */ 380 protected void adaptTo(final IEnabledStateUpdating listener, LayerListModel listModel) { 381 listModel.addTableModelListener(e -> listener.updateEnabledState()); 382 } 383 384 @Override 385 public void destroy() { 386 for (int i = 0; i < 10; i++) { 387 MainApplication.unregisterActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]); 388 } 389 MultikeyActionsHandler.getInstance().removeAction(activateLayerAction); 390 MultikeyActionsHandler.getInstance().removeAction(showHideLayerAction); 391 JumpToMarkerActions.unregisterActions(); 392 super.destroy(); 393 instance = null; 394 } 395 396 static ImageIcon createBlankIcon() { 397 return ImageProvider.createBlankIcon(ImageSizes.LAYER); 398 } 399 400 private static class ActiveLayerCheckBox extends JCheckBox { 401 ActiveLayerCheckBox() { 402 setHorizontalAlignment(javax.swing.SwingConstants.CENTER); 403 ImageIcon blank = createBlankIcon(); 404 ImageIcon active = ImageProvider.get("dialogs/layerlist", "active"); 405 setIcon(blank); 406 setSelectedIcon(active); 407 setRolloverIcon(blank); 408 setRolloverSelectedIcon(active); 409 setPressedIcon(ImageProvider.get("dialogs/layerlist", "active-pressed")); 410 } 411 } 412 413 private static class LayerVisibleCheckBox extends JCheckBox { 414 private final ImageIcon iconEye; 415 private final ImageIcon iconEyeTranslucent; 416 private boolean isTranslucent; 417 418 /** 419 * Constructs a new {@code LayerVisibleCheckBox}. 420 */ 421 LayerVisibleCheckBox() { 422 setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); 423 iconEye = ImageProvider.get("dialogs/layerlist", "eye"); 424 iconEyeTranslucent = ImageProvider.get("dialogs/layerlist", "eye-translucent"); 425 setIcon(ImageProvider.get("dialogs/layerlist", "eye-off")); 426 setPressedIcon(ImageProvider.get("dialogs/layerlist", "eye-pressed")); 427 setSelectedIcon(iconEye); 428 isTranslucent = false; 429 } 430 431 public void setTranslucent(boolean isTranslucent) { 432 if (this.isTranslucent == isTranslucent) return; 433 if (isTranslucent) { 434 setSelectedIcon(iconEyeTranslucent); 435 } else { 436 setSelectedIcon(iconEye); 437 } 438 this.isTranslucent = isTranslucent; 439 } 440 441 public void updateStatus(Layer layer) { 442 boolean visible = layer.isVisible(); 443 setSelected(visible); 444 setTranslucent(layer.getOpacity() < 1.0); 445 setToolTipText(visible ? 446 tr("layer is currently visible (click to hide layer)") : 447 tr("layer is currently hidden (click to show layer)")); 448 } 449 } 450 451 private static class NativeScaleLayerCheckBox extends JCheckBox { 452 NativeScaleLayerCheckBox() { 453 setHorizontalAlignment(javax.swing.SwingConstants.CENTER); 454 ImageIcon blank = createBlankIcon(); 455 ImageIcon active = ImageProvider.get("dialogs/layerlist", "scale"); 456 setIcon(blank); 457 setSelectedIcon(active); 458 } 459 } 460 461 private static class OffsetLayerCheckBox extends JCheckBox { 462 OffsetLayerCheckBox() { 463 setHorizontalAlignment(javax.swing.SwingConstants.CENTER); 464 ImageIcon blank = createBlankIcon(); 465 ImageIcon withOffset = ImageProvider.get("dialogs/layerlist", "offset"); 466 setIcon(blank); 467 setSelectedIcon(withOffset); 468 } 469 } 470 471 private static class ActiveLayerCellRenderer implements TableCellRenderer { 472 private final JCheckBox cb; 473 474 /** 475 * Constructs a new {@code ActiveLayerCellRenderer}. 476 */ 477 ActiveLayerCellRenderer() { 478 cb = new ActiveLayerCheckBox(); 479 } 480 481 @Override 482 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 483 boolean active = value != null && (Boolean) value; 484 cb.setSelected(active); 485 cb.setToolTipText(active ? tr("this layer is the active layer") : tr("this layer is not currently active (click to activate)")); 486 return cb; 487 } 488 } 489 490 private static class LayerVisibleCellRenderer implements TableCellRenderer { 491 private final LayerVisibleCheckBox cb; 492 493 /** 494 * Constructs a new {@code LayerVisibleCellRenderer}. 495 */ 496 LayerVisibleCellRenderer() { 497 this.cb = new LayerVisibleCheckBox(); 498 } 499 500 @Override 501 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 502 if (value != null) { 503 cb.updateStatus((Layer) value); 504 } 505 return cb; 506 } 507 } 508 509 private static class LayerVisibleCellEditor extends DefaultCellEditor { 510 private final LayerVisibleCheckBox cb; 511 512 LayerVisibleCellEditor(LayerVisibleCheckBox cb) { 513 super(cb); 514 this.cb = cb; 515 } 516 517 @Override 518 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 519 cb.updateStatus((Layer) value); 520 return cb; 521 } 522 } 523 524 private static class NativeScaleLayerCellRenderer implements TableCellRenderer { 525 private final JCheckBox cb; 526 527 /** 528 * Constructs a new {@code ActiveLayerCellRenderer}. 529 */ 530 NativeScaleLayerCellRenderer() { 531 cb = new NativeScaleLayerCheckBox(); 532 } 533 534 @Override 535 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 536 Layer layer = (Layer) value; 537 if (layer instanceof NativeScaleLayer) { 538 boolean active = ((NativeScaleLayer) layer) == MainApplication.getMap().mapView.getNativeScaleLayer(); 539 cb.setSelected(active); 540 cb.setToolTipText(active 541 ? tr("scale follows native resolution of this layer") 542 : tr("scale follows native resolution of another layer (click to set this layer)") 543 ); 544 } else { 545 cb.setSelected(false); 546 cb.setToolTipText(tr("this layer has no native resolution")); 547 } 548 return cb; 549 } 550 } 551 552 private static class OffsetLayerCellRenderer implements TableCellRenderer { 553 private final JCheckBox cb; 554 555 /** 556 * Constructs a new {@code OffsetLayerCellRenderer}. 557 */ 558 OffsetLayerCellRenderer() { 559 cb = new OffsetLayerCheckBox(); 560 cb.setEnabled(false); 561 } 562 563 @Override 564 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 565 Layer layer = (Layer) value; 566 if (layer instanceof AbstractTileSourceLayer<?>) { 567 if (EastNorth.ZERO.equals(((AbstractTileSourceLayer<?>) layer).getDisplaySettings().getDisplacement())) { 568 cb.setSelected(false); 569 cb.setEnabled(false); // TODO: allow reselecting checkbox and thereby setting the old offset again 570 cb.setToolTipText(tr("layer is without a user-defined offset")); 571 } else { 572 cb.setSelected(true); 573 cb.setEnabled(true); 574 cb.setToolTipText(tr("layer has a user-defined offset (click to remove offset)")); 575 } 576 577 } else { 578 cb.setSelected(false); 579 cb.setEnabled(false); 580 cb.setToolTipText(tr("this layer can not have an offset")); 581 } 582 return cb; 583 } 584 } 585 586 private class LayerNameCellRenderer extends DefaultTableCellRenderer { 587 588 protected boolean isActiveLayer(Layer layer) { 589 return getLayerManager().getActiveLayer() == layer; 590 } 591 592 @Override 593 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 594 if (value == null) 595 return this; 596 Layer layer = (Layer) value; 597 JLabel label = (JLabel) super.getTableCellRendererComponent(table, 598 layer.getName(), isSelected, hasFocus, row, column); 599 if (isActiveLayer(layer)) { 600 label.setFont(label.getFont().deriveFont(Font.BOLD)); 601 } 602 if (Config.getPref().getBoolean("dialog.layer.colorname", true)) { 603 AbstractProperty<Color> prop = layer.getColorProperty(); 604 Color c = prop == null ? null : prop.get(); 605 if (c == null || model.getLayers().stream() 606 .map(Layer::getColorProperty) 607 .filter(Objects::nonNull) 608 .map(AbstractProperty::get) 609 .noneMatch(oc -> oc != null && !oc.equals(c))) { 610 /* not more than one color, don't use coloring */ 611 label.setForeground(UIManager.getColor(isSelected ? "Table.selectionForeground" : "Table.foreground")); 612 } else { 613 label.setForeground(c); 614 } 615 } 616 label.setIcon(layer.getIcon()); 617 label.setToolTipText(layer.getToolTipText()); 618 return label; 619 } 620 } 621 622 private static class LayerNameCellEditor extends DefaultCellEditor { 623 LayerNameCellEditor(DisableShortcutsOnFocusGainedTextField tf) { 624 super(tf); 625 } 626 627 @Override 628 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 629 JosmTextField tf = (JosmTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column); 630 tf.setText(value == null ? "" : ((Layer) value).getName()); 631 return tf; 632 } 633 } 634 635 class PopupMenuHandler extends PopupMenuLauncher { 636 @Override 637 public void showMenu(MouseEvent evt) { 638 menu = new LayerListPopup(getModel().getSelectedLayers()); 639 super.showMenu(evt); 640 } 641 } 642 643 /** 644 * Observer interface to be implemented by views using {@link LayerListModel}. 645 */ 646 public interface LayerListModelListener { 647 648 /** 649 * Fired when a layer is made visible. 650 * @param index the layer index 651 * @param layer the layer 652 */ 653 void makeVisible(int index, Layer layer); 654 655 656 /** 657 * Fired when something has changed in the layer list model. 658 */ 659 void refresh(); 660 } 661 662 /** 663 * The layer list model. The model manages a list of layers and provides methods for 664 * moving layers up and down, for toggling their visibility, and for activating a layer. 665 * 666 * The model is a {@link TableModel} and it provides a {@link ListSelectionModel}. It expects 667 * to be configured with a {@link DefaultListSelectionModel}. The selection model is used 668 * to update the selection state of views depending on messages sent to the model. 669 * 670 * The model manages a list of {@link LayerListModelListener} which are mainly notified if 671 * the model requires views to make a specific list entry visible. 672 * 673 * It also listens to {@link PropertyChangeEvent}s of every {@link Layer} it manages, in particular to 674 * the properties {@link Layer#VISIBLE_PROP} and {@link Layer#NAME_PROP}. 675 */ 676 public static final class LayerListModel extends AbstractTableModel 677 implements LayerChangeListener, ActiveLayerChangeListener, PropertyChangeListener { 678 /** manages list selection state*/ 679 private final DefaultListSelectionModel selectionModel; 680 private final CopyOnWriteArrayList<LayerListModelListener> listeners; 681 private LayerList layerList; 682 private final MainLayerManager layerManager; 683 684 /** 685 * constructor 686 * @param layerManager The layer manager to use for the list. 687 * @param selectionModel the list selection model 688 */ 689 LayerListModel(MainLayerManager layerManager, DefaultListSelectionModel selectionModel) { 690 this.layerManager = layerManager; 691 this.selectionModel = selectionModel; 692 listeners = new CopyOnWriteArrayList<>(); 693 } 694 695 void setLayerList(LayerList layerList) { 696 this.layerList = layerList; 697 } 698 699 /** 700 * The layer manager this model is for. 701 * @return The layer manager. 702 */ 703 public MainLayerManager getLayerManager() { 704 return layerManager; 705 } 706 707 /** 708 * Adds a listener to this model 709 * 710 * @param listener the listener 711 */ 712 public void addLayerListModelListener(LayerListModelListener listener) { 713 if (listener != null) { 714 listeners.addIfAbsent(listener); 715 } 716 } 717 718 /** 719 * removes a listener from this model 720 * @param listener the listener 721 */ 722 public void removeLayerListModelListener(LayerListModelListener listener) { 723 listeners.remove(listener); 724 } 725 726 /** 727 * Fires a make visible event to listeners 728 * 729 * @param index the index of the row to make visible 730 * @param layer the layer at this index 731 * @see LayerListModelListener#makeVisible(int, Layer) 732 */ 733 private void fireMakeVisible(int index, Layer layer) { 734 for (LayerListModelListener listener : listeners) { 735 listener.makeVisible(index, layer); 736 } 737 } 738 739 /** 740 * Fires a refresh event to listeners of this model 741 * 742 * @see LayerListModelListener#refresh() 743 */ 744 private void fireRefresh() { 745 for (LayerListModelListener listener : listeners) { 746 listener.refresh(); 747 } 748 } 749 750 /** 751 * Populates the model with the current layers managed by {@link MapView}. 752 */ 753 public void populate() { 754 for (Layer layer: getLayers()) { 755 // make sure the model is registered exactly once 756 layer.removePropertyChangeListener(this); 757 layer.addPropertyChangeListener(this); 758 } 759 fireTableDataChanged(); 760 } 761 762 /** 763 * Marks <code>layer</code> as selected layer. Ignored, if layer is null. 764 * 765 * @param layer the layer. 766 */ 767 public void setSelectedLayer(Layer layer) { 768 if (layer == null) 769 return; 770 int idx = getLayers().indexOf(layer); 771 if (idx >= 0) { 772 selectionModel.setSelectionInterval(idx, idx); 773 } 774 ensureSelectedIsVisible(); 775 } 776 777 /** 778 * Replies the list of currently selected layers. Never null, but may be empty. 779 * 780 * @return the list of currently selected layers. Never null, but may be empty. 781 */ 782 public List<Layer> getSelectedLayers() { 783 List<Layer> selected = new ArrayList<>(); 784 List<Layer> layers = getLayers(); 785 for (int i = 0; i < layers.size(); i++) { 786 if (selectionModel.isSelectedIndex(i)) { 787 selected.add(layers.get(i)); 788 } 789 } 790 return selected; 791 } 792 793 /** 794 * Replies a the list of indices of the selected rows. Never null, but may be empty. 795 * 796 * @return the list of indices of the selected rows. Never null, but may be empty. 797 */ 798 public List<Integer> getSelectedRows() { 799 List<Integer> selected = new ArrayList<>(); 800 for (int i = 0; i < getLayers().size(); i++) { 801 if (selectionModel.isSelectedIndex(i)) { 802 selected.add(i); 803 } 804 } 805 return selected; 806 } 807 808 /** 809 * Invoked if a layer managed by {@link MapView} is removed 810 * 811 * @param layer the layer which is removed 812 */ 813 private void onRemoveLayer(Layer layer) { 814 if (layer == null) 815 return; 816 layer.removePropertyChangeListener(this); 817 final int size = getRowCount(); 818 final List<Integer> rows = getSelectedRows(); 819 820 if (rows.isEmpty() && size > 0) { 821 selectionModel.setSelectionInterval(size-1, size-1); 822 } 823 fireTableDataChanged(); 824 fireRefresh(); 825 ensureActiveSelected(); 826 } 827 828 /** 829 * Invoked when a layer managed by {@link MapView} is added 830 * 831 * @param layer the layer 832 */ 833 private void onAddLayer(Layer layer) { 834 if (layer == null) 835 return; 836 layer.addPropertyChangeListener(this); 837 fireTableDataChanged(); 838 int idx = getLayers().indexOf(layer); 839 Icon icon = layer.getIcon(); 840 if (layerList != null && icon != null) { 841 layerList.setRowHeight(idx, Math.max(16, icon.getIconHeight())); 842 } 843 selectionModel.setSelectionInterval(idx, idx); 844 ensureSelectedIsVisible(); 845 if (layer instanceof AbstractTileSourceLayer<?>) { 846 ((AbstractTileSourceLayer<?>) layer).getDisplaySettings().addSettingsChangeListener(LayerListDialog.getInstance()); 847 } 848 } 849 850 /** 851 * Replies the first layer. Null if no layers are present 852 * 853 * @return the first layer. Null if no layers are present 854 */ 855 public Layer getFirstLayer() { 856 if (getRowCount() == 0) 857 return null; 858 return getLayers().get(0); 859 } 860 861 /** 862 * Replies the layer at position <code>index</code> 863 * 864 * @param index the index 865 * @return the layer at position <code>index</code>. Null, 866 * if index is out of range. 867 */ 868 public Layer getLayer(int index) { 869 if (index < 0 || index >= getRowCount()) 870 return null; 871 return getLayers().get(index); 872 } 873 874 /** 875 * Replies true if the currently selected layers can move up by one position 876 * 877 * @return true if the currently selected layers can move up by one position 878 */ 879 public boolean canMoveUp() { 880 List<Integer> sel = getSelectedRows(); 881 return !sel.isEmpty() && sel.get(0) > 0; 882 } 883 884 /** 885 * Move up the currently selected layers by one position 886 * 887 */ 888 public void moveUp() { 889 if (!canMoveUp()) 890 return; 891 List<Integer> sel = getSelectedRows(); 892 List<Layer> layers = getLayers(); 893 MapView mapView = MainApplication.getMap().mapView; 894 for (int row : sel) { 895 Layer l1 = layers.get(row); 896 mapView.moveLayer(l1, row-1); 897 } 898 fireTableDataChanged(); 899 selectionModel.setValueIsAdjusting(true); 900 selectionModel.clearSelection(); 901 for (int row : sel) { 902 selectionModel.addSelectionInterval(row-1, row-1); 903 } 904 selectionModel.setValueIsAdjusting(false); 905 ensureSelectedIsVisible(); 906 } 907 908 /** 909 * Replies true if the currently selected layers can move down by one position 910 * 911 * @return true if the currently selected layers can move down by one position 912 */ 913 public boolean canMoveDown() { 914 List<Integer> sel = getSelectedRows(); 915 return !sel.isEmpty() && sel.get(sel.size()-1) < getLayers().size()-1; 916 } 917 918 /** 919 * Move down the currently selected layers by one position 920 */ 921 public void moveDown() { 922 if (!canMoveDown()) 923 return; 924 List<Integer> sel = getSelectedRows(); 925 Collections.reverse(sel); 926 List<Layer> layers = getLayers(); 927 MapView mapView = MainApplication.getMap().mapView; 928 for (int row : sel) { 929 Layer l1 = layers.get(row); 930 mapView.moveLayer(l1, row+1); 931 } 932 fireTableDataChanged(); 933 selectionModel.setValueIsAdjusting(true); 934 selectionModel.clearSelection(); 935 for (int row : sel) { 936 selectionModel.addSelectionInterval(row+1, row+1); 937 } 938 selectionModel.setValueIsAdjusting(false); 939 ensureSelectedIsVisible(); 940 } 941 942 /** 943 * Make sure the first of the selected layers is visible in the views of this model. 944 */ 945 private void ensureSelectedIsVisible() { 946 int index = selectionModel.getMinSelectionIndex(); 947 if (index < 0) 948 return; 949 List<Layer> layers = getLayers(); 950 if (index >= layers.size()) 951 return; 952 Layer layer = layers.get(index); 953 fireMakeVisible(index, layer); 954 } 955 956 /** 957 * Replies a list of layers which are possible merge targets for <code>source</code> 958 * 959 * @param source the source layer 960 * @return a list of layers which are possible merge targets 961 * for <code>source</code>. Never null, but can be empty. 962 */ 963 public List<Layer> getPossibleMergeTargets(Layer source) { 964 List<Layer> targets = new ArrayList<>(); 965 if (source == null) { 966 return targets; 967 } 968 for (Layer target : getLayers()) { 969 if (source == target) { 970 continue; 971 } 972 if (target.isMergable(source) && source.isMergable(target)) { 973 targets.add(target); 974 } 975 } 976 return targets; 977 } 978 979 /** 980 * Replies the list of layers currently managed by {@link MapView}. 981 * Never null, but can be empty. 982 * 983 * @return the list of layers currently managed by {@link MapView}. 984 * Never null, but can be empty. 985 */ 986 public List<Layer> getLayers() { 987 return getLayerManager().getLayers(); 988 } 989 990 /** 991 * Ensures that at least one layer is selected in the layer dialog 992 * 993 */ 994 private void ensureActiveSelected() { 995 List<Layer> layers = getLayers(); 996 if (layers.isEmpty()) 997 return; 998 final Layer activeLayer = getActiveLayer(); 999 if (activeLayer != null) { 1000 // there's an active layer - select it and make it visible 1001 int idx = layers.indexOf(activeLayer); 1002 selectionModel.setSelectionInterval(idx, idx); 1003 ensureSelectedIsVisible(); 1004 } else { 1005 // no active layer - select the first one and make it visible 1006 selectionModel.setSelectionInterval(0, 0); 1007 ensureSelectedIsVisible(); 1008 } 1009 } 1010 1011 /** 1012 * Replies the active layer. null, if no active layer is available 1013 * 1014 * @return the active layer. null, if no active layer is available 1015 */ 1016 private Layer getActiveLayer() { 1017 return getLayerManager().getActiveLayer(); 1018 } 1019 1020 /* ------------------------------------------------------------------------------ */ 1021 /* Interface TableModel */ 1022 /* ------------------------------------------------------------------------------ */ 1023 1024 @Override 1025 public int getRowCount() { 1026 List<Layer> layers = getLayers(); 1027 return layers == null ? 0 : layers.size(); 1028 } 1029 1030 @Override 1031 public int getColumnCount() { 1032 return 5; 1033 } 1034 1035 @Override 1036 public Object getValueAt(int row, int col) { 1037 List<Layer> layers = getLayers(); 1038 if (row >= 0 && row < layers.size()) { 1039 switch (col) { 1040 case 0: return layers.get(row) == getActiveLayer(); 1041 case 1: 1042 case 2: 1043 case 3: 1044 case 4: return layers.get(row); 1045 default: // Do nothing 1046 } 1047 } 1048 return null; 1049 } 1050 1051 @Override 1052 public boolean isCellEditable(int row, int col) { 1053 return col != 0 || getActiveLayer() != getLayers().get(row); 1054 } 1055 1056 @Override 1057 public void setValueAt(Object value, int row, int col) { 1058 List<Layer> layers = getLayers(); 1059 if (row < layers.size()) { 1060 Layer l = layers.get(row); 1061 switch (col) { 1062 case 0: 1063 getLayerManager().setActiveLayer(l); 1064 l.setVisible(true); 1065 break; 1066 case 1: 1067 MapFrame map = MainApplication.getMap(); 1068 NativeScaleLayer oldLayer = map.mapView.getNativeScaleLayer(); 1069 if (oldLayer == l) { 1070 map.mapView.setNativeScaleLayer(null); 1071 } else if (l instanceof NativeScaleLayer) { 1072 map.mapView.setNativeScaleLayer((NativeScaleLayer) l); 1073 if (oldLayer instanceof Layer) { 1074 int idx = getLayers().indexOf((Layer) oldLayer); 1075 if (idx >= 0) { 1076 fireTableCellUpdated(idx, col); 1077 } 1078 } 1079 } 1080 break; 1081 case 2: 1082 // reset layer offset 1083 if (l instanceof AbstractTileSourceLayer<?>) { 1084 AbstractTileSourceLayer<?> abstractTileSourceLayer = (AbstractTileSourceLayer<?>) l; 1085 OffsetBookmark offsetBookmark = abstractTileSourceLayer.getDisplaySettings().getOffsetBookmark(); 1086 if (offsetBookmark != null) { 1087 offsetBookmark.setDisplacement(EastNorth.ZERO); 1088 abstractTileSourceLayer.getDisplaySettings().setOffsetBookmark(offsetBookmark); 1089 } 1090 } 1091 break; 1092 case 3: 1093 l.setVisible((Boolean) value); 1094 break; 1095 case 4: 1096 l.rename((String) value); 1097 break; 1098 default: 1099 throw new IllegalArgumentException("Wrong column: " + col); 1100 } 1101 fireTableCellUpdated(row, col); 1102 } 1103 } 1104 1105 /* ------------------------------------------------------------------------------ */ 1106 /* Interface ActiveLayerChangeListener */ 1107 /* ------------------------------------------------------------------------------ */ 1108 @Override 1109 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 1110 Layer oldLayer = e.getPreviousActiveLayer(); 1111 if (oldLayer != null) { 1112 int idx = getLayers().indexOf(oldLayer); 1113 if (idx >= 0) { 1114 fireTableRowsUpdated(idx, idx); 1115 } 1116 } 1117 1118 Layer newLayer = getActiveLayer(); 1119 if (newLayer != null) { 1120 int idx = getLayers().indexOf(newLayer); 1121 if (idx >= 0) { 1122 fireTableRowsUpdated(idx, idx); 1123 } 1124 } 1125 ensureActiveSelected(); 1126 } 1127 1128 /* ------------------------------------------------------------------------------ */ 1129 /* Interface LayerChangeListener */ 1130 /* ------------------------------------------------------------------------------ */ 1131 @Override 1132 public void layerAdded(LayerAddEvent e) { 1133 onAddLayer(e.getAddedLayer()); 1134 } 1135 1136 @Override 1137 public void layerRemoving(LayerRemoveEvent e) { 1138 onRemoveLayer(e.getRemovedLayer()); 1139 } 1140 1141 @Override 1142 public void layerOrderChanged(LayerOrderChangeEvent e) { 1143 fireTableDataChanged(); 1144 } 1145 1146 /* ------------------------------------------------------------------------------ */ 1147 /* Interface PropertyChangeListener */ 1148 /* ------------------------------------------------------------------------------ */ 1149 @Override 1150 public void propertyChange(PropertyChangeEvent evt) { 1151 if (evt.getSource() instanceof Layer) { 1152 Layer layer = (Layer) evt.getSource(); 1153 final int idx = getLayers().indexOf(layer); 1154 if (idx < 0) 1155 return; 1156 fireRefresh(); 1157 } 1158 } 1159 } 1160 1161 /** 1162 * This component displays a list of layers and provides the methods needed by {@link LayerListModel}. 1163 */ 1164 static class LayerList extends ScrollableTable { 1165 1166 LayerList(LayerListModel dataModel) { 1167 super(dataModel); 1168 dataModel.setLayerList(this); 1169 if (!GraphicsEnvironment.isHeadless()) { 1170 setDragEnabled(true); 1171 } 1172 setDropMode(DropMode.INSERT_ROWS); 1173 setTransferHandler(new LayerListTransferHandler()); 1174 } 1175 1176 @Override 1177 public LayerListModel getModel() { 1178 return (LayerListModel) super.getModel(); 1179 } 1180 } 1181 1182 /** 1183 * Creates a {@link ShowHideLayerAction} in the context of this {@link LayerListDialog}. 1184 * 1185 * @return the action 1186 */ 1187 public ShowHideLayerAction createShowHideLayerAction() { 1188 return new ShowHideLayerAction(model); 1189 } 1190 1191 /** 1192 * Creates a {@link DeleteLayerAction} in the context of this {@link LayerListDialog}. 1193 * 1194 * @return the action 1195 */ 1196 public DeleteLayerAction createDeleteLayerAction() { 1197 return new DeleteLayerAction(model); 1198 } 1199 1200 /** 1201 * Creates a {@link ActivateLayerAction} for <code>layer</code> in the context of this {@link LayerListDialog}. 1202 * 1203 * @param layer the layer 1204 * @return the action 1205 */ 1206 public ActivateLayerAction createActivateLayerAction(Layer layer) { 1207 return new ActivateLayerAction(layer, model); 1208 } 1209 1210 /** 1211 * Creates a {@link MergeLayerAction} for <code>layer</code> in the context of this {@link LayerListDialog}. 1212 * 1213 * @param layer the layer 1214 * @return the action 1215 */ 1216 public MergeAction createMergeLayerAction(Layer layer) { 1217 return new MergeAction(layer, model); 1218 } 1219 1220 /** 1221 * Creates a {@link DuplicateAction} for <code>layer</code> in the context of this {@link LayerListDialog}. 1222 * 1223 * @param layer the layer 1224 * @return the action 1225 */ 1226 public DuplicateAction createDuplicateLayerAction(Layer layer) { 1227 return new DuplicateAction(layer, model); 1228 } 1229 1230 /** 1231 * Returns the layer at given index, or {@code null}. 1232 * @param index the index 1233 * @return the layer at given index, or {@code null} if index out of range 1234 */ 1235 public static Layer getLayerForIndex(int index) { 1236 List<Layer> layers = MainApplication.getLayerManager().getLayers(); 1237 1238 if (index < layers.size() && index >= 0) 1239 return layers.get(index); 1240 else 1241 return null; 1242 } 1243 1244 /** 1245 * Returns a list of info on all layers of a given class. 1246 * @param layerClass The layer class. This is not {@code Class<? extends Layer>} on purpose, 1247 * to allow asking for layers implementing some interface 1248 * @return list of info on all layers assignable from {@code layerClass} 1249 */ 1250 public static List<MultikeyInfo> getLayerInfoByClass(Class<?> layerClass) { 1251 List<MultikeyInfo> result = new ArrayList<>(); 1252 1253 List<Layer> layers = MainApplication.getLayerManager().getLayers(); 1254 1255 int index = 0; 1256 for (Layer l: layers) { 1257 if (layerClass.isAssignableFrom(l.getClass())) { 1258 result.add(new MultikeyInfo(index, l.getName())); 1259 } 1260 index++; 1261 } 1262 1263 return result; 1264 } 1265 1266 /** 1267 * Determines if a layer is valid (contained in global layer list). 1268 * @param l the layer 1269 * @return {@code true} if layer {@code l} is contained in current layer list 1270 */ 1271 public static boolean isLayerValid(Layer l) { 1272 if (l == null) 1273 return false; 1274 1275 return MainApplication.getLayerManager().containsLayer(l); 1276 } 1277 1278 /** 1279 * Returns info about layer. 1280 * @param l the layer 1281 * @return info about layer {@code l} 1282 */ 1283 public static MultikeyInfo getLayerInfo(Layer l) { 1284 if (l == null) 1285 return null; 1286 1287 int index = MainApplication.getLayerManager().getLayers().indexOf(l); 1288 if (index < 0) 1289 return null; 1290 1291 return new MultikeyInfo(index, l.getName()); 1292 } 1293 1294 @Override 1295 public void displaySettingsChanged(DisplaySettingsChangeEvent e) { 1296 if ("displacement".equals(e.getChangedSetting())) { 1297 layerList.repaint(); 1298 } 1299 } 1300}