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;
017import java.util.Optional;
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.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 mapview coordinates.
045 * @since 3737
046 */
047public class ImageryMenu extends JMenu implements LayerChangeListener {
048
049    static final class AdjustImageryOffsetAction extends JosmAction {
050
051        AdjustImageryOffsetAction() {
052            super(tr("Imagery offset"), "mapmode/adjustimg", tr("Adjust imagery offset"), null, false, false);
053            putValue("toolbar", "imagery-offset");
054            MainApplication.getToolbar().register(this);
055        }
056
057        @Override
058        public void actionPerformed(ActionEvent e) {
059            Collection<ImageryLayer> layers = MainApplication.getLayerManager().getLayersOfType(ImageryLayer.class);
060            if (layers.isEmpty()) {
061                setEnabled(false);
062                return;
063            }
064            Component source = null;
065            if (e.getSource() instanceof Component) {
066                source = (Component) e.getSource();
067            }
068            JPopupMenu popup = new JPopupMenu();
069            if (layers.size() == 1) {
070                JComponent c = layers.iterator().next().getOffsetMenuItem(popup);
071                if (c instanceof JMenuItem) {
072                    ((JMenuItem) c).getAction().actionPerformed(e);
073                } else {
074                    if (source == null) return;
075                    popup.show(source, source.getWidth()/2, source.getHeight()/2);
076                }
077                return;
078            }
079            if (source == null || !source.isShowing()) return;
080            for (ImageryLayer layer : layers) {
081                JMenuItem layerMenu = layer.getOffsetMenuItem();
082                layerMenu.setText(layer.getName());
083                layerMenu.setIcon(layer.getIcon());
084                popup.add(layerMenu);
085            }
086            popup.show(source, source.getWidth()/2, source.getHeight()/2);
087        }
088    }
089
090    /**
091     * Compare ImageryInfo objects alphabetically by name.
092     *
093     * ImageryInfo objects are normally sorted by country code first
094     * (for the preferences). We don't want this in the imagery menu.
095     */
096    public static final Comparator<ImageryInfo> alphabeticImageryComparator =
097            (ii1, ii2) -> ii1.getName().toLowerCase(Locale.ENGLISH).compareTo(ii2.getName().toLowerCase(Locale.ENGLISH));
098
099    private final transient Action offsetAction = new AdjustImageryOffsetAction();
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        MainApplication.getLayerManager().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        savedLayers.sort(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 (MainApplication.isDisplayingMapView()) {
162            MapView mv = MainApplication.getMap().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                inViewLayers.sort(alphabeticImageryComparator);
189                addDynamicSeparator();
190                for (ImageryInfo i : inViewLayers) {
191                    addDynamic(new AddImageryLayerAction(i));
192                }
193            }
194        }
195
196        addDynamicSeparator();
197        JMenu subMenu = MainApplication.getMenu().imagerySubMenu;
198        int heightUnrolled = 30*(getItemCount()+subMenu.getItemCount());
199        if (heightUnrolled < MainApplication.getMainPanel().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        Collection<ImageryLayer> layers = MainApplication.getLayerManager().getLayersOfType(ImageryLayer.class);
213        if (layers.isEmpty()) {
214            offsetAction.setEnabled(false);
215            return singleOffset;
216        }
217        offsetAction.setEnabled(true);
218        JMenu newMenu = new JMenu(trc("layer", "Offset"));
219        newMenu.setIcon(ImageProvider.get("mapmode", "adjustimg"));
220        newMenu.setAction(offsetAction);
221        if (layers.size() == 1)
222            return (JMenuItem) layers.iterator().next().getOffsetMenuItem(newMenu);
223        for (ImageryLayer layer : layers) {
224            JMenuItem layerMenu = layer.getOffsetMenuItem();
225            layerMenu.setText(layer.getName());
226            layerMenu.setIcon(layer.getIcon());
227            newMenu.add(layerMenu);
228        }
229        return newMenu;
230    }
231
232    /**
233     * Refresh offset menu item.
234     */
235    public void refreshOffsetMenu() {
236        offsetMenuItem = getNewOffsetMenu();
237    }
238
239    @Override
240    public void layerAdded(LayerAddEvent e) {
241        if (e.getAddedLayer() instanceof ImageryLayer) {
242            refreshOffsetMenu();
243        }
244    }
245
246    @Override
247    public void layerRemoving(LayerRemoveEvent e) {
248        if (e.getRemovedLayer() instanceof ImageryLayer) {
249            refreshOffsetMenu();
250        }
251    }
252
253    @Override
254    public void layerOrderChanged(LayerOrderChangeEvent e) {
255        refreshOffsetMenu();
256    }
257
258    /**
259     * Collection to store temporary menu items. They will be deleted
260     * (and possibly recreated) when refreshImageryMenu() is called.
261     * @since 5803
262     */
263    private final List<Object> dynamicItems = new ArrayList<>(20);
264
265    /**
266     * Remove all the items in dynamic items collection
267     * @since 5803
268     */
269    private void removeDynamicItems() {
270        for (Object item : dynamicItems) {
271            if (item instanceof JMenuItem) {
272                Optional.ofNullable(((JMenuItem) item).getAction()).ifPresent(MainApplication.getToolbar()::unregister);
273                remove((JMenuItem) item);
274            } else if (item instanceof MenuComponent) {
275                remove((MenuComponent) item);
276            } else if (item instanceof Component) {
277                remove((Component) item);
278            }
279        }
280        dynamicItems.clear();
281    }
282
283    private void addDynamicSeparator() {
284        JPopupMenu.Separator s = new JPopupMenu.Separator();
285        dynamicItems.add(s);
286        add(s);
287    }
288
289    private void addDynamic(Action a) {
290        dynamicItems.add(this.add(a));
291    }
292
293    private void addDynamic(JMenuItem it) {
294        dynamicItems.add(this.add(it));
295    }
296}