MapWithAI/src/main/java/org/openstreetmap/josm/plugins/mapwithai/gui/preferences/mapwithai/MapWithAIProvidersPanel.java

872 wiersze
38 KiB
Java

// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.mapwithai.gui.preferences.mapwithai;
import static org.openstreetmap.josm.tools.I18n.marktr;
import static org.openstreetmap.josm.tools.I18n.tr;
import javax.swing.AbstractAction;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JToolBar;
import javax.swing.UIManager;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableColumnModel;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.openstreetmap.gui.jmapviewer.Coordinate;
import org.openstreetmap.gui.jmapviewer.MapPolygonImpl;
import org.openstreetmap.gui.jmapviewer.MapRectangleImpl;
import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
import org.openstreetmap.gui.jmapviewer.interfaces.MapPolygon;
import org.openstreetmap.gui.jmapviewer.interfaces.MapRectangle;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
import org.openstreetmap.josm.data.imagery.Shape;
import org.openstreetmap.josm.data.preferences.NamedColorProperty;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.gui.bbox.SlippyMapBBoxChooser;
import org.openstreetmap.josm.gui.preferences.imagery.ImageryProvidersPanel;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.gui.widgets.FilterField;
import org.openstreetmap.josm.gui.widgets.HtmlPanel;
import org.openstreetmap.josm.plugins.mapwithai.backend.MapWithAIDataUtils;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAICategory;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAIInfo;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAILayerInfo;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAILayerInfo.LayerChangeListener;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAIType;
import org.openstreetmap.josm.plugins.mapwithai.io.mapwithai.ESRISourceReader;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
import org.openstreetmap.josm.tools.ImageResource;
import org.openstreetmap.josm.tools.ListenerList;
import org.openstreetmap.josm.tools.Logging;
import org.openstreetmap.josm.tools.OpenBrowser;
/**
* A panel displaying imagery providers. Largely duplicates
* {@link ImageryProvidersPanel}.
*
* @since 15115 (extracted from ImageryPreferences)
*/
public class MapWithAIProvidersPanel extends JPanel {
public enum Options {
/** Hide the active table */
SHOW_ACTIVE
}
private static final long serialVersionUID = -5876039771496409422L;
// Public JTables and JosmMapViewer
/** The table of active providers **/
public final JTable activeTable;
/** The table of default providers **/
public final JTable defaultTable;
/** The filter of default providers **/
private final FilterField defaultFilter;
/** The directory for dialog images */
private static final String DIALOG_IMAGES_DIR = "dialogs";
/**
* The selection listener synchronizing map display with table of default
* providers
**/
private final transient DefListSelectionListener defaultTableListener;
/** The map displaying imagery bounds of selected default providers **/
public final SlippyMapBBoxChooser defaultMap;
// Public models
/** The model of active providers **/
public static final MapWithAILayerTableModel ACTIVE_MODEL = new MapWithAILayerTableModel();
/** The model of default providers **/
public static final MapWithAIDefaultLayerTableModel DEFAULT_MODEL = new MapWithAIDefaultLayerTableModel();
// Public JToolbars
/** The toolbar on the right of active providers **/
public final JToolBar activeToolbar;
/** The toolbar on the middle of the panel **/
public final JToolBar middleToolbar;
/** The toolbar on the right of default providers **/
public final JToolBar defaultToolbar;
// Private members
private final JComponent gui;
private final transient ListenerList<AreaListener> areaListeners = ListenerList.create();
/** Options that were passed to the constructor */
private final Options[] options;
protected interface AreaListener {
void updateArea(Bounds area);
}
/**
* class to render an information of MapWithAI source
*
* @param <T> type of information
*/
private static class MapWithAITableCellRenderer<T> extends DefaultTableCellRenderer implements AreaListener {
private static final NamedColorProperty IMAGERY_BACKGROUND_COLOR = new NamedColorProperty(
marktr("MapWithAI Background: Default"), new Color(200, 255, 200));
private static final NamedColorProperty MAPWITHAI_AREA_BACKGROUND_COLOR = new NamedColorProperty(
marktr("MapWithAI Background: Layer in area"), Color.decode("#f1ffc7"));
private final transient Function<T, Object> mapper;
private final transient Function<T, String> tooltip;
private final transient BiConsumer<T, JLabel> decorator;
private final transient Function<Object, MapWithAIInfo> reverseMapper;
private final boolean highlightIfActive;
private transient Bounds area;
/**
* Initialize a cell renderer with specific rules
*
* @param mapper Map from &lt;T&gt; to an Object (to get a cell
* renderer)
* @param reverseMapper Map from an Object to &lt;T&gt;
* @param tooltip The tooltip to show
* @param decorator The decorator
* @param highlightIfActive If true, highlight when the entry is activated
*/
MapWithAITableCellRenderer(Function<T, Object> mapper, Function<Object, MapWithAIInfo> reverseMapper,
Function<T, String> tooltip, BiConsumer<T, JLabel> decorator, boolean highlightIfActive) {
this.mapper = mapper;
this.reverseMapper = reverseMapper;
this.tooltip = tooltip;
this.decorator = decorator;
this.highlightIfActive = highlightIfActive;
}
@Override
@SuppressWarnings("unchecked")
public final Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
boolean hasFocus, int row, int column) {
T obj = (T) value;
JLabel label = (JLabel) super.getTableCellRendererComponent(table, mapper.apply(obj), isSelected, hasFocus,
row, column);
Color defaultColor = UIManager.getColor("Table.background");
Color selectedColor = UIManager.getColor("Table.selectionBackground");
GuiHelper.setBackgroundReadable(label, defaultColor);
GuiHelper.setBackgroundReadable(label, isSelected ? selectedColor : defaultColor);
if (this.highlightIfActive) {
MapWithAIInfo info = obj instanceof MapWithAIInfo ? (MapWithAIInfo) obj : reverseMapper.apply(obj);
if (info == null) {
GuiHelper.setBackgroundReadable(label, defaultColor);
} else {
if (MapWithAILayerTableModel.contains(info)) {
Color t = IMAGERY_BACKGROUND_COLOR.get();
GuiHelper.setBackgroundReadable(label, isSelected ? t.darker() : t);
} else if (this.area != null && info.getBounds() != null
&& (this.area.intersects(info.getBounds()) || info.getBounds().intersects(this.area))) {
Color t = MAPWITHAI_AREA_BACKGROUND_COLOR.get();
GuiHelper.setBackgroundReadable(label, isSelected ? t.darker() : t);
} else {
GuiHelper.setBackgroundReadable(label, isSelected ? selectedColor : defaultColor);
}
}
}
if (obj != null) {
label.setToolTipText(tooltip.apply(obj));
if (decorator != null) {
decorator.accept(obj, label);
}
}
return label;
}
@SuppressWarnings("hiding")
@Override
public void updateArea(Bounds area) {
this.area = area;
}
}
/**
* class to render the license/terms of use URL of MapWithAI source
*/
private static class MapWithAILicenseTableCellRenderer extends MapWithAITableCellRenderer<String> {
MapWithAILicenseTableCellRenderer() {
super(s -> s != null && !s.isEmpty() ? "<html><a href=\"" + s + "\">License</a>" : "",
u -> MapWithAILayerInfo.getInstance().getAllDefaultLayers().stream()
.filter(i -> u.equals(i.getUrl())).findFirst().orElse(null),
u -> u, null, true);
}
}
/**
* class to render the URL information of MapWithAI source
*
* @since 8065
*/
private static class MapWithAIURLTableCellRenderer extends MapWithAITableCellRenderer<String> {
MapWithAIURLTableCellRenderer() {
super(s -> s, u -> MapWithAILayerInfo.getInstance().getAllDefaultLayers().stream()
.filter(i -> u.equals(i.getUrl())).findFirst().orElse(null), u -> u, null, true);
}
}
/**
* class to render the category information of MapWithAI source
*/
private static class MapWithAICategoryTableCellRenderer
extends MapWithAIProvidersPanel.MapWithAITableCellRenderer<MapWithAICategory> {
MapWithAICategoryTableCellRenderer() {
super(cat -> null, i -> null, cat -> tr("MapWithAI category: {0}", cat.getDescription()),
(cat, label) -> label.setIcon(cat.getIcon(ImageSizes.TABLE)), false);
}
}
/**
* class to render the country information of MapWithAI source
*/
private static class MapWithAITypeTableCellRenderer
extends MapWithAIProvidersPanel.MapWithAITableCellRenderer<List<String>> {
MapWithAITypeTableCellRenderer() {
super(MapWithAITypeTableCellRenderer::joinList, i -> null, MapWithAITypeTableCellRenderer::joinList, null,
false);
}
private static String joinList(List<String> list) {
return list != null ? String.join(",", list) : "";
}
}
/**
* class to render the source provider information of a MapWithAI source
*/
private static class MapWithAIProviderTableCellRenderer
extends MapWithAIProvidersPanel.MapWithAITableCellRenderer<String> {
MapWithAIProviderTableCellRenderer() {
super(s -> s, s -> null, s -> s, null, false);
}
}
/**
* class to render the name information of Imagery source
*/
private static class MapWithAINameTableCellRenderer
extends MapWithAIProvidersPanel.MapWithAITableCellRenderer<MapWithAIInfo> {
private static final long serialVersionUID = 6669934435517244629L;
MapWithAINameTableCellRenderer(boolean showActive) {
super(info -> info == null ? null : info.getName(), i -> null, MapWithAIInfo::getToolTipText, null,
showActive);
}
}
/**
* Constructs a new {@code MapWithAIProvidersPanel}.
*
* @param gui The parent preference tab pane
* @param options The options for this instance
*/
public MapWithAIProvidersPanel(final JComponent gui, Options... options) {
super(new GridBagLayout());
this.gui = gui;
this.options = options;
boolean showActive = Arrays.asList(options).contains(Options.SHOW_ACTIVE);
activeTable = new JTable(ACTIVE_MODEL) {
private static final long serialVersionUID = -6136421378119093719L;
@Override
public String getToolTipText(MouseEvent e) {
java.awt.Point p = e.getPoint();
try {
return ACTIVE_MODEL.getValueAt(rowAtPoint(p), columnAtPoint(p)).toString();
} catch (ArrayIndexOutOfBoundsException ex) {
Logging.debug(ex);
return null;
}
}
};
activeTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
defaultTable = new JTable(DEFAULT_MODEL);
defaultTable.setAutoCreateRowSorter(true);
defaultFilter = new FilterField().filter(defaultTable, DEFAULT_MODEL);
DEFAULT_MODEL.addTableModelListener(e -> activeTable.repaint());
ACTIVE_MODEL.addTableModelListener(e -> defaultTable.repaint());
setupDefaultTable(defaultTable, options, areaListeners);
TableColumnModel mod = activeTable.getColumnModel();
mod.getColumn(1).setPreferredWidth(800);
MapWithAIURLTableCellRenderer activeTableCellRenderer = new MapWithAIURLTableCellRenderer();
areaListeners.addListener(activeTableCellRenderer);
mod.getColumn(1).setCellRenderer(activeTableCellRenderer);
mod.getColumn(0).setMaxWidth(200);
RemoveEntryAction remove = new RemoveEntryAction();
activeTable.getSelectionModel().addListSelectionListener(remove);
EditEntryAction edit = new EditEntryAction();
activeTable.getSelectionModel().addListSelectionListener(edit);
add(new JLabel(tr("Available default entries:")), GBC.std().insets(5, 5, 0, 0));
add(new JLabel(tr("Boundaries of selected MapWithAI entries:")), GBC.eol().insets(5, 5, 0, 0));
// Add default item list
JPanel defaultPane = new JPanel(new GridBagLayout());
JScrollPane scrolldef = new JScrollPane(defaultTable);
scrolldef.setPreferredSize(new Dimension(200, 200));
defaultPane.add(defaultFilter, GBC.eol().insets(0, 0, 0, 0).fill(GridBagConstraints.HORIZONTAL));
defaultPane.add(scrolldef, GBC.eol().insets(0, 0, 0, 0).fill(GridBagConstraints.BOTH));
add(defaultPane, GBC.std().fill(GridBagConstraints.BOTH).weight(1.0, 0.6).insets(5, 0, 0, 0));
// Add default item map
defaultMap = new SlippyMapBBoxChooser();
defaultMap.setTileSource(SlippyMapBBoxChooser.DefaultOsmTileSourceProvider.get()); // for attribution
defaultMap.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
defaultMap.getAttribution().handleAttribution(e.getPoint(), true);
}
}
});
defaultMap.setZoomControlsVisible(false);
defaultMap.setMinimumSize(new Dimension(100, 200));
defaultMap.addJMVListener(e -> {
Rectangle2D visibleRect = defaultMap.getVisibleRect();
ICoordinate max = defaultMap.getPosition((int) visibleRect.getMaxX(), (int) visibleRect.getMaxY());
ICoordinate min = defaultMap.getPosition((int) visibleRect.getMinX(), (int) visibleRect.getMinY());
Bounds b = new Bounds(
new LatLon(Math.min(max.getLat(), min.getLat()),
LatLon.toIntervalLon(Math.min(max.getLon(), min.getLon()))),
new LatLon(Math.max(max.getLat(), min.getLat()),
LatLon.toIntervalLon(Math.max(max.getLon(), min.getLon()))));
this.areaListeners.fireEvent(f -> f.updateArea(b));
// This is required to ensure that all cells are appropriately coloured
// Both revalidate and repaint are needed.
this.defaultTable.revalidate();
this.defaultTable.repaint();
});
add(defaultMap, GBC.std().fill(GridBagConstraints.BOTH).weight(0.33, 0.6).insets(5, 0, 0, 0));
defaultTableListener = new DefListSelectionListener();
defaultTable.getSelectionModel().addListSelectionListener(defaultTableListener);
defaultToolbar = new JToolBar(JToolBar.VERTICAL);
defaultToolbar.setFloatable(false);
defaultToolbar.setBorderPainted(false);
defaultToolbar.setOpaque(false);
defaultToolbar.add(new ReloadAction());
add(defaultToolbar, GBC.eol().anchor(GBC.SOUTH).insets(0, 0, 5, 0));
HtmlPanel help = new HtmlPanel(
tr("New default entries can be added in the <a href=\"{0}\">GitHub Repository</a>.",
"https://github.com/JOSM/MapWithAI/blob/pages/json/sources.json"));
help.enableClickableHyperlinks();
add(help, GBC.eol().insets(10, 0, 0, 0).fill(GBC.HORIZONTAL));
ActivateAction activate = new ActivateAction();
defaultTable.getSelectionModel().addListSelectionListener(activate);
JButton btnActivate = new JButton(activate);
middleToolbar = new JToolBar(JToolBar.HORIZONTAL);
middleToolbar.setFloatable(false);
middleToolbar.setBorderPainted(false);
middleToolbar.setOpaque(false);
middleToolbar.add(btnActivate);
add(middleToolbar, GBC.eol().anchor(GBC.CENTER).insets(5, 5, 5, 0));
add(Box.createHorizontalGlue(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
JScrollPane scroll = new JScrollPane(activeTable);
scroll.setPreferredSize(new Dimension(200, 200));
activeToolbar = new JToolBar(JToolBar.VERTICAL);
activeToolbar.setFloatable(false);
activeToolbar.setBorderPainted(false);
activeToolbar.setOpaque(false);
activeToolbar.add(new NewEntryAction(MapWithAIType.THIRD_PARTY));
activeToolbar.add(edit);
activeToolbar.add(remove);
if (showActive) {
add(new JLabel(tr("Selected entries:")), GBC.eol().insets(5, 0, 0, 0));
add(scroll, GBC.std().fill(GridBagConstraints.BOTH).span(GridBagConstraints.RELATIVE).weight(1.0, 0.4)
.insets(5, 0, 0, 5));
add(activeToolbar, GBC.eol().anchor(GBC.NORTH).insets(0, 0, 5, 5));
}
}
private static void setupDefaultTable(JTable defaultTable, Options[] options,
ListenerList<AreaListener> areaListeners) {
boolean showActive = Arrays.asList(options).contains(Options.SHOW_ACTIVE);
int tenXWidth = defaultTable.getFontMetrics(defaultTable.getFont()).stringWidth("XXXXXXXXXX");
TableColumnModel mod = defaultTable.getColumnModel();
int urlWidth = (showActive ? 3 : 0) * tenXWidth;
mod.getColumn(6).setCellRenderer(defaultTable.getDefaultRenderer(Boolean.class));
mod.getColumn(6).setMaxWidth(20);
mod.getColumn(5).setMaxWidth((!showActive ? 1 : 0) * tenXWidth);
mod.getColumn(5).setCellRenderer(new MapWithAILicenseTableCellRenderer());
mod.getColumn(4).setPreferredWidth((showActive ? 2 : 0) * tenXWidth);
mod.getColumn(4).setCellRenderer(new MapWithAIProviderTableCellRenderer());
mod.getColumn(3).setPreferredWidth(urlWidth);
MapWithAIURLTableCellRenderer defaultUrlTableCellRenderer = new MapWithAIURLTableCellRenderer();
mod.getColumn(3).setCellRenderer(defaultUrlTableCellRenderer);
mod.getColumn(2).setPreferredWidth((int) ((showActive ? 0 : 0.3) * tenXWidth));
mod.getColumn(2).setCellRenderer(new MapWithAITypeTableCellRenderer());
MapWithAINameTableCellRenderer defaultNameTableCellRenderer = new MapWithAINameTableCellRenderer(!showActive);
mod.getColumn(1).setCellRenderer(defaultNameTableCellRenderer);
mod.getColumn(1).setPreferredWidth((showActive ? 3 : 2) * tenXWidth);
mod.getColumn(0).setCellRenderer(new MapWithAICategoryTableCellRenderer());
mod.getColumn(0).setMaxWidth(ImageProvider.ImageSizes.MENU.getAdjustedWidth() + 5);
if (showActive) {
defaultTable.removeColumn(mod.getColumn(6));
defaultTable.removeColumn(mod.getColumn(4));
defaultTable.removeColumn(mod.getColumn(2));
areaListeners.addListener(defaultUrlTableCellRenderer);
} else {
defaultTable.removeColumn(mod.getColumn(3));
areaListeners.addListener(defaultNameTableCellRenderer);
}
defaultTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
clickListener(e);
}
});
}
/**
* This is a function to be called when a cell is clicked
*
* @param e The MouseEvent (used to get the appropriate JTable)
*/
private static void clickListener(MouseEvent e) {
if (e.getSource() instanceof JTable) {
JTable table = (JTable) e.getSource();
if (table.getSelectedRow() >= 0 && table.getSelectedColumn() >= 0) {
int realCol = table.convertColumnIndexToModel(table.getSelectedColumn());
int realRow = table.convertRowIndexToModel(table.getSelectedRow());
String tableName = table.getModel().getColumnName(realCol);
if (tr("License").equals(tableName)) {
MapWithAIInfo info = MapWithAIDefaultLayerTableModel.getRow(realRow);
if (info.getTermsOfUseURL() != null) {
OpenBrowser.displayUrl(info.getTermsOfUseURL());
}
} else if (tr("Enabled").equals(tableName)) {
MapWithAIInfo info = MapWithAIDefaultLayerTableModel.getRow(realRow);
MapWithAILayerInfo instance = MapWithAILayerInfo.getInstance();
if (instance.getLayers().contains(info)) {
instance.remove(info);
} else {
instance.add(info);
}
}
}
}
}
/**
* Set the current bounds of the map and the area to select
*
* @param area The current area to highlight data from
*/
public void setCurrentBounds(Bounds area) {
this.defaultMap.setBoundingBox(area);
fireAreaListeners();
}
/**
* Fire area listeners
*/
public void fireAreaListeners() {
this.areaListeners.fireEvent(f -> f.updateArea(this.defaultMap.getBoundingBox()));
}
// Listener of default providers list selection
private final class DefListSelectionListener implements ListSelectionListener {
// The current drawn rectangles and polygons
private final Map<Integer, MapRectangle> mapRectangles;
private final Map<Integer, List<MapPolygon>> mapPolygons;
private DefListSelectionListener() {
this.mapRectangles = new HashMap<>();
this.mapPolygons = new HashMap<>();
}
private void clearMap() {
defaultMap.removeAllMapRectangles();
defaultMap.removeAllMapPolygons();
mapRectangles.clear();
mapPolygons.clear();
}
@Override
public void valueChanged(ListSelectionEvent e) {
// First index can be set to -1 when the list is refreshed, so discard all map
// rectangles and polygons
if (e.getFirstIndex() == -1) {
clearMap();
} else if (!e.getValueIsAdjusting()) {
// Only process complete (final) selection events
for (int i = e.getFirstIndex(); i <= e.getLastIndex(); i++) {
if (i < defaultTable.getRowCount()) {
updateBoundsAndShapes(defaultTable.convertRowIndexToModel(i));
}
}
// Cleanup residual selected bounds which may have disappeared after a filter
cleanupResidualBounds();
// If needed, adjust map to show all map rectangles and polygons
if (!mapRectangles.isEmpty() || !mapPolygons.isEmpty()) {
defaultMap.setDisplayToFitMapElements(false, true, true);
defaultMap.zoomOut();
}
}
}
/**
* update bounds and shapes for a new entry
*
* @param i model index
*/
private void updateBoundsAndShapes(int i) {
ImageryBounds bounds = MapWithAIDefaultLayerTableModel.getRow(i).getBounds();
if (bounds != null) {
int viewIndex = defaultTable.convertRowIndexToView(i);
List<Shape> shapes = bounds.getShapes();
if (shapes != null && !shapes.isEmpty()) {
if (defaultTable.getSelectionModel().isSelectedIndex(viewIndex)) {
mapPolygons.computeIfAbsent(i, key -> {
List<MapPolygon> list = new ArrayList<>();
// Add new map polygons
for (Shape shape : shapes) {
MapPolygon polygon = new MapPolygonImpl(shape.getPoints());
list.add(polygon);
defaultMap.addMapPolygon(polygon);
}
return list;
});
} else if (mapPolygons.containsKey(i)) {
// Remove previously drawn map polygons
for (MapPolygon polygon : mapPolygons.get(i)) {
defaultMap.removeMapPolygon(polygon);
}
mapPolygons.remove(i);
}
// Only display bounds when no polygons (shapes) are defined for this provider
} else {
if (defaultTable.getSelectionModel().isSelectedIndex(viewIndex)) {
mapRectangles.computeIfAbsent(i, key -> {
// Add new map rectangle
Coordinate topLeft = new Coordinate(bounds.getMaxLat(), bounds.getMinLon());
Coordinate bottomRight = new Coordinate(bounds.getMinLat(), bounds.getMaxLon());
MapRectangle rectangle = new MapRectangleImpl(topLeft, bottomRight);
defaultMap.addMapRectangle(rectangle);
return rectangle;
});
} else if (mapRectangles.containsKey(i)) {
// Remove previously drawn map rectangle
defaultMap.removeMapRectangle(mapRectangles.get(i));
mapRectangles.remove(i);
}
}
}
}
private <T> void doCleanupResidualBounds(Map<Integer, T> map, Consumer<T> removalEffect) {
List<Integer> toRemove = new ArrayList<>();
for (Integer i : map.keySet()) {
int viewIndex = defaultTable.convertRowIndexToView(i);
if (!defaultTable.getSelectionModel().isSelectedIndex(viewIndex)) {
toRemove.add(i);
}
}
toRemove.forEach(i -> removalEffect.accept(map.remove(i)));
}
private void cleanupResidualBounds() {
doCleanupResidualBounds(mapPolygons, l -> l.forEach(defaultMap::removeMapPolygon));
doCleanupResidualBounds(mapRectangles, defaultMap::removeMapRectangle);
}
}
private class NewEntryAction extends AbstractAction {
private static final long serialVersionUID = 7451336680150337942L;
NewEntryAction(MapWithAIType type) {
putValue(NAME, type.toString());
putValue(SHORT_DESCRIPTION, tr("Add a new {0} entry by entering the URL", type.toString()));
String icon = /* ICON(dialogs/) */ "add";
new ImageProvider(DIALOG_IMAGES_DIR, icon).getResource().attachImageIcon(this, true);
}
@Override
public void actionPerformed(ActionEvent evt) {
final AddMapWithAIPanel p = new AddMapWithAIPanel();
final AddMapWithAIDialog addDialog = new AddMapWithAIDialog(gui, p);
addDialog.showDialog();
if (addDialog.getValue() == 1) {
try {
MapWithAIInfo info = p.getSourceInfo();
// Fix a possible NPE
if (info.getSourceType() == null) {
info.setSourceType(MapWithAIType.THIRD_PARTY);
}
if (MapWithAIType.ESRI == info.getSourceType()) {
final ESRISourceReader reader = new ESRISourceReader(info);
try {
for (Future<MapWithAIInfo> i : reader.parse()) {
try {
ACTIVE_MODEL.addRow(i.get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Logging.error(e);
} catch (ExecutionException e) {
Logging.error(e);
}
}
} catch (IOException e) {
Logging.error(e);
}
} else {
ACTIVE_MODEL.addRow(info);
}
} catch (IllegalArgumentException ex) {
if (ex.getMessage() == null || ex.getMessage().isEmpty()) {
throw ex;
}
JOptionPane.showMessageDialog(MainApplication.getMainFrame(), ex.getMessage(), tr("Error"),
JOptionPane.ERROR_MESSAGE);
}
}
}
}
private class EditEntryAction extends AbstractAction implements ListSelectionListener {
private static final long serialVersionUID = -1682304557691078801L;
/**
* Constructs a new {@code EditEntryAction}.
*/
EditEntryAction() {
putValue(NAME, tr("Edit"));
putValue(SHORT_DESCRIPTION, tr("Edit entry"));
new ImageProvider(DIALOG_IMAGES_DIR, "edit").getResource().attachImageIcon(this, true);
updateEnabledState();
}
protected final void updateEnabledState() {
setEnabled(activeTable.getSelectedRowCount() > 0);
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
@Override
public void actionPerformed(ActionEvent e) {
if (activeTable.getSelectedRow() != -1) {
final AddMapWithAIPanel p = new AddMapWithAIPanel(
MapWithAILayerTableModel.getRow(activeTable.getSelectedRow()));
final AddMapWithAIDialog addDialog = new AddMapWithAIDialog(gui, p);
addDialog.showDialog();
if (addDialog.getValue() == 1) {
p.getSourceInfo();
}
}
}
}
private class RemoveEntryAction extends AbstractAction implements ListSelectionListener {
private static final long serialVersionUID = 2666450386256004180L;
/**
* Constructs a new {@code RemoveEntryAction}.
*/
RemoveEntryAction() {
putValue(NAME, tr("Remove"));
putValue(SHORT_DESCRIPTION, tr("Remove entry"));
new ImageProvider(DIALOG_IMAGES_DIR, "delete").getResource().attachImageIcon(this, true);
updateEnabledState();
}
protected final void updateEnabledState() {
setEnabled(activeTable.getSelectedRowCount() > 0);
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
@Override
public void actionPerformed(ActionEvent e) {
int i;
while ((i = activeTable.getSelectedRow()) != -1) {
ACTIVE_MODEL.removeRow(i);
}
}
}
private class ActivateAction extends AbstractAction implements ListSelectionListener, LayerChangeListener {
private static final long serialVersionUID = -452335751201424801L;
private final transient ImageResource activate;
private final transient ImageResource deactivate;
/**
* Constructs a new {@code ActivateAction}.
*/
ActivateAction() {
putValue(NAME, tr("Activate"));
putValue(SHORT_DESCRIPTION, tr("Copy selected default entries from the list above into the list below."));
activate = new ImageProvider("svpDown").setMaxSize(ImageProvider.ImageSizes.MENU).getResource();
activate.attachImageIcon(this, true);
deactivate = new ImageProvider("svpUp").setMaxSize(ImageProvider.ImageSizes.MENU).getResource();
MapWithAILayerInfo.getInstance().addListener(this);
}
protected void updateEnabledState() {
setEnabled(defaultTable.getSelectedRowCount() > 0);
List<MapWithAIInfo> selected = Arrays.stream(defaultTable.getSelectedRows())
.map(defaultTable::convertRowIndexToModel).mapToObj(MapWithAIDefaultLayerTableModel::getRow)
.collect(Collectors.toList());
if (selected.stream().anyMatch(MapWithAILayerTableModel::doesNotContain)) {
activate.attachImageIcon(this, true);
putValue(NAME, tr("Activate"));
putValue(SHORT_DESCRIPTION,
tr("Copy selected default entries from the list above into the list below."));
} else {
deactivate.attachImageIcon(this, true);
putValue(NAME, tr("Deactivate"));
putValue(SHORT_DESCRIPTION,
tr("Remove selected default entries from the list above into the list below."));
}
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
@Override
public void actionPerformed(ActionEvent e) {
int[] lines = defaultTable.getSelectedRows();
if (lines.length == 0) {
JOptionPane.showMessageDialog(gui, tr("Please select at least one row to copy."), tr("Information"),
JOptionPane.INFORMATION_MESSAGE);
return;
}
List<MapWithAIInfo> selected = Arrays.stream(defaultTable.getSelectedRows())
.map(defaultTable::convertRowIndexToModel).mapToObj(MapWithAIDefaultLayerTableModel::getRow)
.collect(Collectors.toList());
if (selected.stream().anyMatch(MapWithAILayerTableModel::doesNotContain)) {
List<MapWithAIInfo> toAdd = selected.stream().filter(MapWithAILayerTableModel::doesNotContain)
.collect(Collectors.toList());
activeTable.getSelectionModel().clearSelection();
for (MapWithAIInfo info : toAdd) {
ACTIVE_MODEL.addRow(new MapWithAIInfo(info));
int lastLine = ACTIVE_MODEL.getRowCount() - 1;
activeTable.getSelectionModel().setSelectionInterval(lastLine, lastLine);
activeTable.scrollRectToVisible(activeTable.getCellRect(lastLine, 0, true));
}
selected.removeIf(toAdd::contains);
selected.stream().mapToInt(ACTIVE_MODEL::getRowIndex).filter(i -> i >= 0).forEach(j -> {
activeTable.getSelectionModel().addSelectionInterval(j, j);
activeTable.scrollRectToVisible(activeTable.getCellRect(j, 0, true));
});
} else {
selected.stream().mapToInt(ACTIVE_MODEL::getRowIndex).filter(i -> i >= 0).boxed()
.sorted(Collections.reverseOrder()).forEach(ACTIVE_MODEL::removeRow);
}
updateEnabledState();
if (Stream.of(options).noneMatch(Options.SHOW_ACTIVE::equals)) {
MapWithAILayerInfo.getInstance().save();
}
}
@Override
public void changeEvent(MapWithAIInfo modified) {
GuiHelper.runInEDT(this::updateEnabledState);
}
}
private class ReloadAction extends AbstractAction {
private static final long serialVersionUID = 7801339998423585685L;
/**
* Constructs a new {@code ReloadAction}.
*/
ReloadAction() {
putValue(SHORT_DESCRIPTION, tr("Update default entries"));
new ImageProvider(DIALOG_IMAGES_DIR, "refresh").getResource().attachImageIcon(this, true);
}
@Override
public void actionPerformed(ActionEvent evt) {
this.setEnabled(false);
MapWithAILayerInfo.getInstance().loadDefaults(true, MapWithAIDataUtils.getForkJoinPool(), false, () ->
// This needs to be run in a block to avoid race conditions.
GuiHelper.runInEDT(() -> {
defaultTable.getSelectionModel().clearSelection();
defaultTableListener.clearMap();
DEFAULT_MODEL.fireTableDataChanged();
/* loading new file may change active layers */
ACTIVE_MODEL.fireTableDataChanged();
this.setEnabled(true);
}));
}
}
}