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                // Do nothing
124            }
125
126            @Override
127            public void menuCanceled(MenuEvent e) {
128                // Do nothing
129            }
130        });
131        MainMenu.add(subMenu, rectaction);
132    }
133
134    private void setupMenuScroller() {
135        if (!GraphicsEnvironment.isHeadless()) {
136            MenuScroller.setScrollerFor(this, 150, 2);
137        }
138    }
139
140    /**
141     * Refresh imagery menu.
142     *
143     * Outside this class only called in {@link ImageryPreference#initialize()}.
144     * (In order to have actions ready for the toolbar, see #8446.)
145     */
146    public void refreshImageryMenu() {
147        removeDynamicItems();
148
149        addDynamic(offsetMenuItem);
150        addDynamicSeparator();
151
152        // for each configured ImageryInfo, add a menu entry.
153        final List<ImageryInfo> savedLayers = new ArrayList<>(ImageryLayerInfo.instance.getLayers());
154        Collections.sort(savedLayers, alphabeticImageryComparator);
155        for (final ImageryInfo u : savedLayers) {
156            addDynamic(new AddImageryLayerAction(u));
157        }
158
159        // list all imagery entries where the current map location
160        // is within the imagery bounds
161        if (Main.isDisplayingMapView()) {
162            MapView mv = Main.map.mapView;
163            LatLon pos = mv.getProjection().eastNorth2latlon(mv.getCenter());
164            final List<ImageryInfo> inViewLayers = new ArrayList<>();
165
166            for (ImageryInfo i : ImageryLayerInfo.instance.getDefaultLayers()) {
167                if (i.getBounds() != null && i.getBounds().contains(pos)) {
168                    inViewLayers.add(i);
169                }
170            }
171            // Do not suggest layers already in use
172            inViewLayers.removeAll(ImageryLayerInfo.instance.getLayers());
173            // For layers containing complex shapes, check that center is in one
174            // of its shapes (fix #7910)
175            for (Iterator<ImageryInfo> iti = inViewLayers.iterator(); iti.hasNext();) {
176                List<Shape> shapes = iti.next().getBounds().getShapes();
177                if (shapes != null && !shapes.isEmpty()) {
178                    boolean found = false;
179                    for (Iterator<Shape> its = shapes.iterator(); its.hasNext() && !found;) {
180                        found = its.next().contains(pos);
181                    }
182                    if (!found) {
183                        iti.remove();
184                    }
185                }
186            }
187            if (!inViewLayers.isEmpty()) {
188                Collections.sort(inViewLayers, alphabeticImageryComparator);
189                addDynamicSeparator();
190                for (ImageryInfo i : inViewLayers) {
191                    addDynamic(new AddImageryLayerAction(i));
192                }
193            }
194        }
195
196        addDynamicSeparator();
197        JMenu subMenu = Main.main.menu.imagerySubMenu;
198        int heightUnrolled = 30*(getItemCount()+subMenu.getItemCount());
199        if (heightUnrolled < Main.panel.getHeight()) {
200            // add all items of submenu if they will fit on screen
201            int n = subMenu.getItemCount();
202            for (int i = 0; i < n; i++) {
203                addDynamic(subMenu.getItem(i).getAction());
204            }
205        } else {
206            // or add the submenu itself
207            addDynamic(subMenu);
208        }
209    }
210
211    private JMenuItem getNewOffsetMenu() {
212        if (!Main.isDisplayingMapView()) {
213            offsetAction.setEnabled(false);
214            return singleOffset;
215        }
216        Collection<ImageryLayer> layers = Main.map.mapView.getLayersOfType(ImageryLayer.class);
217        if (layers.isEmpty()) {
218            offsetAction.setEnabled(false);
219            return singleOffset;
220        }
221        offsetAction.setEnabled(true);
222        JMenu newMenu = new JMenu(trc("layer", "Offset"));
223        newMenu.setIcon(ImageProvider.get("mapmode", "adjustimg"));
224        newMenu.setAction(offsetAction);
225        if (layers.size() == 1)
226            return (JMenuItem) layers.iterator().next().getOffsetMenuItem(newMenu);
227        for (ImageryLayer layer : layers) {
228            JMenuItem layerMenu = layer.getOffsetMenuItem();
229            layerMenu.setText(layer.getName());
230            layerMenu.setIcon(layer.getIcon());
231            newMenu.add(layerMenu);
232        }
233        return newMenu;
234    }
235
236    public void refreshOffsetMenu() {
237        offsetMenuItem = getNewOffsetMenu();
238    }
239
240    @Override
241    public void activeLayerChange(Layer oldLayer, Layer newLayer) {
242        // Do nothing
243    }
244
245    @Override
246    public void layerAdded(Layer newLayer) {
247        if (newLayer instanceof ImageryLayer) {
248            refreshOffsetMenu();
249        }
250    }
251
252    @Override
253    public void layerRemoved(Layer oldLayer) {
254        if (oldLayer instanceof ImageryLayer) {
255            refreshOffsetMenu();
256        }
257    }
258
259    /**
260     * Collection to store temporary menu items. They will be deleted
261     * (and possibly recreated) when refreshImageryMenu() is called.
262     * @since 5803
263     */
264    private final List<Object> dynamicItems = new ArrayList<>(20);
265
266    /**
267     * Remove all the items in @field dynamicItems collection
268     * @since 5803
269     */
270    private void removeDynamicItems() {
271        for (Object item : dynamicItems) {
272            if (item instanceof JMenuItem) {
273                remove((JMenuItem) item);
274            }
275            if (item instanceof MenuComponent) {
276                remove((MenuComponent) item);
277            }
278            if (item instanceof Component) {
279                remove((Component) item);
280            }
281        }
282        dynamicItems.clear();
283    }
284
285    private void addDynamicSeparator() {
286        JPopupMenu.Separator s =  new JPopupMenu.Separator();
287        dynamicItems.add(s);
288        add(s);
289    }
290
291    private void addDynamic(Action a) {
292        dynamicItems.add(this.add(a));
293    }
294
295    private void addDynamic(JMenuItem it) {
296        dynamicItems.add(this.add(it));
297    }
298}