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; 046import org.openstreetmap.josm.tools.Utils; 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 setToolbarId("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 Comparator.comparing(ii -> ii.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 Utils.isEmpty(shapes) || 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); 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 List<JMenuItem> list = e.getValue(); 199 if (list.size() > 1) { 200 JMenuItem categoryMenu = new JMenu(cat.getDescription()); 201 categoryMenu.setIcon(cat.getIcon(ImageSizes.MENU)); 202 for (JMenuItem it : list) { 203 categoryMenu.add(it); 204 } 205 dynamicNonPhotoMenus.add(add(categoryMenu)); 206 } else if (!list.isEmpty()) { 207 dynamicNonPhotoMenus.add(add(list.get(0))); 208 } 209 } 210 } 211 } 212 213 addDynamicSeparator(); 214 JMenu subMenu = MainApplication.getMenu().imagerySubMenu; 215 int heightUnrolled = 30*(getItemCount()+subMenu.getItemCount()); 216 if (heightUnrolled < MainApplication.getMainPanel().getHeight()) { 217 // add all items of submenu if they will fit on screen 218 int n = subMenu.getItemCount(); 219 for (int i = 0; i < n; i++) { 220 addDynamic(subMenu.getItem(i).getAction(), null); 221 } 222 } else { 223 // or add the submenu itself 224 addDynamic(subMenu); 225 } 226 } 227 228 private JMenuItem getNewOffsetMenu() { 229 Collection<ImageryLayer> layers = MainApplication.getLayerManager().getLayersOfType(ImageryLayer.class); 230 if (layers.isEmpty()) { 231 offsetAction.setEnabled(false); 232 return singleOffset; 233 } 234 offsetAction.setEnabled(true); 235 JMenu newMenu = new JMenu(trc("layer", "Offset")); 236 newMenu.setIcon(ImageProvider.get("mapmode", "adjustimg")); 237 newMenu.setAction(offsetAction); 238 if (layers.size() == 1) 239 return (JMenuItem) layers.iterator().next().getOffsetMenuItem(newMenu); 240 for (ImageryLayer layer : layers) { 241 JMenuItem layerMenu = layer.getOffsetMenuItem(); 242 layerMenu.setText(layer.getName()); 243 layerMenu.setIcon(layer.getIcon()); 244 newMenu.add(layerMenu); 245 } 246 return newMenu; 247 } 248 249 /** 250 * Refresh offset menu item. 251 */ 252 public void refreshOffsetMenu() { 253 offsetMenuItem = getNewOffsetMenu(); 254 } 255 256 @Override 257 public void layerAdded(LayerAddEvent e) { 258 if (e.getAddedLayer() instanceof ImageryLayer) { 259 refreshOffsetMenu(); 260 } 261 } 262 263 @Override 264 public void layerRemoving(LayerRemoveEvent e) { 265 if (e.getRemovedLayer() instanceof ImageryLayer) { 266 refreshOffsetMenu(); 267 } 268 } 269 270 @Override 271 public void layerOrderChanged(LayerOrderChangeEvent e) { 272 refreshOffsetMenu(); 273 } 274 275 /** 276 * List to store temporary "photo" menu items. They will be deleted 277 * (and possibly recreated) when refreshImageryMenu() is called. 278 */ 279 private final List<Object> dynamicItems = new ArrayList<>(20); 280 /** 281 * Map to store temporary "not photo" menu items. They will be deleted 282 * (and possibly recreated) when refreshImageryMenu() is called. 283 */ 284 private final Map<ImageryCategory, List<JMenuItem>> dynamicNonPhotoItems = new EnumMap<>(ImageryCategory.class); 285 /** 286 * List to store temporary "not photo" submenus. They will be deleted 287 * (and possibly recreated) when refreshImageryMenu() is called. 288 */ 289 private final List<JMenuItem> dynamicNonPhotoMenus = new ArrayList<>(20); 290 private final List<JosmAction> dynJosmActions = new ArrayList<>(20); 291 292 /** 293 * Remove all the items in dynamic items collection 294 * @since 5803 295 */ 296 private void removeDynamicItems() { 297 dynJosmActions.forEach(JosmAction::destroy); 298 dynJosmActions.clear(); 299 dynamicItems.forEach(this::removeDynamicItem); 300 dynamicItems.clear(); 301 dynamicNonPhotoMenus.forEach(this::removeDynamicItem); 302 dynamicItems.clear(); 303 dynamicNonPhotoItems.clear(); 304 } 305 306 private void removeDynamicItem(Object item) { 307 if (item instanceof JMenuItem) { 308 remove((JMenuItem) item); 309 } else if (item instanceof MenuComponent) { 310 remove((MenuComponent) item); 311 } else if (item instanceof Component) { 312 remove((Component) item); 313 } else { 314 Logging.error("Unknown imagery menu item type: {0}", item); 315 } 316 } 317 318 private void addDynamicSeparator() { 319 JPopupMenu.Separator s = new JPopupMenu.Separator(); 320 dynamicItems.add(s); 321 add(s); 322 } 323 324 private void addDynamic(Action a, ImageryCategory category) { 325 JMenuItem item = createActionComponent(a); 326 item.setAction(a); 327 doAddDynamic(item, category); 328 } 329 330 private void addDynamic(JMenuItem it) { 331 doAddDynamic(it, null); 332 } 333 334 private void doAddDynamic(JMenuItem item, ImageryCategory category) { 335 if (category == null || category == ImageryCategory.PHOTO) { 336 dynamicItems.add(this.add(item)); 337 } else { 338 dynamicNonPhotoItems.computeIfAbsent(category, x -> new ArrayList<>()).add(item); 339 } 340 } 341 342 private Action trackJosmAction(Action action) { 343 if (action instanceof JosmAction) { 344 dynJosmActions.add((JosmAction) action); 345 } 346 return action; 347 } 348 349}