001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.Component;
008import java.awt.Rectangle;
009import java.awt.event.ActionEvent;
010import java.awt.event.ActionListener;
011import java.awt.event.KeyEvent;
012import java.awt.event.MouseEvent;
013import java.util.ArrayList;
014import java.util.Arrays;
015import java.util.Collection;
016import java.util.Collections;
017import java.util.Comparator;
018import java.util.HashSet;
019import java.util.LinkedList;
020import java.util.List;
021import java.util.Set;
022
023import javax.swing.AbstractAction;
024import javax.swing.AbstractListModel;
025import javax.swing.DefaultListSelectionModel;
026import javax.swing.JList;
027import javax.swing.JMenuItem;
028import javax.swing.JPopupMenu;
029import javax.swing.ListSelectionModel;
030import javax.swing.event.ListDataEvent;
031import javax.swing.event.ListDataListener;
032import javax.swing.event.ListSelectionEvent;
033import javax.swing.event.ListSelectionListener;
034
035import org.openstreetmap.josm.Main;
036import org.openstreetmap.josm.actions.AutoScaleAction;
037import org.openstreetmap.josm.actions.relation.DownloadSelectedIncompleteMembersAction;
038import org.openstreetmap.josm.actions.relation.EditRelationAction;
039import org.openstreetmap.josm.actions.relation.SelectInRelationListAction;
040import org.openstreetmap.josm.actions.search.SearchAction.SearchSetting;
041import org.openstreetmap.josm.data.SelectionChangedListener;
042import org.openstreetmap.josm.data.osm.Node;
043import org.openstreetmap.josm.data.osm.OsmPrimitive;
044import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator;
045import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
046import org.openstreetmap.josm.data.osm.Relation;
047import org.openstreetmap.josm.data.osm.Way;
048import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
049import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
050import org.openstreetmap.josm.data.osm.event.DataSetListener;
051import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
052import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
053import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
054import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
055import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
056import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
057import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
058import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
059import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
060import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
061import org.openstreetmap.josm.gui.DefaultNameFormatter;
062import org.openstreetmap.josm.gui.MapView;
063import org.openstreetmap.josm.gui.MapView.EditLayerChangeListener;
064import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
065import org.openstreetmap.josm.gui.PopupMenuHandler;
066import org.openstreetmap.josm.gui.SideButton;
067import org.openstreetmap.josm.gui.layer.OsmDataLayer;
068import org.openstreetmap.josm.gui.util.GuiHelper;
069import org.openstreetmap.josm.gui.util.HighlightHelper;
070import org.openstreetmap.josm.gui.widgets.ListPopupMenu;
071import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
072import org.openstreetmap.josm.tools.ImageProvider;
073import org.openstreetmap.josm.tools.InputMapUtils;
074import org.openstreetmap.josm.tools.Shortcut;
075import org.openstreetmap.josm.tools.SubclassFilteredCollection;
076
077/**
078 * A small tool dialog for displaying the current selection.
079 * @since 8
080 */
081public class SelectionListDialog extends ToggleDialog  {
082    private JList lstPrimitives;
083    private DefaultListSelectionModel selectionModel  = new DefaultListSelectionModel();
084    private SelectionListModel model = new SelectionListModel(selectionModel);
085
086    private SelectAction actSelect = new SelectAction();
087    private SearchAction actSearch = new SearchAction();
088    private ZoomToJOSMSelectionAction actZoomToJOSMSelection = new ZoomToJOSMSelectionAction();
089    private ZoomToListSelection actZoomToListSelection = new ZoomToListSelection();
090    private SelectInRelationListAction actSetRelationSelection = new SelectInRelationListAction();
091    private EditRelationAction actEditRelationSelection = new EditRelationAction();
092    private DownloadSelectedIncompleteMembersAction actDownloadSelectedIncompleteMembers = new DownloadSelectedIncompleteMembersAction();
093
094    /** the popup menu and its handler */
095    private final ListPopupMenu popupMenu;
096    private final PopupMenuHandler popupMenuHandler;
097
098    /**
099     * Builds the content panel for this dialog
100     */
101    protected void buildContentPanel() {
102        lstPrimitives = new JList(model);
103        lstPrimitives.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
104        lstPrimitives.setSelectionModel(selectionModel);
105        lstPrimitives.setCellRenderer(new OsmPrimitivRenderer());
106        lstPrimitives.setTransferHandler(null); // Fix #6290. Drag & Drop is not supported anyway and Copy/Paste is better propagated to main window
107
108        // the select action
109        final SideButton selectButton = new SideButton(actSelect);
110        lstPrimitives.getSelectionModel().addListSelectionListener(actSelect);
111        selectButton.createArrow(new ActionListener() {
112            @Override
113            public void actionPerformed(ActionEvent e) {
114                SelectionHistoryPopup.launch(selectButton, model.getSelectionHistory());
115            }
116        });
117
118        // the search button
119        final SideButton searchButton = new SideButton(actSearch);
120        searchButton.createArrow(new ActionListener() {
121            @Override
122            public void actionPerformed(ActionEvent e) {
123                SearchPopupMenu.launch(searchButton);
124            }
125        });
126
127        createLayout(lstPrimitives, true, Arrays.asList(new SideButton[] {
128            selectButton, searchButton
129        }));
130    }
131
132    /**
133     * Constructs a new {@code SelectionListDialog}.
134     */
135    public SelectionListDialog() {
136        super(tr("Selection"), "selectionlist", tr("Open a selection list window."),
137                Shortcut.registerShortcut("subwindow:selection", tr("Toggle: {0}",
138                tr("Current Selection")), KeyEvent.VK_T, Shortcut.ALT_SHIFT),
139                150, // default height
140                true // default is "show dialog"
141        );
142
143        buildContentPanel();
144        model.addListDataListener(new TitleUpdater());
145        model.addListDataListener(actZoomToJOSMSelection);
146
147        popupMenu = new ListPopupMenu(lstPrimitives);
148        popupMenuHandler = setupPopupMenuHandler();
149
150        lstPrimitives.addListSelectionListener(new ListSelectionListener() {
151            @Override
152            public void valueChanged(ListSelectionEvent e) {
153                actZoomToListSelection.valueChanged(e);
154                popupMenuHandler.setPrimitives(model.getSelected());
155            }
156        });
157
158        lstPrimitives.addMouseListener(new MouseEventHandler());
159
160        InputMapUtils.addEnterAction(lstPrimitives, actZoomToListSelection);
161    }
162
163    @Override
164    public void showNotify() {
165        MapView.addEditLayerChangeListener(model);
166        SelectionEventManager.getInstance().addSelectionListener(model, FireMode.IN_EDT_CONSOLIDATED);
167        DatasetEventManager.getInstance().addDatasetListener(model, FireMode.IN_EDT);
168        MapView.addEditLayerChangeListener(actSearch);
169        // editLayerChanged also gets the selection history of the level
170        OsmDataLayer editLayer = Main.main.getEditLayer();
171        model.editLayerChanged(null, editLayer);
172        if (editLayer != null) {
173            model.setJOSMSelection(editLayer.data.getAllSelected());
174        }
175        actSearch.updateEnabledState();
176    }
177
178    @Override
179    public void hideNotify() {
180        MapView.removeEditLayerChangeListener(actSearch);
181        MapView.removeEditLayerChangeListener(model);
182        SelectionEventManager.getInstance().removeSelectionListener(model);
183        DatasetEventManager.getInstance().removeDatasetListener(model);
184    }
185
186    /**
187     * Responds to double clicks on the list of selected objects and launches the popup menu
188     */
189    class MouseEventHandler extends PopupMenuLauncher {
190        private final HighlightHelper helper = new HighlightHelper();
191        private boolean highlightEnabled = Main.pref.getBoolean("draw.target-highlight", true);
192        public MouseEventHandler() {
193            super(popupMenu);
194        }
195
196        @Override
197        public void mouseClicked(MouseEvent e) {
198            int idx = lstPrimitives.locationToIndex(e.getPoint());
199            if (idx < 0) return;
200            if (isDoubleClick(e)) {
201                OsmDataLayer layer = Main.main.getEditLayer();
202                if (layer == null) return;
203                layer.data.setSelected(Collections.singleton((OsmPrimitive)model.getElementAt(idx)));
204            } else if (highlightEnabled && Main.isDisplayingMapView()) {
205                if (helper.highlightOnly((OsmPrimitive)model.getElementAt(idx))) {
206                    Main.map.mapView.repaint();
207                }
208            }
209        }
210
211        @Override
212        public void mouseExited(MouseEvent me) {
213            if (highlightEnabled) helper.clear();
214            super.mouseExited(me);
215        }
216    }
217
218    private PopupMenuHandler setupPopupMenuHandler() {
219        PopupMenuHandler handler = new PopupMenuHandler(popupMenu);
220        handler.addAction(actZoomToJOSMSelection);
221        handler.addAction(actZoomToListSelection);
222        handler.addSeparator();
223        handler.addAction(actSetRelationSelection);
224        handler.addAction(actEditRelationSelection);
225        handler.addSeparator();
226        handler.addAction(actDownloadSelectedIncompleteMembers);
227        return handler;
228    }
229
230    /**
231     * Replies the popup menu handler.
232     * @return The popup menu handler
233     */
234    public PopupMenuHandler getPopupMenuHandler() {
235        return popupMenuHandler;
236    }
237
238    /**
239     * Replies the selected OSM primitives.
240     * @return The selected OSM primitives
241     */
242    public Collection<OsmPrimitive> getSelectedPrimitives() {
243        return model.getSelected();
244    }
245
246    /**
247     * Updates the dialog title with a summary of the current JOSM selection
248     */
249    class TitleUpdater implements ListDataListener {
250        protected void updateTitle() {
251            setTitle(model.getJOSMSelectionSummary());
252        }
253
254        @Override
255        public void contentsChanged(ListDataEvent e) {
256            updateTitle();
257        }
258
259        @Override
260        public void intervalAdded(ListDataEvent e) {
261            updateTitle();
262        }
263
264        @Override
265        public void intervalRemoved(ListDataEvent e) {
266            updateTitle();
267        }
268    }
269
270    /**
271     * Launches the search dialog
272     */
273    static class SearchAction extends AbstractAction implements EditLayerChangeListener {
274        public SearchAction() {
275            putValue(NAME, tr("Search"));
276            putValue(SHORT_DESCRIPTION,   tr("Search for objects"));
277            putValue(SMALL_ICON, ImageProvider.get("dialogs","select"));
278            updateEnabledState();
279        }
280
281        @Override
282        public void actionPerformed(ActionEvent e) {
283            if (!isEnabled()) return;
284            org.openstreetmap.josm.actions.search.SearchAction.search();
285        }
286
287        public void updateEnabledState() {
288            setEnabled(Main.main != null && Main.main.hasEditLayer());
289        }
290
291        @Override
292        public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) {
293            updateEnabledState();
294        }
295    }
296
297    /**
298     * Sets the current JOSM selection to the OSM primitives selected in the list
299     * of this dialog
300     */
301    class SelectAction extends AbstractAction implements ListSelectionListener {
302        public SelectAction() {
303            putValue(NAME, tr("Select"));
304            putValue(SHORT_DESCRIPTION,  tr("Set the selected elements on the map to the selected items in the list above."));
305            putValue(SMALL_ICON, ImageProvider.get("dialogs","select"));
306            updateEnabledState();
307        }
308
309        @Override
310        public void actionPerformed(ActionEvent e) {
311            Collection<OsmPrimitive> sel = model.getSelected();
312            if (sel.isEmpty())return;
313            OsmDataLayer editLayer = Main.main.getEditLayer();
314            if (editLayer == null) return;
315            editLayer.data.setSelected(sel);
316        }
317
318        public void updateEnabledState() {
319            setEnabled(!model.getSelected().isEmpty());
320        }
321
322        @Override
323        public void valueChanged(ListSelectionEvent e) {
324            updateEnabledState();
325        }
326    }
327
328    /**
329     * The action for zooming to the primitives in the current JOSM selection
330     *
331     */
332    class ZoomToJOSMSelectionAction extends AbstractAction implements ListDataListener {
333
334        public ZoomToJOSMSelectionAction() {
335            putValue(NAME,tr("Zoom to selection"));
336            putValue(SHORT_DESCRIPTION, tr("Zoom to selection"));
337            putValue(SMALL_ICON, ImageProvider.get("dialogs/autoscale", "selection"));
338            updateEnabledState();
339        }
340
341        @Override
342        public void actionPerformed(ActionEvent e) {
343            AutoScaleAction.autoScale("selection");
344        }
345
346        public void updateEnabledState() {
347            setEnabled(model.getSize() > 0);
348        }
349
350        @Override
351        public void contentsChanged(ListDataEvent e) {
352            updateEnabledState();
353        }
354
355        @Override
356        public void intervalAdded(ListDataEvent e) {
357            updateEnabledState();
358        }
359
360        @Override
361        public void intervalRemoved(ListDataEvent e) {
362            updateEnabledState();
363        }
364    }
365
366    /**
367     * The action for zooming to the primitives which are currently selected in
368     * the list displaying the JOSM selection
369     *
370     */
371    class ZoomToListSelection extends AbstractAction implements ListSelectionListener{
372        public ZoomToListSelection() {
373            putValue(NAME, tr("Zoom to selected element(s)"));
374            putValue(SHORT_DESCRIPTION, tr("Zoom to selected element(s)"));
375            putValue(SMALL_ICON, ImageProvider.get("dialogs/autoscale", "selection"));
376            updateEnabledState();
377        }
378
379        @Override
380        public void actionPerformed(ActionEvent e) {
381            BoundingXYVisitor box = new BoundingXYVisitor();
382            Collection<OsmPrimitive> sel = model.getSelected();
383            if (sel.isEmpty()) return;
384            box.computeBoundingBox(sel);
385            if (box.getBounds() == null)
386                return;
387            box.enlargeBoundingBox();
388            Main.map.mapView.recalculateCenterScale(box);
389        }
390
391        public void updateEnabledState() {
392            setEnabled(!model.getSelected().isEmpty());
393        }
394
395        @Override
396        public void valueChanged(ListSelectionEvent e) {
397            updateEnabledState();
398        }
399    }
400
401    /**
402     * The list model for the list of OSM primitives in the current JOSM selection.
403     *
404     * The model also maintains a history of the last {@link SelectionListModel#SELECTION_HISTORY_SIZE}
405     * JOSM selection.
406     *
407     */
408    static private class SelectionListModel extends AbstractListModel implements EditLayerChangeListener, SelectionChangedListener, DataSetListener{
409
410        private static final int SELECTION_HISTORY_SIZE = 10;
411
412        // Variable to store history from currentDataSet()
413        private LinkedList<Collection<? extends OsmPrimitive>> history;
414        private final List<OsmPrimitive> selection = new ArrayList<OsmPrimitive>();
415        private DefaultListSelectionModel selectionModel;
416
417        /**
418         * Constructor
419         * @param selectionModel the selection model used in the list
420         */
421        public SelectionListModel(DefaultListSelectionModel selectionModel) {
422            this.selectionModel = selectionModel;
423        }
424
425        /**
426         * Replies a summary of the current JOSM selection
427         *
428         * @return a summary of the current JOSM selection
429         */
430        public String getJOSMSelectionSummary() {
431            if (selection.isEmpty()) return tr("Selection");
432            int numNodes = 0;
433            int numWays = 0;
434            int numRelations = 0;
435            for (OsmPrimitive p: selection) {
436                switch(p.getType()) {
437                case NODE: numNodes++; break;
438                case WAY: numWays++; break;
439                case RELATION: numRelations++; break;
440                }
441            }
442            return tr("Sel.: Rel.:{0} / Ways:{1} / Nodes:{2}", numRelations, numWays, numNodes);
443        }
444
445        /**
446         * Remembers a JOSM selection the history of JOSM selections
447         *
448         * @param selection the JOSM selection. Ignored if null or empty.
449         */
450        public void remember(Collection<? extends OsmPrimitive> selection) {
451            if (selection == null)return;
452            if (selection.isEmpty())return;
453            if (history == null) return;
454            if (history.isEmpty()) {
455                history.add(selection);
456                return;
457            }
458            if (history.getFirst().equals(selection)) return;
459            history.addFirst(selection);
460            for(int i = 1; i < history.size(); ++i) {
461                if(history.get(i).equals(selection)) {
462                    history.remove(i);
463                    break;
464                }
465            }
466            int maxsize = Main.pref.getInteger("select.history-size", SELECTION_HISTORY_SIZE);
467            while (history.size() > maxsize) {
468                history.removeLast();
469            }
470        }
471
472        /**
473         * Replies the history of JOSM selections
474         *
475         * @return history of JOSM selections
476         */
477        public List<Collection<? extends OsmPrimitive>> getSelectionHistory() {
478            return history;
479        }
480
481        @Override
482        public Object getElementAt(int index) {
483            return selection.get(index);
484        }
485
486        @Override
487        public int getSize() {
488            return selection.size();
489        }
490
491        /**
492         * Replies the collection of OSM primitives currently selected in the view
493         * of this model
494         *
495         * @return choosen elements in the view
496         */
497        public Collection<OsmPrimitive> getSelected() {
498            Set<OsmPrimitive> sel = new HashSet<OsmPrimitive>();
499            for(int i=0; i< getSize();i++) {
500                if (selectionModel.isSelectedIndex(i)) {
501                    sel.add(selection.get(i));
502                }
503            }
504            return sel;
505        }
506
507        /**
508         * Sets the OSM primitives to be selected in the view of this model
509         *
510         * @param sel the collection of primitives to select
511         */
512        public void setSelected(Collection<OsmPrimitive> sel) {
513            selectionModel.clearSelection();
514            if (sel == null) return;
515            for (OsmPrimitive p: sel){
516                int i = selection.indexOf(p);
517                if (i >= 0){
518                    selectionModel.addSelectionInterval(i, i);
519                }
520            }
521        }
522
523        @Override
524        protected void fireContentsChanged(Object source, int index0, int index1) {
525            Collection<OsmPrimitive> sel = getSelected();
526            super.fireContentsChanged(source, index0, index1);
527            setSelected(sel);
528        }
529
530        /**
531         * Sets the collection of currently selected OSM objects
532         *
533         * @param selection the collection of currently selected OSM objects
534         */
535        public void setJOSMSelection(final Collection<? extends OsmPrimitive> selection) {
536            this.selection.clear();
537            if (selection != null) {
538                this.selection.addAll(selection);
539                sort();
540            }
541            GuiHelper.runInEDTAndWait(new Runnable() {
542                @Override public void run() {
543                    fireContentsChanged(this, 0, getSize());
544                    if (selection != null) {
545                        remember(selection);
546                        Main.map.statusLine.setDist(new SubclassFilteredCollection<OsmPrimitive, Way>(selection, OsmPrimitive.wayPredicate));
547                    }
548                }
549            });
550        }
551
552        /**
553         * Triggers a refresh of the view for all primitives in {@code toUpdate}
554         * which are currently displayed in the view
555         *
556         * @param toUpdate the collection of primitives to update
557         */
558        public void update(Collection<? extends OsmPrimitive> toUpdate) {
559            if (toUpdate == null) return;
560            if (toUpdate.isEmpty()) return;
561            Collection<OsmPrimitive> sel = getSelected();
562            for (OsmPrimitive p: toUpdate){
563                int i = selection.indexOf(p);
564                if (i >= 0) {
565                    super.fireContentsChanged(this, i,i);
566                }
567            }
568            setSelected(sel);
569        }
570
571        /**
572         * Sorts the current elements in the selection
573         */
574        public void sort() {
575            if (this.selection.size()>Main.pref.getInteger("selection.no_sort_above",100000)) return;
576            if (this.selection.size()>Main.pref.getInteger("selection.fast_sort_above",10000)) {
577                Collections.sort(this.selection, new OsmPrimitiveQuickComparator());
578            } else {
579                Collections.sort(this.selection, new OsmPrimitiveComparator());
580            }
581        }
582
583        /* ------------------------------------------------------------------------ */
584        /* interface EditLayerChangeListener                                        */
585        /* ------------------------------------------------------------------------ */
586        @Override
587        public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) {
588            if (newLayer == null) {
589                setJOSMSelection(null);
590                history = null;
591            } else {
592                history = newLayer.data.getSelectionHistory();
593                setJOSMSelection(newLayer.data.getAllSelected());
594            }
595        }
596
597        /* ------------------------------------------------------------------------ */
598        /* interface SelectionChangeListener                                        */
599        /* ------------------------------------------------------------------------ */
600        @Override
601        public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
602            setJOSMSelection(newSelection);
603        }
604
605        /* ------------------------------------------------------------------------ */
606        /* interface DataSetListener                                                */
607        /* ------------------------------------------------------------------------ */
608        @Override
609        public void dataChanged(DataChangedEvent event) {
610            // refresh the whole list
611            fireContentsChanged(this, 0, getSize());
612        }
613
614        @Override
615        public void nodeMoved(NodeMovedEvent event) {
616            // may influence the display name of primitives, update the data
617            update(event.getPrimitives());
618        }
619
620        @Override
621        public void otherDatasetChange(AbstractDatasetChangedEvent event) {
622            // may influence the display name of primitives, update the data
623            update(event.getPrimitives());
624        }
625
626        @Override
627        public void relationMembersChanged(RelationMembersChangedEvent event) {
628            // may influence the display name of primitives, update the data
629            update(event.getPrimitives());
630        }
631
632        @Override
633        public void tagsChanged(TagsChangedEvent event) {
634            // may influence the display name of primitives, update the data
635            update(event.getPrimitives());
636        }
637
638        @Override
639        public void wayNodesChanged(WayNodesChangedEvent event) {
640            // may influence the display name of primitives, update the data
641            update(event.getPrimitives());
642        }
643
644        @Override
645        public void primitivesAdded(PrimitivesAddedEvent event) {/* ignored - handled by SelectionChangeListener */}
646        @Override
647        public void primitivesRemoved(PrimitivesRemovedEvent event) {/* ignored - handled by SelectionChangeListener*/}
648    }
649
650    /**
651     * A specialized {@link JMenuItem} for presenting one entry of the search history
652     *
653     * @author Jan Peter Stotz
654     */
655    protected static class SearchMenuItem extends JMenuItem implements ActionListener {
656        final protected SearchSetting s;
657
658        public SearchMenuItem(SearchSetting s) {
659            super(s.toString());
660            this.s = s;
661            addActionListener(this);
662        }
663
664        @Override
665        public void actionPerformed(ActionEvent e) {
666            org.openstreetmap.josm.actions.search.SearchAction.searchWithoutHistory(s);
667        }
668    }
669
670    /**
671     * The popup menu for the search history entries
672     *
673     */
674    protected static class SearchPopupMenu extends JPopupMenu {
675        static public void launch(Component parent) {
676            if (org.openstreetmap.josm.actions.search.SearchAction.getSearchHistory().isEmpty())
677                return;
678            JPopupMenu menu = new SearchPopupMenu();
679            Rectangle r = parent.getBounds();
680            menu.show(parent, r.x, r.y + r.height);
681        }
682
683        public SearchPopupMenu() {
684            for (SearchSetting ss: org.openstreetmap.josm.actions.search.SearchAction.getSearchHistory()) {
685                add(new SearchMenuItem(ss));
686            }
687        }
688    }
689
690    /**
691     * A specialized {@link JMenuItem} for presenting one entry of the selection history
692     *
693     * @author Jan Peter Stotz
694     */
695    protected static class SelectionMenuItem extends JMenuItem implements ActionListener {
696        final private DefaultNameFormatter df = DefaultNameFormatter.getInstance();
697        protected Collection<? extends OsmPrimitive> sel;
698
699        public SelectionMenuItem(Collection<? extends OsmPrimitive> sel) {
700            super();
701            this.sel = sel;
702            int ways = 0;
703            int nodes = 0;
704            int relations = 0;
705            for (OsmPrimitive o : sel) {
706                if (! o.isSelectable()) continue; // skip unselectable primitives
707                if (o instanceof Way) {
708                    ways++;
709                } else if (o instanceof Node) {
710                    nodes++;
711                } else if (o instanceof Relation) {
712                    relations++;
713                }
714            }
715            StringBuffer text = new StringBuffer();
716            if(ways != 0) {
717                text.append(text.length() > 0 ? ", " : "")
718                .append(trn("{0} way", "{0} ways", ways, ways));
719            }
720            if(nodes != 0) {
721                text.append(text.length() > 0 ? ", " : "")
722                .append(trn("{0} node", "{0} nodes", nodes, nodes));
723            }
724            if(relations != 0) {
725                text.append(text.length() > 0 ? ", " : "")
726                .append(trn("{0} relation", "{0} relations", relations, relations));
727            }
728            if(ways + nodes + relations == 0) {
729                text.append(tr("Unselectable now"));
730                this.sel=new ArrayList<OsmPrimitive>(); // empty selection
731            }
732            if(ways + nodes + relations == 1)
733            {
734                text.append(": ");
735                for(OsmPrimitive o : sel) {
736                    text.append(o.getDisplayName(df));
737                }
738                setText(text.toString());
739            } else {
740                setText(tr("Selection: {0}", text));
741            }
742            addActionListener(this);
743        }
744
745        @Override
746        public void actionPerformed(ActionEvent e) {
747            Main.main.getCurrentDataSet().setSelected(sel);
748        }
749    }
750
751    /**
752     * The popup menu for the JOSM selection history entries
753     */
754    protected static class SelectionHistoryPopup extends JPopupMenu {
755        static public void launch(Component parent, Collection<Collection<? extends OsmPrimitive>> history) {
756            if (history == null || history.isEmpty()) return;
757            JPopupMenu menu = new SelectionHistoryPopup(history);
758            Rectangle r = parent.getBounds();
759            menu.show(parent, r.x, r.y + r.height);
760        }
761
762        public SelectionHistoryPopup(Collection<Collection<? extends OsmPrimitive>> history) {
763            for (Collection<? extends OsmPrimitive> sel : history) {
764                add(new SelectionMenuItem(sel));
765            }
766        }
767    }
768
769    /** Quicker comparator, comparing just by type and ID's */
770    static private class OsmPrimitiveQuickComparator implements Comparator<OsmPrimitive> {
771
772        private int compareId(OsmPrimitive a, OsmPrimitive b) {
773            long id_a=a.getUniqueId();
774            long id_b=b.getUniqueId();
775            if (id_a<id_b) return -1;
776            if (id_a>id_b) return 1;
777            return 0;
778        }
779
780        private int compareType(OsmPrimitive a, OsmPrimitive b) {
781            // show ways before relations, then nodes
782            if (a.getType().equals(OsmPrimitiveType.WAY)) return -1;
783            if (a.getType().equals(OsmPrimitiveType.NODE)) return 1;
784            // a is a relation
785            if (b.getType().equals(OsmPrimitiveType.WAY)) return 1;
786            // b is a node
787            return -1;
788        }
789
790        @Override
791        public int compare(OsmPrimitive a, OsmPrimitive b) {
792            if (a.getType().equals(b.getType()))
793                return compareId(a, b);
794            return compareType(a, b);
795        }
796    }
797
798}