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