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