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