source: josm/trunk/src/org/openstreetmap/josm/gui/ImageryMenu.java@ 15371

Last change on this file since 15371 was 15154, checked in by Don-vip, 5 years ago

fix #17779 - make sure imagery entries without shapes are displayed in imagery menu

  • Property svn:eol-style set to native
File size: 13.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trc;
6
7import java.awt.Component;
8import java.awt.GraphicsEnvironment;
9import java.awt.MenuComponent;
10import java.awt.event.ActionEvent;
11import java.util.ArrayList;
12import java.util.Collection;
13import java.util.Comparator;
14import java.util.EnumMap;
15import java.util.List;
16import java.util.Locale;
17import java.util.Map;
18import java.util.Map.Entry;
19import java.util.Optional;
20import java.util.stream.Collectors;
21
22import javax.swing.Action;
23import javax.swing.JComponent;
24import javax.swing.JMenu;
25import javax.swing.JMenuItem;
26import javax.swing.JPopupMenu;
27import javax.swing.event.MenuEvent;
28import javax.swing.event.MenuListener;
29
30import org.openstreetmap.josm.actions.AddImageryLayerAction;
31import org.openstreetmap.josm.actions.JosmAction;
32import org.openstreetmap.josm.actions.MapRectifierWMSmenuAction;
33import org.openstreetmap.josm.data.coor.LatLon;
34import org.openstreetmap.josm.data.imagery.ImageryInfo;
35import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryCategory;
36import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
37import org.openstreetmap.josm.data.imagery.Shape;
38import org.openstreetmap.josm.gui.layer.ImageryLayer;
39import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
40import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
41import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
42import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
43import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference;
44import org.openstreetmap.josm.tools.ImageProvider;
45import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
46import org.openstreetmap.josm.tools.Logging;
47
48/**
49 * Imagery menu, holding entries for imagery preferences, offset actions and dynamic imagery entries
50 * depending on current mapview coordinates.
51 * @since 3737
52 */
53public class ImageryMenu extends JMenu implements LayerChangeListener {
54
55 static final class AdjustImageryOffsetAction extends JosmAction {
56
57 AdjustImageryOffsetAction() {
58 super(tr("Imagery offset"), "mapmode/adjustimg", tr("Adjust imagery offset"), null, false, false);
59 putValue("toolbar", "imagery-offset");
60 MainApplication.getToolbar().register(this);
61 }
62
63 @Override
64 public void actionPerformed(ActionEvent e) {
65 Collection<ImageryLayer> layers = MainApplication.getLayerManager().getLayersOfType(ImageryLayer.class);
66 if (layers.isEmpty()) {
67 setEnabled(false);
68 return;
69 }
70 Component source = null;
71 if (e.getSource() instanceof Component) {
72 source = (Component) e.getSource();
73 }
74 JPopupMenu popup = new JPopupMenu();
75 if (layers.size() == 1) {
76 JComponent c = layers.iterator().next().getOffsetMenuItem(popup);
77 if (c instanceof JMenuItem) {
78 ((JMenuItem) c).getAction().actionPerformed(e);
79 } else {
80 if (source == null || !source.isShowing()) return;
81 popup.show(source, source.getWidth()/2, source.getHeight()/2);
82 }
83 return;
84 }
85 if (source == null || !source.isShowing()) return;
86 for (ImageryLayer layer : layers) {
87 JMenuItem layerMenu = layer.getOffsetMenuItem();
88 layerMenu.setText(layer.getName());
89 layerMenu.setIcon(layer.getIcon());
90 popup.add(layerMenu);
91 }
92 popup.show(source, source.getWidth()/2, source.getHeight()/2);
93 }
94 }
95
96 /**
97 * Compare ImageryInfo objects alphabetically by name.
98 *
99 * ImageryInfo objects are normally sorted by country code first
100 * (for the preferences). We don't want this in the imagery menu.
101 */
102 public static final Comparator<ImageryInfo> alphabeticImageryComparator =
103 (ii1, ii2) -> ii1.getName().toLowerCase(Locale.ENGLISH).compareTo(ii2.getName().toLowerCase(Locale.ENGLISH));
104
105 private final transient Action offsetAction = new AdjustImageryOffsetAction();
106
107 private final JMenuItem singleOffset = new JMenuItem(offsetAction);
108 private JMenuItem offsetMenuItem = singleOffset;
109 private final MapRectifierWMSmenuAction rectaction = new MapRectifierWMSmenuAction();
110
111 /**
112 * Constructs a new {@code ImageryMenu}.
113 * @param subMenu submenu in that contains plugin-managed additional imagery layers
114 */
115 public ImageryMenu(JMenu subMenu) {
116 /* I18N: mnemonic: I */
117 super(trc("menu", "Imagery"));
118 setupMenuScroller();
119 MainApplication.getLayerManager().addLayerChangeListener(this);
120 // build dynamically
121 addMenuListener(new MenuListener() {
122 @Override
123 public void menuSelected(MenuEvent e) {
124 refreshImageryMenu();
125 }
126
127 @Override
128 public void menuDeselected(MenuEvent e) {
129 // Do nothing
130 }
131
132 @Override
133 public void menuCanceled(MenuEvent e) {
134 // Do nothing
135 }
136 });
137 MainMenu.add(subMenu, rectaction);
138 }
139
140 private void setupMenuScroller() {
141 if (!GraphicsEnvironment.isHeadless()) {
142 MenuScroller.setScrollerFor(this, 150, 2);
143 }
144 }
145
146 /**
147 * For layers containing complex shapes, check that center is in one of its shapes (fix #7910)
148 * @param info layer info
149 * @param pos center
150 * @return {@code true} if center is in one of info shapes
151 */
152 private static boolean isPosInOneShapeIfAny(ImageryInfo info, LatLon pos) {
153 List<Shape> shapes = info.getBounds().getShapes();
154 return shapes == null || shapes.isEmpty() || shapes.stream().anyMatch(s -> s.contains(pos));
155 }
156
157 /**
158 * Refresh imagery menu.
159 *
160 * Outside this class only called in {@link ImageryPreference#initialize()}.
161 * (In order to have actions ready for the toolbar, see #8446.)
162 */
163 public void refreshImageryMenu() {
164 removeDynamicItems();
165
166 addDynamic(offsetMenuItem, null);
167 addDynamicSeparator();
168
169 // for each configured ImageryInfo, add a menu entry.
170 final List<ImageryInfo> savedLayers = new ArrayList<>(ImageryLayerInfo.instance.getLayers());
171 savedLayers.sort(alphabeticImageryComparator);
172 for (final ImageryInfo u : savedLayers) {
173 addDynamic(trackJosmAction(new AddImageryLayerAction(u)), null);
174 }
175
176 // list all imagery entries where the current map location is within the imagery bounds
177 if (MainApplication.isDisplayingMapView()) {
178 MapView mv = MainApplication.getMap().mapView;
179 LatLon pos = mv.getProjection().eastNorth2latlon(mv.getCenter());
180 final List<ImageryInfo> alreadyInUse = ImageryLayerInfo.instance.getLayers();
181 final List<ImageryInfo> inViewLayers = ImageryLayerInfo.instance.getDefaultLayers()
182 .stream().filter(i -> i.getBounds() != null && i.getBounds().contains(pos)
183 && !alreadyInUse.contains(i) && isPosInOneShapeIfAny(i, pos))
184 .sorted(alphabeticImageryComparator)
185 .collect(Collectors.toList());
186 if (!inViewLayers.isEmpty()) {
187 if (inViewLayers.stream().anyMatch(i -> i.getImageryCategory() == ImageryCategory.PHOTO)) {
188 addDynamicSeparator();
189 }
190 for (ImageryInfo i : inViewLayers) {
191 addDynamic(trackJosmAction(new AddImageryLayerAction(i)), i.getImageryCategory());
192 }
193 }
194 if (!dynamicNonPhotoItems.isEmpty()) {
195 addDynamicSeparator();
196 for (Entry<ImageryCategory, List<JMenuItem>> e : dynamicNonPhotoItems.entrySet()) {
197 ImageryCategory cat = e.getKey();
198 JMenuItem categoryMenu = new JMenu(cat.getDescription());
199 categoryMenu.setIcon(cat.getIcon(ImageSizes.MENU));
200 for (JMenuItem it : e.getValue()) {
201 categoryMenu.add(it);
202 }
203 dynamicNonPhotoMenus.add(add(categoryMenu));
204 }
205 }
206 }
207
208 addDynamicSeparator();
209 JMenu subMenu = MainApplication.getMenu().imagerySubMenu;
210 int heightUnrolled = 30*(getItemCount()+subMenu.getItemCount());
211 if (heightUnrolled < MainApplication.getMainPanel().getHeight()) {
212 // add all items of submenu if they will fit on screen
213 int n = subMenu.getItemCount();
214 for (int i = 0; i < n; i++) {
215 addDynamic(subMenu.getItem(i).getAction(), null);
216 }
217 } else {
218 // or add the submenu itself
219 addDynamic(subMenu, null);
220 }
221 }
222
223 private JMenuItem getNewOffsetMenu() {
224 Collection<ImageryLayer> layers = MainApplication.getLayerManager().getLayersOfType(ImageryLayer.class);
225 if (layers.isEmpty()) {
226 offsetAction.setEnabled(false);
227 return singleOffset;
228 }
229 offsetAction.setEnabled(true);
230 JMenu newMenu = new JMenu(trc("layer", "Offset"));
231 newMenu.setIcon(ImageProvider.get("mapmode", "adjustimg"));
232 newMenu.setAction(offsetAction);
233 if (layers.size() == 1)
234 return (JMenuItem) layers.iterator().next().getOffsetMenuItem(newMenu);
235 for (ImageryLayer layer : layers) {
236 JMenuItem layerMenu = layer.getOffsetMenuItem();
237 layerMenu.setText(layer.getName());
238 layerMenu.setIcon(layer.getIcon());
239 newMenu.add(layerMenu);
240 }
241 return newMenu;
242 }
243
244 /**
245 * Refresh offset menu item.
246 */
247 public void refreshOffsetMenu() {
248 offsetMenuItem = getNewOffsetMenu();
249 }
250
251 @Override
252 public void layerAdded(LayerAddEvent e) {
253 if (e.getAddedLayer() instanceof ImageryLayer) {
254 refreshOffsetMenu();
255 }
256 }
257
258 @Override
259 public void layerRemoving(LayerRemoveEvent e) {
260 if (e.getRemovedLayer() instanceof ImageryLayer) {
261 refreshOffsetMenu();
262 }
263 }
264
265 @Override
266 public void layerOrderChanged(LayerOrderChangeEvent e) {
267 refreshOffsetMenu();
268 }
269
270 /**
271 * List to store temporary "photo" menu items. They will be deleted
272 * (and possibly recreated) when refreshImageryMenu() is called.
273 */
274 private final List<Object> dynamicItems = new ArrayList<>(20);
275 /**
276 * Map to store temporary "not photo" menu items. They will be deleted
277 * (and possibly recreated) when refreshImageryMenu() is called.
278 */
279 private final Map<ImageryCategory, List<JMenuItem>> dynamicNonPhotoItems = new EnumMap<>(ImageryCategory.class);
280 /**
281 * List to store temporary "not photo" submenus. They will be deleted
282 * (and possibly recreated) when refreshImageryMenu() is called.
283 */
284 private final List<JMenuItem> dynamicNonPhotoMenus = new ArrayList<>(20);
285 private final List<JosmAction> dynJosmActions = new ArrayList<>(20);
286
287 /**
288 * Remove all the items in dynamic items collection
289 * @since 5803
290 */
291 private void removeDynamicItems() {
292 dynJosmActions.forEach(JosmAction::destroy);
293 dynJosmActions.clear();
294 dynamicItems.forEach(this::removeDynamicItem);
295 dynamicItems.clear();
296 dynamicNonPhotoMenus.forEach(this::removeDynamicItem);
297 dynamicItems.clear();
298 dynamicNonPhotoItems.clear();
299 }
300
301 private void removeDynamicItem(Object item) {
302 if (item instanceof JMenuItem) {
303 Optional.ofNullable(((JMenuItem) item).getAction()).ifPresent(MainApplication.getToolbar()::unregister);
304 remove((JMenuItem) item);
305 } else if (item instanceof MenuComponent) {
306 remove((MenuComponent) item);
307 } else if (item instanceof Component) {
308 remove((Component) item);
309 } else {
310 Logging.error("Unknown imagery menu item type: {0}", item);
311 }
312 }
313
314 private void addDynamicSeparator() {
315 JPopupMenu.Separator s = new JPopupMenu.Separator();
316 dynamicItems.add(s);
317 add(s);
318 }
319
320 private void addDynamic(Action a, ImageryCategory category) {
321 JMenuItem item = createActionComponent(a);
322 item.setAction(a);
323 doAddDynamic(item, category);
324 }
325
326 private void addDynamic(JMenuItem it, ImageryCategory category) {
327 doAddDynamic(it, category);
328 }
329
330 private void doAddDynamic(JMenuItem item, ImageryCategory category) {
331 if (category == null || category == ImageryCategory.PHOTO) {
332 dynamicItems.add(this.add(item));
333 } else {
334 dynamicNonPhotoItems.computeIfAbsent(category, x -> new ArrayList<>()).add(item);
335 }
336 }
337
338 private Action trackJosmAction(Action action) {
339 if (action instanceof JosmAction) {
340 dynJosmActions.add((JosmAction) action);
341 }
342 return action;
343 }
344
345}
Note: See TracBrowser for help on using the repository browser.