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