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