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.MapView.LayerChangeListener; 036import org.openstreetmap.josm.gui.layer.ImageryLayer; 037import org.openstreetmap.josm.gui.layer.Layer; 038import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference; 039import org.openstreetmap.josm.tools.ImageProvider; 040 041/** 042 * Imagery menu, holding entries for imagery preferences, offset actions and dynamic imagery entries 043 * depending on current maview coordinates. 044 * @since 3737 045 */ 046public class ImageryMenu extends JMenu implements LayerChangeListener { 047 048 /** 049 * Compare ImageryInfo objects alphabetically by name. 050 * 051 * ImageryInfo objects are normally sorted by country code first 052 * (for the preferences). We don't want this in the imagery menu. 053 */ 054 public static final Comparator<ImageryInfo> alphabeticImageryComparator = new Comparator<ImageryInfo>() { 055 @Override 056 public int compare(ImageryInfo ii1, ImageryInfo ii2) { 057 return ii1.getName().toLowerCase(Locale.ENGLISH).compareTo(ii2.getName().toLowerCase(Locale.ENGLISH)); 058 } 059 }; 060 061 private final transient Action offsetAction = new JosmAction( 062 tr("Imagery offset"), "mapmode/adjustimg", tr("Adjust imagery offset"), null, false, false) { 063 { 064 putValue("toolbar", "imagery-offset"); 065 Main.toolbar.register(this); 066 } 067 068 @Override 069 public void actionPerformed(ActionEvent e) { 070 Collection<ImageryLayer> layers = Main.map.mapView.getLayersOfType(ImageryLayer.class); 071 if (layers.isEmpty()) { 072 setEnabled(false); 073 return; 074 } 075 Component source = null; 076 if (e.getSource() instanceof Component) { 077 source = (Component) e.getSource(); 078 } 079 JPopupMenu popup = new JPopupMenu(); 080 if (layers.size() == 1) { 081 JComponent c = layers.iterator().next().getOffsetMenuItem(popup); 082 if (c instanceof JMenuItem) { 083 ((JMenuItem) c).getAction().actionPerformed(e); 084 } else { 085 if (source == null) return; 086 popup.show(source, source.getWidth()/2, source.getHeight()/2); 087 } 088 return; 089 } 090 if (source == null) return; 091 for (ImageryLayer layer : layers) { 092 JMenuItem layerMenu = layer.getOffsetMenuItem(); 093 layerMenu.setText(layer.getName()); 094 layerMenu.setIcon(layer.getIcon()); 095 popup.add(layerMenu); 096 } 097 popup.show(source, source.getWidth()/2, source.getHeight()/2); 098 } 099 }; 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 MapView.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 } 124 125 @Override 126 public void menuCanceled(MenuEvent e) { 127 } 128 }); 129 MainMenu.add(subMenu, rectaction); 130 } 131 132 private void setupMenuScroller() { 133 if (!GraphicsEnvironment.isHeadless()) { 134 MenuScroller.setScrollerFor(this, 150, 2); 135 } 136 } 137 138 /** 139 * Refresh imagery menu. 140 * 141 * Outside this class only called in {@link ImageryPreference#initialize()}. 142 * (In order to have actions ready for the toolbar, see #8446.) 143 */ 144 public void refreshImageryMenu() { 145 removeDynamicItems(); 146 147 addDynamic(offsetMenuItem); 148 addDynamicSeparator(); 149 150 // for each configured ImageryInfo, add a menu entry. 151 final List<ImageryInfo> savedLayers = new ArrayList<>(ImageryLayerInfo.instance.getLayers()); 152 Collections.sort(savedLayers, alphabeticImageryComparator); 153 for (final ImageryInfo u : savedLayers) { 154 addDynamic(new AddImageryLayerAction(u)); 155 } 156 157 // list all imagery entries where the current map location 158 // is within the imagery bounds 159 if (Main.isDisplayingMapView()) { 160 MapView mv = Main.map.mapView; 161 LatLon pos = mv.getProjection().eastNorth2latlon(mv.getCenter()); 162 final List<ImageryInfo> inViewLayers = new ArrayList<>(); 163 164 for (ImageryInfo i : ImageryLayerInfo.instance.getDefaultLayers()) { 165 if (i.getBounds() != null && i.getBounds().contains(pos)) { 166 inViewLayers.add(i); 167 } 168 } 169 // Do not suggest layers already in use 170 inViewLayers.removeAll(ImageryLayerInfo.instance.getLayers()); 171 // For layers containing complex shapes, check that center is in one 172 // of its shapes (fix #7910) 173 for (Iterator<ImageryInfo> iti = inViewLayers.iterator(); iti.hasNext();) { 174 List<Shape> shapes = iti.next().getBounds().getShapes(); 175 if (shapes != null && !shapes.isEmpty()) { 176 boolean found = false; 177 for (Iterator<Shape> its = shapes.iterator(); its.hasNext() && !found;) { 178 found = its.next().contains(pos); 179 } 180 if (!found) { 181 iti.remove(); 182 } 183 } 184 } 185 if (!inViewLayers.isEmpty()) { 186 Collections.sort(inViewLayers, alphabeticImageryComparator); 187 addDynamicSeparator(); 188 for (ImageryInfo i : inViewLayers) { 189 addDynamic(new AddImageryLayerAction(i)); 190 } 191 } 192 } 193 194 addDynamicSeparator(); 195 JMenu subMenu = Main.main.menu.imagerySubMenu; 196 int heightUnrolled = 30*(getItemCount()+subMenu.getItemCount()); 197 if (heightUnrolled < Main.panel.getHeight()) { 198 // add all items of submenu if they will fit on screen 199 int n = subMenu.getItemCount(); 200 for (int i = 0; i < n; i++) { 201 addDynamic(subMenu.getItem(i).getAction()); 202 } 203 } else { 204 // or add the submenu itself 205 addDynamic(subMenu); 206 } 207 } 208 209 private JMenuItem getNewOffsetMenu() { 210 if (!Main.isDisplayingMapView()) { 211 offsetAction.setEnabled(false); 212 return singleOffset; 213 } 214 Collection<ImageryLayer> layers = Main.map.mapView.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 activeLayerChange(Layer oldLayer, Layer newLayer) { 240 } 241 242 @Override 243 public void layerAdded(Layer newLayer) { 244 if (newLayer instanceof ImageryLayer) { 245 refreshOffsetMenu(); 246 } 247 } 248 249 @Override 250 public void layerRemoved(Layer oldLayer) { 251 if (oldLayer instanceof ImageryLayer) { 252 refreshOffsetMenu(); 253 } 254 } 255 256 /** 257 * Collection to store temporary menu items. They will be deleted 258 * (and possibly recreated) when refreshImageryMenu() is called. 259 * @since 5803 260 */ 261 private final List<Object> dynamicItems = new ArrayList<>(20); 262 263 /** 264 * Remove all the items in @field dynamicItems collection 265 * @since 5803 266 */ 267 private void removeDynamicItems() { 268 for (Object item : dynamicItems) { 269 if (item instanceof JMenuItem) { 270 remove((JMenuItem) item); 271 } 272 if (item instanceof MenuComponent) { 273 remove((MenuComponent) item); 274 } 275 if (item instanceof Component) { 276 remove((Component) item); 277 } 278 } 279 dynamicItems.clear(); 280 } 281 282 private void addDynamicSeparator() { 283 JPopupMenu.Separator s = new JPopupMenu.Separator(); 284 dynamicItems.add(s); 285 add(s); 286 } 287 288 private void addDynamic(Action a) { 289 dynamicItems.add(this.add(a)); 290 } 291 292 private void addDynamic(JMenuItem it) { 293 dynamicItems.add(this.add(it)); 294 } 295}