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