Create a dynamic menu that shows sources in the current view

This fixes #88.

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
pull/1/head
Taylor Smock 2020-06-30 09:41:40 -06:00
rodzic 786ee5f689
commit 1414061c23
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 625F6A74A3E4311A
6 zmienionych plików z 380 dodań i 26 usunięć

Wyświetl plik

@ -1,9 +1,10 @@
// License: GPL. For details, see LICENSE file. // License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.mapwithai; package org.openstreetmap.josm.plugins.mapwithai;
import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
import static org.openstreetmap.josm.tools.I18n.tr; import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Component; import java.awt.event.KeyEvent;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -12,10 +13,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
import javax.swing.Action;
import javax.swing.JMenu;
import javax.swing.JMenuItem; import javax.swing.JMenuItem;
import org.openstreetmap.josm.actions.JosmAction; import org.openstreetmap.josm.actions.JosmAction;
@ -46,6 +44,7 @@ import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.RoutingIsl
import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.StreetAddressOrder; import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.StreetAddressOrder;
import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.StreetAddressTest; import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.StreetAddressTest;
import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.StubEndsTest; import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.StubEndsTest;
import org.openstreetmap.josm.plugins.mapwithai.gui.MapWithAIMenu;
import org.openstreetmap.josm.plugins.mapwithai.gui.download.MapWithAIDownloadOptions; import org.openstreetmap.josm.plugins.mapwithai.gui.download.MapWithAIDownloadOptions;
import org.openstreetmap.josm.plugins.mapwithai.gui.download.MapWithAIDownloadSourceType; import org.openstreetmap.josm.plugins.mapwithai.gui.download.MapWithAIDownloadSourceType;
import org.openstreetmap.josm.plugins.mapwithai.gui.preferences.MapWithAIPreferences; import org.openstreetmap.josm.plugins.mapwithai.gui.preferences.MapWithAIPreferences;
@ -66,6 +65,8 @@ public final class MapWithAIPlugin extends Plugin implements Destroyable {
private PreferencesAction preferenceAction; private PreferencesAction preferenceAction;
private MapWithAIMenu mapwithaiMenu;
private static final Map<Class<? extends JosmAction>, Boolean> MENU_ENTRIES = new LinkedHashMap<>(); private static final Map<Class<? extends JosmAction>, Boolean> MENU_ENTRIES = new LinkedHashMap<>();
static { static {
MENU_ENTRIES.put(MapWithAIAction.class, false); MENU_ENTRIES.put(MapWithAIAction.class, false);
@ -81,13 +82,17 @@ public final class MapWithAIPlugin extends Plugin implements Destroyable {
preferenceSetting = new MapWithAIPreferences(); preferenceSetting = new MapWithAIPreferences();
final JMenu dataMenu = MainApplication.getMenu().dataMenu; // Add MapWithAI specific menu
mapwithaiMenu = new MapWithAIMenu();
MainApplication.getMenu().addMenu(mapwithaiMenu, "mapwithai:menu", KeyEvent.VK_M, 9, ht("/Plugin/MapWithAI"));
for (final Entry<Class<? extends JosmAction>, Boolean> entry : MENU_ENTRIES.entrySet()) { for (final Entry<Class<? extends JosmAction>, Boolean> entry : MENU_ENTRIES.entrySet()) {
if (Arrays.asList(dataMenu.getMenuComponents()).parallelStream().filter(JMenuItem.class::isInstance) if (Arrays.asList(mapwithaiMenu.getMenuComponents()).parallelStream().filter(JMenuItem.class::isInstance)
.map(JMenuItem.class::cast) .map(JMenuItem.class::cast)
.noneMatch(component -> entry.getKey().equals(component.getAction().getClass()))) { .noneMatch(component -> entry.getKey().equals(component.getAction().getClass()))) {
try { try {
MainMenu.add(dataMenu, entry.getKey().getDeclaredConstructor().newInstance(), entry.getValue()); MainMenu.add(mapwithaiMenu, entry.getKey().getDeclaredConstructor().newInstance(),
entry.getValue());
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) { | InvocationTargetException | NoSuchMethodException | SecurityException e) {
Logging.debug(e); Logging.debug(e);
@ -98,7 +103,7 @@ public final class MapWithAIPlugin extends Plugin implements Destroyable {
// Add the preferences last to data // Add the preferences last to data
preferenceAction = PreferencesAction.forPreferenceTab(tr("MapWithAI Preferences"), tr("MapWithAI Preferences"), preferenceAction = PreferencesAction.forPreferenceTab(tr("MapWithAI Preferences"), tr("MapWithAI Preferences"),
MapWithAIPreferences.class); MapWithAIPreferences.class);
MainMenu.add(dataMenu, preferenceAction); MainMenu.add(mapwithaiMenu, preferenceAction);
VALIDATORS.forEach(clazz -> { VALIDATORS.forEach(clazz -> {
if (!OsmValidator.getAllAvailableTestClasses().contains(clazz)) { if (!OsmValidator.getAllAvailableTestClasses().contains(clazz)) {
@ -166,18 +171,7 @@ public final class MapWithAIPlugin extends Plugin implements Destroyable {
*/ */
@Override @Override
public void destroy() { public void destroy() {
final JMenu dataMenu = MainApplication.getMenu().dataMenu; MainApplication.getMenu().remove(this.mapwithaiMenu);
final Map<Action, Component> actions = Arrays.asList(dataMenu.getMenuComponents()).stream()
.filter(JMenuItem.class::isInstance).map(JMenuItem.class::cast)
.collect(Collectors.toMap(JMenuItem::getAction, component -> component));
for (final Entry<Action, Component> action : actions.entrySet()) {
if (MENU_ENTRIES.containsKey(action.getKey().getClass())) {
dataMenu.remove(action.getValue());
} else if (action.getKey().equals(preferenceAction)) {
dataMenu.remove(action.getValue());
}
}
MainApplication.getLayerManager().getLayersOfType(MapWithAILayer.class).stream() MainApplication.getLayerManager().getLayersOfType(MapWithAILayer.class).stream()
.forEach(layer -> MainApplication.getLayerManager().removeLayer(layer)); .forEach(layer -> MainApplication.getLayerManager().removeLayer(layer));

Wyświetl plik

@ -0,0 +1,110 @@
// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.mapwithai.actions;
import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
import java.awt.event.ActionEvent;
import org.openstreetmap.josm.actions.AdaptableAction;
import org.openstreetmap.josm.actions.AddImageryLayerAction;
import org.openstreetmap.josm.actions.JosmAction;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.OsmData;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.io.OsmTransferException;
import org.openstreetmap.josm.plugins.mapwithai.backend.BoundingBoxMapWithAIDownloader;
import org.openstreetmap.josm.plugins.mapwithai.backend.MapWithAIDataUtils;
import org.openstreetmap.josm.plugins.mapwithai.backend.MapWithAILayer;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAIInfo;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
import org.openstreetmap.josm.tools.ImageResource;
import org.openstreetmap.josm.tools.Logging;
/**
* Action displayed in MapWithAI menu to add data to the MapWithAI layer.
* Largely copied from {@link AddImageryLayerAction}.
*/
public class AddMapWithAILayerAction extends JosmAction implements AdaptableAction {
private final transient MapWithAIInfo info;
/**
* Constructs a new {@code AddMapWithAILayerAction} for the given
* {@code MapWithAIInfo}. If an http:// icon is specified, it is fetched
* asynchronously.
*
* @param info The source info
*/
public AddMapWithAILayerAction(MapWithAIInfo info) {
super(info.getName(), /* ICON */"imagery_menu", info.getToolTipText(), null, true,
ToolbarPreferences.IMAGERY_PREFIX + info.getToolbarName(), false);
setHelpId(ht("/Preferences/Imagery"));
this.info = info;
installAdapters();
// change toolbar icon from if specified
String icon = info.getIcon();
if (icon != null) {
new ImageProvider(icon).setOptional(true).getResourceAsync(result -> {
if (result != null) {
GuiHelper.runInEDT(() -> result.attachImageIcon(this));
}
});
} else {
ImageResource resource = new ImageResource(
this.info.getSourceCategory().getIcon(ImageSizes.MENU).getImage());
resource.attachImageIcon(this);
}
}
@Override
public void actionPerformed(ActionEvent e) {
if (!isEnabled()) {
return;
}
MapWithAILayer layer = MapWithAIDataUtils.getLayer(false);
final DataSet ds;
final OsmData<?, ?, ?, ?> boundsSource;
if (layer != null && !layer.getData().getDataSourceBounds().isEmpty()) {
ds = layer.getDataSet();
boundsSource = ds;
} else if (MainApplication.getLayerManager().getActiveData() != null
&& !MainApplication.getLayerManager().getActiveData().getDataSourceBounds().isEmpty()) {
boundsSource = MainApplication.getLayerManager().getActiveData();
ds = MapWithAIDataUtils.getLayer(true).getDataSet();
} else {
boundsSource = null;
ds = null;
}
if (boundsSource != null && ds != null) {
boundsSource.getDataSourceBounds().forEach(b -> MainApplication.worker.execute(() -> {
try {
ds.mergeFrom(
new BoundingBoxMapWithAIDownloader(b, info, false).parseOsm(NullProgressMonitor.INSTANCE));
} catch (OsmTransferException error) {
Logging.error(error);
}
}));
}
}
@Override
protected boolean listenToSelectionChange() {
return false;
}
@Override
protected void updateEnabledState() {
setEnabled(!info.isBlacklisted());
}
@Override
public String toString() {
return "AddMapWithAILayerAction [info=" + info + ']';
}
}

Wyświetl plik

@ -34,7 +34,12 @@ import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAIType;
import org.openstreetmap.josm.tools.HttpClient; import org.openstreetmap.josm.tools.HttpClient;
import org.openstreetmap.josm.tools.Logging; import org.openstreetmap.josm.tools.Logging;
class BoundingBoxMapWithAIDownloader extends BoundingBoxDownloader { /**
* A bounding box downloader for MapWithAI
*
* @author Taylor Smock
*/
public class BoundingBoxMapWithAIDownloader extends BoundingBoxDownloader {
private final String url; private final String url;
private final boolean crop; private final boolean crop;
@ -46,6 +51,13 @@ class BoundingBoxMapWithAIDownloader extends BoundingBoxDownloader {
private static final int DEFAULT_TIMEOUT = 50_000; // 50 seconds private static final int DEFAULT_TIMEOUT = 50_000; // 50 seconds
/**
* Create a new {@link BoundingBoxMapWithAIDownloader} object
*
* @param downloadArea The area to download
* @param info The info to use to get the url to download
* @param crop Whether or not to crop the download area
*/
public BoundingBoxMapWithAIDownloader(Bounds downloadArea, MapWithAIInfo info, boolean crop) { public BoundingBoxMapWithAIDownloader(Bounds downloadArea, MapWithAIInfo info, boolean crop) {
super(downloadArea); super(downloadArea);
this.info = info; this.info = info;

Wyświetl plik

@ -536,4 +536,13 @@ public class MapWithAIInfo extends
} }
return Collections.unmodifiableList(this.conflationIgnoreCategory); return Collections.unmodifiableList(this.conflationIgnoreCategory);
} }
/**
* Get a string usable for toolbars
*
* @return Currently, the name of the source.
*/
public String getToolbarName() {
return this.getName();
}
} }

Wyświetl plik

@ -0,0 +1,229 @@
// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.mapwithai.gui;
import static org.openstreetmap.josm.tools.I18n.trc;
import java.awt.Component;
import java.awt.GraphicsEnvironment;
import java.awt.MenuComponent;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import javax.swing.Action;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
import org.openstreetmap.josm.actions.JosmAction;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.imagery.Shape;
import org.openstreetmap.josm.data.sources.SourceInfo;
import org.openstreetmap.josm.gui.ImageryMenu;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.gui.MenuScroller;
import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference;
import org.openstreetmap.josm.plugins.mapwithai.actions.AddMapWithAILayerAction;
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.tools.ImageProvider.ImageSizes;
import org.openstreetmap.josm.tools.Logging;
/**
* MapWithAI menu, holding entries for MapWithAI preferences and dynamic source
* entries depending on current mapview coordinates.
*
* Largely copied from {@link ImageryMenu}, but highly modified.
*/
public class MapWithAIMenu extends JMenu {
/**
* Compare MapWithAIInfo objects alphabetically by name.
*
* MapWithAIInfo objects are normally sorted by country code first (for the
* preferences). We don't want this in the MapWithAI menu.
*/
public static final Comparator<SourceInfo<?, ?, ?, ?>> alphabeticSourceComparator = (ii1, ii2) -> ii1.getName()
.toLowerCase(Locale.ENGLISH).compareTo(ii2.getName().toLowerCase(Locale.ENGLISH));
/**
* Constructs a new {@code ImageryMenu}.
*/
public MapWithAIMenu() {
/* I18N: mnemonic: I */
super(trc("menu", "MapWithAI"));
setupMenuScroller();
// build dynamically
addMenuListener(new MenuListener() {
@Override
public void menuSelected(MenuEvent e) {
refreshImageryMenu();
}
@Override
public void menuDeselected(MenuEvent e) {
// Do nothing
}
@Override
public void menuCanceled(MenuEvent e) {
// Do nothing
}
});
}
private void setupMenuScroller() {
if (!GraphicsEnvironment.isHeadless()) {
MenuScroller.setScrollerFor(this, 150, 2);
}
}
/**
* For layers containing complex shapes, check that center is in one of its
* shapes (fix #7910)
*
* @param info layer info
* @param pos center
* @return {@code true} if center is in one of info shapes
*/
private static boolean isPosInOneShapeIfAny(SourceInfo<?, ?, ?, ?> info, LatLon pos) {
List<Shape> shapes = info.getBounds().getShapes();
return shapes == null || shapes.isEmpty() || shapes.stream().anyMatch(s -> s.contains(pos));
}
/**
* Refresh imagery menu.
*
* Outside this class only called in {@link ImageryPreference#initialize()}. (In
* order to have actions ready for the toolbar, see #8446.)
*/
public void refreshImageryMenu() {
removeDynamicItems();
addDynamicSeparator();
// for each configured ImageryInfo, add a menu entry.
final List<MapWithAIInfo> savedLayers = new ArrayList<>(MapWithAILayerInfo.getInstance().getLayers());
savedLayers.sort(alphabeticSourceComparator);
for (final MapWithAIInfo u : savedLayers) {
addDynamic(trackJosmAction(new AddMapWithAILayerAction(u)), null);
}
// list all imagery entries where the current map location is within the imagery
// bounds
if (MainApplication.isDisplayingMapView()) {
MapView mv = MainApplication.getMap().mapView;
LatLon pos = mv.getProjection().eastNorth2latlon(mv.getCenter());
final List<MapWithAIInfo> alreadyInUse = MapWithAILayerInfo.getInstance().getLayers();
final List<MapWithAIInfo> inViewLayers = MapWithAILayerInfo.getInstance().getDefaultLayers().stream()
.filter(i -> i.getBounds() != null && i.getBounds().contains(pos) && !alreadyInUse.contains(i)
&& isPosInOneShapeIfAny(i, pos))
.sorted(alphabeticSourceComparator).collect(Collectors.toList());
if (!inViewLayers.isEmpty()) {
if (inViewLayers.stream().anyMatch(i -> i.getCategory() == i.getCategory().getDefault())) {
addDynamicSeparator();
}
for (MapWithAIInfo i : inViewLayers) {
addDynamic(trackJosmAction(new AddMapWithAILayerAction(i)), i.getCategory());
}
}
if (!dynamicNonPhotoItems.isEmpty()) {
addDynamicSeparator();
for (Entry<MapWithAICategory, List<JMenuItem>> e : dynamicNonPhotoItems.entrySet()) {
MapWithAICategory cat = e.getKey();
List<JMenuItem> list = e.getValue();
if (list.size() > 1) {
JMenuItem categoryMenu = new JMenu(cat.getDescription());
categoryMenu.setIcon(cat.getIcon(ImageSizes.MENU));
for (JMenuItem it : list) {
categoryMenu.add(it);
}
dynamicNonPhotoMenus.add(add(categoryMenu));
} else if (!list.isEmpty()) {
dynamicNonPhotoMenus.add(add(list.get(0)));
}
}
}
}
}
/**
* List to store temporary "photo" menu items. They will be deleted (and
* possibly recreated) when refreshImageryMenu() is called.
*/
private final List<Object> dynamicItems = new ArrayList<>(20);
/**
* Map to store temporary "not photo" menu items. They will be deleted (and
* possibly recreated) when refreshImageryMenu() is called.
*/
private final Map<MapWithAICategory, List<JMenuItem>> dynamicNonPhotoItems = new EnumMap<>(MapWithAICategory.class);
/**
* List to store temporary "not photo" submenus. They will be deleted (and
* possibly recreated) when refreshImageryMenu() is called.
*/
private final List<JMenuItem> dynamicNonPhotoMenus = new ArrayList<>(20);
private final List<JosmAction> dynJosmActions = new ArrayList<>(20);
/**
* Remove all the items in dynamic items collection
*
* @since 5803
*/
private void removeDynamicItems() {
dynJosmActions.forEach(JosmAction::destroy);
dynJosmActions.clear();
dynamicItems.forEach(this::removeDynamicItem);
dynamicItems.clear();
dynamicNonPhotoMenus.forEach(this::removeDynamicItem);
dynamicItems.clear();
dynamicNonPhotoItems.clear();
}
private void removeDynamicItem(Object item) {
if (item instanceof JMenuItem) {
remove((JMenuItem) item);
} else if (item instanceof MenuComponent) {
remove((MenuComponent) item);
} else if (item instanceof Component) {
remove((Component) item);
} else {
Logging.error("Unknown imagery menu item type: {0}", item);
}
}
private void addDynamicSeparator() {
JPopupMenu.Separator s = new JPopupMenu.Separator();
dynamicItems.add(s);
add(s);
}
private void addDynamic(Action a, MapWithAICategory category) {
JMenuItem item = createActionComponent(a);
item.setAction(a);
doAddDynamic(item, category);
}
private void doAddDynamic(JMenuItem item, MapWithAICategory category) {
if (category == null || category == MapWithAICategory.FEATURED) {
dynamicItems.add(this.add(item));
} else {
dynamicNonPhotoItems.computeIfAbsent(category, x -> new ArrayList<>()).add(item);
}
}
private Action trackJosmAction(Action action) {
if (action instanceof JosmAction) {
dynJosmActions.add((JosmAction) action);
}
return action;
}
}

Wyświetl plik

@ -88,10 +88,11 @@ public class MapWithAIPluginTest {
menuEntries.setAccessible(true); menuEntries.setAccessible(true);
// + 1 comes from the preferences panel // + 1 comes from the preferences panel
final int addedMenuItems = ((Map<?, ?>) menuEntries.get(plugin)).size() + 1; final int addedMenuItems = ((Map<?, ?>) menuEntries.get(plugin)).size() + 1;
final JMenu dataMenu = MainApplication.getMenu().dataMenu;
final int dataMenuSize = dataMenu.getMenuComponentCount();
plugin = new MapWithAIPlugin(info); plugin = new MapWithAIPlugin(info);
assertEquals(dataMenuSize + addedMenuItems, dataMenu.getMenuComponentCount(), "Menu items were not added"); // Currently adding the menu at the 9th index
final JMenu dataMenu = MainApplication.getMenu().getMenu(9);
final int dataMenuSize = dataMenu.getMenuComponentCount();
assertEquals(addedMenuItems, dataMenu.getMenuComponentCount(), "Menu items were not added");
assertEquals(1, assertEquals(1,
MapPaintStyles.getStyles().getStyleSources().parallelStream() MapPaintStyles.getStyles().getStyleSources().parallelStream()
.filter(source -> source.url != null && source.name.contains("MapWithAI")).count(), .filter(source -> source.url != null && source.name.contains("MapWithAI")).count(),
@ -114,8 +115,7 @@ public class MapWithAIPluginTest {
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
plugin = new MapWithAIPlugin(info); plugin = new MapWithAIPlugin(info);
assertEquals(dataMenuSize + addedMenuItems, dataMenu.getMenuComponentCount(), assertEquals(addedMenuItems, dataMenu.getMenuComponentCount(), "The menu items were added multiple times");
"The menu items were added multiple times");
assertEquals(1, assertEquals(1,
MapPaintStyles.getStyles().getStyleSources().parallelStream() MapPaintStyles.getStyles().getStyleSources().parallelStream()
.filter(source -> source.url != null && source.name.contains("MapWithAI")).count(), .filter(source -> source.url != null && source.name.contains("MapWithAI")).count(),