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

Last change on this file since 15835 was 15521, checked in by Don-vip, 4 years ago

fix #18313 - proper toolbar cleanup when JOSM actions are destroyed

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