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