MapWithAI/src/main/java/org/openstreetmap/josm/plugins/mapwithai/backend/MapWithAILayer.java

447 wiersze
16 KiB
Java

// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.mapwithai.backend;
import static org.openstreetmap.josm.tools.I18n.tr;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.io.Serial;
import java.io.Serializable;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.TreeSet;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import org.openstreetmap.josm.actions.ExpertToggleAction;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.DataSource;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.DownloadPolicy;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.UploadPolicy;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.gui.Notification;
import org.openstreetmap.josm.gui.dialogs.layer.DuplicateAction;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
import org.openstreetmap.josm.gui.mappaint.StyleSource;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.gui.widgets.HtmlPanel;
import org.openstreetmap.josm.plugins.mapwithai.MapWithAIPlugin;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAIInfo;
import org.openstreetmap.josm.plugins.mapwithai.tools.BlacklistUtils;
import org.openstreetmap.josm.plugins.mapwithai.tools.MapPaintUtils;
import org.openstreetmap.josm.spi.preferences.Config;
import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.Utils;
/**
* This layer shows MapWithAI data. For various reasons, we currently only allow
* one to be created, although this may change.
*
* @author Taylor Smock
*
*/
public class MapWithAILayer extends OsmDataLayer implements ActiveLayerChangeListener, PreferenceChangedListener {
private static final Collection<String> COMPACT = Collections.singleton("esri");
private Integer maximumAddition;
private MapWithAIInfo url;
private Boolean switchLayers;
private boolean continuousDownload = true;
private final Lock lock;
private final HashSet<MapWithAIInfo> downloadedInfo = new HashSet<>();
/**
* Create a new MapWithAI layer
*
* @param data OSM data from MapWithAI
* @param name Layer name
* @param associatedFile an associated file (can be null)
*/
public MapWithAILayer(DataSet data, String name, File associatedFile) {
super(data, name, associatedFile);
data.setUploadPolicy(UploadPolicy.BLOCKED);
data.setDownloadPolicy(DownloadPolicy.BLOCKED);
lock = new MapLock();
MainApplication.getLayerManager().addActiveLayerChangeListener(this);
new ContinuousDownloadAction(this); // Initialize data source listeners
Config.getPref().addKeyPreferenceChangeListener("download.mapwithai.data", this);
}
@Override
public String getChangesetSourceTag() {
if (MapWithAIDataUtils.getAddedObjects() > 0) {
TreeSet<String> sources = MapWithAIDataUtils.getAddedObjectsSource().stream().filter(Objects::nonNull)
.map(string -> COMPACT.stream().filter(string::contains).findAny().orElse(string))
.collect(Collectors.toCollection(TreeSet::new));
sources.add("MapWithAI");
return String.join("; ", sources);
}
return null;
}
public void setMaximumAddition(Integer max) {
maximumAddition = max;
}
public Integer getMaximumAddition() {
return maximumAddition;
}
public void setMapWithAIUrl(MapWithAIInfo info) {
this.url = info;
}
public MapWithAIInfo getMapWithAIUrl() {
return url;
}
public void setSwitchLayers(boolean selected) {
switchLayers = selected;
}
public Boolean isSwitchLayers() {
return switchLayers;
}
@Override
public Object getInfoComponent() {
final Object p = super.getInfoComponent();
if (p instanceof JPanel panel) {
if (maximumAddition != null) {
panel.add(new JLabel(tr("Maximum Additions: {0}", maximumAddition), SwingConstants.CENTER),
GBC.eop().insets(15, 0, 0, 0));
}
if (url != null) {
panel.add(new JLabel(tr("URL: {0}", url.getUrlExpanded()), SwingConstants.CENTER),
GBC.eop().insets(15, 0, 0, 0));
}
if (switchLayers != null) {
panel.add(new JLabel(tr("Switch Layers: {0}", switchLayers), SwingConstants.CENTER),
GBC.eop().insets(15, 0, 0, 0));
}
}
return p;
}
@Override
public Action[] getMenuEntries() {
Collection<Class<? extends Action>> forbiddenActions = Arrays.asList(LayerSaveAction.class,
LayerSaveAsAction.class, DuplicateAction.class, LayerGpxExportAction.class,
ConvertToGpxLayerAction.class);
final List<Action> actions = Arrays.stream(super.getMenuEntries())
.filter(action -> forbiddenActions.stream().noneMatch(clazz -> clazz.isInstance(action)))
.collect(Collectors.toCollection(ArrayList::new));
if (actions.isEmpty()) {
actions.add(new ContinuousDownloadAction(this));
} else {
actions.add(actions.size() - 2, new ContinuousDownloadAction(this));
}
return actions.toArray(new Action[0]);
}
public Lock getLock() {
return lock;
}
@Override
public void preferenceChanged(PreferenceChangeEvent e) {
if ("download.mapwithai.data".equals(e.getKey())) {
final Object value = e.getNewValue().getValue();
if (value instanceof Boolean bool) {
this.continuousDownload = bool;
} else if (value instanceof String str) {
this.continuousDownload = Boolean.parseBoolean(str);
} else {
this.continuousDownload = false;
}
}
}
private class MapLock extends ReentrantLock {
@Serial
private static final long serialVersionUID = 5441350396443132682L;
private boolean dataSetLocked;
public MapLock() {
super();
// Do nothing
}
@Override
public void lock() {
super.lock();
dataSetLocked = getDataSet().isLocked();
if (dataSetLocked) {
getDataSet().unlock();
}
}
@Override
public void unlock() {
super.unlock();
if (dataSetLocked) {
getDataSet().lock();
}
}
}
@Override
public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
if (checkIfToggleLayer()) {
final StyleSource style = MapPaintUtils.getMapWithAIPaintStyle();
if (style.active != this.equals(MainApplication.getLayerManager().getActiveLayer())) {
MapPaintStyles.toggleStyleActive(MapPaintStyles.getStyles().getStyleSources().indexOf(style));
}
}
}
private static boolean checkIfToggleLayer() {
final List<String> keys = Config.getPref().getKeySet().parallelStream()
.filter(string -> string.contains(MapWithAIPlugin.NAME) && string.contains("boolean:toggle_with_layer"))
.toList();
boolean toggle = false;
if (keys.size() == 1) {
toggle = Config.getPref().getBoolean(keys.get(0), false);
}
return toggle;
}
@Override
public synchronized void destroy() {
Config.getPref().removeKeyPreferenceChangeListener("download.mapwithai.data", this);
super.destroy();
MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
}
@Override
public Icon getIcon() {
return ImageProvider.getIfAvailable("mapwithai") == null ? super.getIcon()
: ImageProvider.get("mapwithai", ImageProvider.ImageSizes.LAYER);
}
/**
* Call after download from server
*
* @param bounds The newly added bounds
*/
public void onPostDownloadFromServer(Bounds bounds) {
super.onPostDownloadFromServer();
GetDataRunnable.cleanup(getDataSet(), bounds, null);
if (!this.data.getDataSourceBounds().contains(bounds)) {
this.data.addDataSource(new DataSource(bounds, null));
}
}
@Override
public void selectionChanged(SelectionChangeEvent event) {
if (BlacklistUtils.isBlacklisted()) {
if (!event.getSelection().isEmpty()) {
GuiHelper.runInEDT(() -> getDataSet().setSelected(Collections.emptySet()));
createBadDataNotification();
}
return;
}
super.selectionChanged(event);
final int maximumAdditionSelection = MapWithAIPreferenceHelper.getMaximumAddition();
if ((event.getSelection().size() - event.getOldSelection().size() > 1
|| maximumAdditionSelection < event.getSelection().size())
&& (MapWithAIPreferenceHelper.getMaximumAddition() != 0 || !ExpertToggleAction.isExpert())) {
Collection<OsmPrimitive> selection;
final Collection<Way> oldWays = Utils.filteredCollection(event.getOldSelection(), Way.class);
if (Utils.filteredCollection(event.getSelection(), Node.class).stream()
.filter(n -> !event.getOldSelection().contains(n))
.allMatch(n -> oldWays.stream().anyMatch(w -> w.containsNode(n)))) {
selection = event.getSelection();
} else {
OsmComparator comparator = new OsmComparator(event.getOldSelection());
selection = event.getSelection().stream().distinct().sorted(comparator).limit(maximumAdditionSelection)
.limit(event.getOldSelection().size() + Math.max(1L, maximumAdditionSelection / 10L))
.collect(Collectors.toList());
}
GuiHelper.runInEDT(() -> getDataSet().setSelected(selection));
}
}
/**
* Create a notification for plugin versions that create bad data.
*/
public static void createBadDataNotification() {
Notification badData = new Notification();
badData.setIcon(JOptionPane.ERROR_MESSAGE);
badData.setDuration(Notification.TIME_LONG);
HtmlPanel panel = new HtmlPanel();
StringBuilder message = new StringBuilder()
.append(tr("This version of the MapWithAI plugin is known to create bad data.")).append("<br />")
.append(tr("Please update plugins and/or JOSM."));
if (BlacklistUtils.isOffline()) {
message.append("<br />").append(tr("This message may occur when JOSM is offline."));
}
panel.setText(message.toString());
badData.setContent(panel);
MapWithAILayer layer = MapWithAIDataUtils.getLayer(false);
if (layer != null) {
layer.setMaximumAddition(0);
}
GuiHelper.runInEDT(badData::show);
}
/**
* Compare OsmPrimitives in a custom manner
*/
private record OsmComparator(
Collection<OsmPrimitive> previousSelection) implements Comparator<OsmPrimitive>, Serializable {
@Override
public int compare(OsmPrimitive o1, OsmPrimitive o2) {
if (previousSelection.contains(o1) == previousSelection.contains(o2)) {
if (o1.isTagged() == o2.isTagged()) {
return o1.compareTo(o2);
} else if (o1.isTagged()) {
return -1;
}
return 1;
}
if (previousSelection.contains(o1)) {
return -1;
}
return 1;
}
}
/**
* Check if we want to download data continuously
*
* @return {@code true} indicates that we should attempt to keep it in sync with
* the data layer(s)
*/
public boolean downloadContinuous() {
return continuousDownload;
}
/**
* Allow continuous download of data (for the layer that MapWithAI is clamped
* to).
*
* @author Taylor Smock
*/
public static class ContinuousDownloadAction extends AbstractAction implements LayerAction {
@Serial
private static final long serialVersionUID = -3528632887550700527L;
private final transient MapWithAILayer layer;
/**
* Create a new continuous download toggle
*
* @param layer the layer to toggle continuous download for
*/
public ContinuousDownloadAction(MapWithAILayer layer) {
super(tr("Continuous download"));
new ImageProvider("download").getResource().attachImageIcon(this, true);
this.layer = layer;
updateListeners();
}
@Override
public void actionPerformed(ActionEvent e) {
layer.continuousDownload = !layer.continuousDownload;
updateListeners();
}
void updateListeners() {
if (layer.continuousDownload) {
for (OsmDataLayer data : MainApplication.getLayerManager().getLayersOfType(OsmDataLayer.class)) {
if (!(data instanceof MapWithAILayer)) {
new DownloadListener(data.getDataSet());
}
}
} else {
DownloadListener.destroyAll();
}
}
@Override
public boolean supportLayers(List<Layer> layers) {
return layers.stream().allMatch(MapWithAILayer.class::isInstance);
}
@Override
public Component createMenuComponent() {
JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
item.setSelected(layer.continuousDownload);
return item;
}
}
/**
* Check if the layer has downloaded a specific data type
*
* @param info The info to check
* @return {@code true} if the info has been added to the layer
*/
public boolean hasDownloaded(MapWithAIInfo info) {
return downloadedInfo.contains(info);
}
/**
* Indicate an info has been downloaded in this layer
*
* @param info The info that has been downloaded
*/
public void addDownloadedInfo(MapWithAIInfo info) {
downloadedInfo.add(info);
}
/**
* Get the info that has been downloaded into this layer
*
* @return An unmodifiable collection of the downloaded info
*/
public Collection<MapWithAIInfo> getDownloadedInfo() {
return Collections.unmodifiableCollection(downloadedInfo);
}
@Override
public boolean autosave(File file) throws IOException {
// Consider a deletion a "successful" save.
return Files.deleteIfExists(file.toPath());
}
@Override
public boolean isMergable(final Layer other) {
// Don't allow this layer to be merged down
return other instanceof MapWithAILayer;
}
}