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}