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