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