001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trc; 006 007import java.awt.Component; 008import java.awt.GraphicsEnvironment; 009import java.awt.MenuComponent; 010import java.awt.event.ActionEvent; 011import java.util.ArrayList; 012import java.util.Collection; 013import java.util.Comparator; 014import java.util.EnumMap; 015import java.util.List; 016import java.util.Locale; 017import java.util.Map; 018import java.util.Map.Entry; 019import java.util.Optional; 020import java.util.stream.Collectors; 021 022import javax.swing.Action; 023import javax.swing.JComponent; 024import javax.swing.JMenu; 025import javax.swing.JMenuItem; 026import javax.swing.JPopupMenu; 027import javax.swing.event.MenuEvent; 028import javax.swing.event.MenuListener; 029 030import org.openstreetmap.josm.actions.AddImageryLayerAction; 031import org.openstreetmap.josm.actions.JosmAction; 032import org.openstreetmap.josm.actions.MapRectifierWMSmenuAction; 033import org.openstreetmap.josm.data.coor.LatLon; 034import org.openstreetmap.josm.data.imagery.ImageryInfo; 035import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryCategory; 036import org.openstreetmap.josm.data.imagery.ImageryLayerInfo; 037import org.openstreetmap.josm.data.imagery.Shape; 038import org.openstreetmap.josm.gui.layer.ImageryLayer; 039import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 040import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 041import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 042import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 043import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference; 044import org.openstreetmap.josm.tools.ImageProvider; 045import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; 046import org.openstreetmap.josm.tools.Logging; 047 048/** 049 * Imagery menu, holding entries for imagery preferences, offset actions and dynamic imagery entries 050 * depending on current mapview coordinates. 051 * @since 3737 052 */ 053public class ImageryMenu extends JMenu implements LayerChangeListener { 054 055 static final class AdjustImageryOffsetAction extends JosmAction { 056 057 AdjustImageryOffsetAction() { 058 super(tr("Imagery offset"), "mapmode/adjustimg", tr("Adjust imagery offset"), null, false, false); 059 putValue("toolbar", "imagery-offset"); 060 MainApplication.getToolbar().register(this); 061 } 062 063 @Override 064 public void actionPerformed(ActionEvent e) { 065 Collection<ImageryLayer> layers = MainApplication.getLayerManager().getLayersOfType(ImageryLayer.class); 066 if (layers.isEmpty()) { 067 setEnabled(false); 068 return; 069 } 070 Component source = null; 071 if (e.getSource() instanceof Component) { 072 source = (Component) e.getSource(); 073 } 074 JPopupMenu popup = new JPopupMenu(); 075 if (layers.size() == 1) { 076 JComponent c = layers.iterator().next().getOffsetMenuItem(popup); 077 if (c instanceof JMenuItem) { 078 ((JMenuItem) c).getAction().actionPerformed(e); 079 } else { 080 if (source == null || !source.isShowing()) return; 081 popup.show(source, source.getWidth()/2, source.getHeight()/2); 082 } 083 return; 084 } 085 if (source == null || !source.isShowing()) return; 086 for (ImageryLayer layer : layers) { 087 JMenuItem layerMenu = layer.getOffsetMenuItem(); 088 layerMenu.setText(layer.getName()); 089 layerMenu.setIcon(layer.getIcon()); 090 popup.add(layerMenu); 091 } 092 popup.show(source, source.getWidth()/2, source.getHeight()/2); 093 } 094 } 095 096 /** 097 * Compare ImageryInfo objects alphabetically by name. 098 * 099 * ImageryInfo objects are normally sorted by country code first 100 * (for the preferences). We don't want this in the imagery menu. 101 */ 102 public static final Comparator<ImageryInfo> alphabeticImageryComparator = 103 (ii1, ii2) -> ii1.getName().toLowerCase(Locale.ENGLISH).compareTo(ii2.getName().toLowerCase(Locale.ENGLISH)); 104 105 private final transient Action offsetAction = new AdjustImageryOffsetAction(); 106 107 private final JMenuItem singleOffset = new JMenuItem(offsetAction); 108 private JMenuItem offsetMenuItem = singleOffset; 109 private final MapRectifierWMSmenuAction rectaction = new MapRectifierWMSmenuAction(); 110 111 /** 112 * Constructs a new {@code ImageryMenu}. 113 * @param subMenu submenu in that contains plugin-managed additional imagery layers 114 */ 115 public ImageryMenu(JMenu subMenu) { 116 /* I18N: mnemonic: I */ 117 super(trc("menu", "Imagery")); 118 setupMenuScroller(); 119 MainApplication.getLayerManager().addLayerChangeListener(this); 120 // build dynamically 121 addMenuListener(new MenuListener() { 122 @Override 123 public void menuSelected(MenuEvent e) { 124 refreshImageryMenu(); 125 } 126 127 @Override 128 public void menuDeselected(MenuEvent e) { 129 // Do nothing 130 } 131 132 @Override 133 public void menuCanceled(MenuEvent e) { 134 // Do nothing 135 } 136 }); 137 MainMenu.add(subMenu, rectaction); 138 } 139 140 private void setupMenuScroller() { 141 if (!GraphicsEnvironment.isHeadless()) { 142 MenuScroller.setScrollerFor(this, 150, 2); 143 } 144 } 145 146 /** 147 * For layers containing complex shapes, check that center is in one of its shapes (fix #7910) 148 * @param info layer info 149 * @param pos center 150 * @return {@code true} if center is in one of info shapes 151 */ 152 private static boolean isPosInOneShapeIfAny(ImageryInfo info, LatLon pos) { 153 List<Shape> shapes = info.getBounds().getShapes(); 154 return shapes == null || shapes.isEmpty() || shapes.stream().anyMatch(s -> s.contains(pos)); 155 } 156 157 /** 158 * Refresh imagery menu. 159 * 160 * Outside this class only called in {@link ImageryPreference#initialize()}. 161 * (In order to have actions ready for the toolbar, see #8446.) 162 */ 163 public void refreshImageryMenu() { 164 removeDynamicItems(); 165 166 addDynamic(offsetMenuItem, null); 167 addDynamicSeparator(); 168 169 // for each configured ImageryInfo, add a menu entry. 170 final List<ImageryInfo> savedLayers = new ArrayList<>(ImageryLayerInfo.instance.getLayers()); 171 savedLayers.sort(alphabeticImageryComparator); 172 for (final ImageryInfo u : savedLayers) { 173 addDynamic(trackJosmAction(new AddImageryLayerAction(u)), null); 174 } 175 176 // list all imagery entries where the current map location is within the imagery bounds 177 if (MainApplication.isDisplayingMapView()) { 178 MapView mv = MainApplication.getMap().mapView; 179 LatLon pos = mv.getProjection().eastNorth2latlon(mv.getCenter()); 180 final List<ImageryInfo> alreadyInUse = ImageryLayerInfo.instance.getLayers(); 181 final List<ImageryInfo> inViewLayers = ImageryLayerInfo.instance.getDefaultLayers() 182 .stream().filter(i -> i.getBounds() != null && i.getBounds().contains(pos) 183 && !alreadyInUse.contains(i) && isPosInOneShapeIfAny(i, pos)) 184 .sorted(alphabeticImageryComparator) 185 .collect(Collectors.toList()); 186 if (!inViewLayers.isEmpty()) { 187 if (inViewLayers.stream().anyMatch(i -> i.getImageryCategory() == ImageryCategory.PHOTO)) { 188 addDynamicSeparator(); 189 } 190 for (ImageryInfo i : inViewLayers) { 191 addDynamic(trackJosmAction(new AddImageryLayerAction(i)), i.getImageryCategory()); 192 } 193 } 194 if (!dynamicNonPhotoItems.isEmpty()) { 195 addDynamicSeparator(); 196 for (Entry<ImageryCategory, List<JMenuItem>> e : dynamicNonPhotoItems.entrySet()) { 197 ImageryCategory cat = e.getKey(); 198 JMenuItem categoryMenu = new JMenu(cat.getDescription()); 199 categoryMenu.setIcon(cat.getIcon(ImageSizes.MENU)); 200 for (JMenuItem it : e.getValue()) { 201 categoryMenu.add(it); 202 } 203 dynamicNonPhotoMenus.add(add(categoryMenu)); 204 } 205 } 206 } 207 208 addDynamicSeparator(); 209 JMenu subMenu = MainApplication.getMenu().imagerySubMenu; 210 int heightUnrolled = 30*(getItemCount()+subMenu.getItemCount()); 211 if (heightUnrolled < MainApplication.getMainPanel().getHeight()) { 212 // add all items of submenu if they will fit on screen 213 int n = subMenu.getItemCount(); 214 for (int i = 0; i < n; i++) { 215 addDynamic(subMenu.getItem(i).getAction(), null); 216 } 217 } else { 218 // or add the submenu itself 219 addDynamic(subMenu, null); 220 } 221 } 222 223 private JMenuItem getNewOffsetMenu() { 224 Collection<ImageryLayer> layers = MainApplication.getLayerManager().getLayersOfType(ImageryLayer.class); 225 if (layers.isEmpty()) { 226 offsetAction.setEnabled(false); 227 return singleOffset; 228 } 229 offsetAction.setEnabled(true); 230 JMenu newMenu = new JMenu(trc("layer", "Offset")); 231 newMenu.setIcon(ImageProvider.get("mapmode", "adjustimg")); 232 newMenu.setAction(offsetAction); 233 if (layers.size() == 1) 234 return (JMenuItem) layers.iterator().next().getOffsetMenuItem(newMenu); 235 for (ImageryLayer layer : layers) { 236 JMenuItem layerMenu = layer.getOffsetMenuItem(); 237 layerMenu.setText(layer.getName()); 238 layerMenu.setIcon(layer.getIcon()); 239 newMenu.add(layerMenu); 240 } 241 return newMenu; 242 } 243 244 /** 245 * Refresh offset menu item. 246 */ 247 public void refreshOffsetMenu() { 248 offsetMenuItem = getNewOffsetMenu(); 249 } 250 251 @Override 252 public void layerAdded(LayerAddEvent e) { 253 if (e.getAddedLayer() instanceof ImageryLayer) { 254 refreshOffsetMenu(); 255 } 256 } 257 258 @Override 259 public void layerRemoving(LayerRemoveEvent e) { 260 if (e.getRemovedLayer() instanceof ImageryLayer) { 261 refreshOffsetMenu(); 262 } 263 } 264 265 @Override 266 public void layerOrderChanged(LayerOrderChangeEvent e) { 267 refreshOffsetMenu(); 268 } 269 270 /** 271 * List to store temporary "photo" menu items. They will be deleted 272 * (and possibly recreated) when refreshImageryMenu() is called. 273 */ 274 private final List<Object> dynamicItems = new ArrayList<>(20); 275 /** 276 * Map to store temporary "not photo" menu items. They will be deleted 277 * (and possibly recreated) when refreshImageryMenu() is called. 278 */ 279 private final Map<ImageryCategory, List<JMenuItem>> dynamicNonPhotoItems = new EnumMap<>(ImageryCategory.class); 280 /** 281 * List to store temporary "not photo" submenus. They will be deleted 282 * (and possibly recreated) when refreshImageryMenu() is called. 283 */ 284 private final List<JMenuItem> dynamicNonPhotoMenus = new ArrayList<>(20); 285 private final List<JosmAction> dynJosmActions = new ArrayList<>(20); 286 287 /** 288 * Remove all the items in dynamic items collection 289 * @since 5803 290 */ 291 private void removeDynamicItems() { 292 dynJosmActions.forEach(JosmAction::destroy); 293 dynJosmActions.clear(); 294 dynamicItems.forEach(this::removeDynamicItem); 295 dynamicItems.clear(); 296 dynamicNonPhotoMenus.forEach(this::removeDynamicItem); 297 dynamicItems.clear(); 298 dynamicNonPhotoItems.clear(); 299 } 300 301 private void removeDynamicItem(Object item) { 302 if (item instanceof JMenuItem) { 303 Optional.ofNullable(((JMenuItem) item).getAction()).ifPresent(MainApplication.getToolbar()::unregister); 304 remove((JMenuItem) item); 305 } else if (item instanceof MenuComponent) { 306 remove((MenuComponent) item); 307 } else if (item instanceof Component) { 308 remove((Component) item); 309 } else { 310 Logging.error("Unknown imagery menu item type: {0}", item); 311 } 312 } 313 314 private void addDynamicSeparator() { 315 JPopupMenu.Separator s = new JPopupMenu.Separator(); 316 dynamicItems.add(s); 317 add(s); 318 } 319 320 private void addDynamic(Action a, ImageryCategory category) { 321 JMenuItem item = createActionComponent(a); 322 item.setAction(a); 323 doAddDynamic(item, category); 324 } 325 326 private void addDynamic(JMenuItem it, ImageryCategory category) { 327 doAddDynamic(it, category); 328 } 329 330 private void doAddDynamic(JMenuItem item, ImageryCategory category) { 331 if (category == null || category == ImageryCategory.PHOTO) { 332 dynamicItems.add(this.add(item)); 333 } else { 334 dynamicNonPhotoItems.computeIfAbsent(category, x -> new ArrayList<>()).add(item); 335 } 336 } 337 338 private Action trackJosmAction(Action action) { 339 if (action instanceof JosmAction) { 340 dynJosmActions.add((JosmAction) action); 341 } 342 return action; 343 } 344 345}