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.event.ActionEvent;
008import java.awt.event.KeyEvent;
009import java.awt.event.MouseAdapter;
010import java.awt.event.MouseEvent;
011import java.text.NumberFormat;
012import java.util.ArrayList;
013import java.util.Arrays;
014import java.util.Collection;
015import java.util.Collections;
016import java.util.HashMap;
017import java.util.HashSet;
018import java.util.Iterator;
019import java.util.LinkedList;
020import java.util.List;
021import java.util.Map;
022import java.util.Set;
023
024import javax.swing.AbstractAction;
025import javax.swing.JTable;
026import javax.swing.ListSelectionModel;
027import javax.swing.event.ListSelectionEvent;
028import javax.swing.event.ListSelectionListener;
029import javax.swing.table.DefaultTableModel;
030
031import org.openstreetmap.josm.Main;
032import org.openstreetmap.josm.actions.AbstractInfoAction;
033import org.openstreetmap.josm.data.SelectionChangedListener;
034import org.openstreetmap.josm.data.osm.DataSet;
035import org.openstreetmap.josm.data.osm.OsmPrimitive;
036import org.openstreetmap.josm.data.osm.User;
037import org.openstreetmap.josm.gui.MapView;
038import org.openstreetmap.josm.gui.SideButton;
039import org.openstreetmap.josm.gui.layer.Layer;
040import org.openstreetmap.josm.gui.layer.OsmDataLayer;
041import org.openstreetmap.josm.gui.util.GuiHelper;
042import org.openstreetmap.josm.tools.ImageProvider;
043import org.openstreetmap.josm.tools.OpenBrowser;
044import org.openstreetmap.josm.tools.Shortcut;
045import org.openstreetmap.josm.tools.Utils;
046
047/**
048 * Displays a dialog with all users who have last edited something in the
049 * selection area, along with the number of objects.
050 *
051 */
052public class UserListDialog extends ToggleDialog implements SelectionChangedListener, MapView.LayerChangeListener {
053
054    /**
055     * The display list.
056     */
057    private JTable userTable;
058    private UserTableModel model;
059    private SelectUsersPrimitivesAction selectionUsersPrimitivesAction;
060    private ShowUserInfoAction showUserInfoAction;
061
062    /**
063     * Constructs a new {@code UserListDialog}.
064     */
065    public UserListDialog() {
066        super(tr("Authors"), "userlist", tr("Open a list of people working on the selected objects."),
067                Shortcut.registerShortcut("subwindow:authors", tr("Toggle: {0}", tr("Authors")), KeyEvent.VK_A, Shortcut.ALT_SHIFT), 150);
068        build();
069    }
070
071    @Override
072    public void showNotify() {
073        DataSet.addSelectionListener(this);
074        MapView.addLayerChangeListener(this);
075    }
076
077    @Override
078    public void hideNotify() {
079        MapView.removeLayerChangeListener(this);
080        DataSet.removeSelectionListener(this);
081    }
082
083    protected void build() {
084        model = new UserTableModel();
085        userTable = new JTable(model);
086        userTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
087        userTable.addMouseListener(new DoubleClickAdapter());
088
089        // -- select users primitives action
090        //
091        selectionUsersPrimitivesAction = new SelectUsersPrimitivesAction();
092        userTable.getSelectionModel().addListSelectionListener(selectionUsersPrimitivesAction);
093
094        // -- info action
095        //
096        showUserInfoAction = new ShowUserInfoAction();
097        userTable.getSelectionModel().addListSelectionListener(showUserInfoAction);
098
099        createLayout(userTable, true, Arrays.asList(new SideButton[] {
100            new SideButton(selectionUsersPrimitivesAction),
101            new SideButton(showUserInfoAction)
102        }));
103    }
104
105    /**
106     * Called when the selection in the dataset changed.
107     * @param newSelection The new selection array.
108     */
109    @Override
110    public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
111        refresh(newSelection);
112    }
113
114    @Override
115    public void activeLayerChange(Layer oldLayer, Layer newLayer) {
116        if (newLayer instanceof OsmDataLayer) {
117            refresh(((OsmDataLayer) newLayer).data.getAllSelected());
118        } else {
119            refresh(null);
120        }
121    }
122
123    @Override
124    public void layerAdded(Layer newLayer) {
125        // do nothing
126    }
127
128    @Override
129    public void layerRemoved(Layer oldLayer) {
130        // do nothing
131    }
132
133    /**
134     * Refreshes user list from given collection of OSM primitives.
135     * @param fromPrimitives OSM primitives to fetch users from
136     */
137    public void refresh(Collection<? extends OsmPrimitive> fromPrimitives) {
138        model.populate(fromPrimitives);
139        GuiHelper.runInEDT(new Runnable() {
140            @Override
141            public void run() {
142                if (model.getRowCount() != 0) {
143                    setTitle(trn("{0} Author", "{0} Authors", model.getRowCount(), model.getRowCount()));
144                } else {
145                    setTitle(tr("Authors"));
146                }
147            }
148        });
149    }
150
151    @Override
152    public void showDialog() {
153        super.showDialog();
154        Layer layer = Main.main.getActiveLayer();
155        if (layer instanceof OsmDataLayer) {
156            refresh(((OsmDataLayer) layer).data.getAllSelected());
157        }
158    }
159
160    class SelectUsersPrimitivesAction extends AbstractAction implements ListSelectionListener {
161
162        /**
163         * Constructs a new {@code SelectUsersPrimitivesAction}.
164         */
165        SelectUsersPrimitivesAction() {
166            putValue(NAME, tr("Select"));
167            putValue(SHORT_DESCRIPTION, tr("Select objects submitted by this user"));
168            putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
169            updateEnabledState();
170        }
171
172        public void select() {
173            int[] indexes = userTable.getSelectedRows();
174            if (indexes == null || indexes.length == 0)
175                return;
176            model.selectPrimitivesOwnedBy(userTable.getSelectedRows());
177        }
178
179        @Override
180        public void actionPerformed(ActionEvent e) {
181            select();
182        }
183
184        protected void updateEnabledState() {
185            setEnabled(userTable != null && userTable.getSelectedRowCount() > 0);
186        }
187
188        @Override
189        public void valueChanged(ListSelectionEvent e) {
190            updateEnabledState();
191        }
192    }
193
194    /**
195     * Action for launching the info page of a user.
196     */
197    class ShowUserInfoAction extends AbstractInfoAction implements ListSelectionListener {
198
199        ShowUserInfoAction() {
200            super(false);
201            putValue(NAME, tr("Show info"));
202            putValue(SHORT_DESCRIPTION, tr("Launches a browser with information about the user"));
203            putValue(SMALL_ICON, ImageProvider.get("help/internet"));
204            updateEnabledState();
205        }
206
207        @Override
208        public void actionPerformed(ActionEvent e) {
209            int[] rows = userTable.getSelectedRows();
210            if (rows == null || rows.length == 0)
211                return;
212            List<User> users = model.getSelectedUsers(rows);
213            if (users.isEmpty())
214                return;
215            if (users.size() > 10) {
216                Main.warn(tr("Only launching info browsers for the first {0} of {1} selected users", 10, users.size()));
217            }
218            int num = Math.min(10, users.size());
219            Iterator<User> it = users.iterator();
220            while (it.hasNext() && num > 0) {
221                String url = createInfoUrl(it.next());
222                if (url == null) {
223                    break;
224                }
225                OpenBrowser.displayUrl(url);
226                num--;
227            }
228        }
229
230        @Override
231        protected String createInfoUrl(Object infoObject) {
232            if (infoObject instanceof User) {
233                User user = (User) infoObject;
234                return Main.getBaseUserUrl() + '/' + Utils.encodeUrl(user.getName()).replaceAll("\\+", "%20");
235            } else {
236                return null;
237            }
238        }
239
240        @Override
241        protected void updateEnabledState() {
242            setEnabled(userTable != null && userTable.getSelectedRowCount() > 0);
243        }
244
245        @Override
246        public void valueChanged(ListSelectionEvent e) {
247            updateEnabledState();
248        }
249    }
250
251    class DoubleClickAdapter extends MouseAdapter {
252        @Override
253        public void mouseClicked(MouseEvent e) {
254            if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
255                selectionUsersPrimitivesAction.select();
256            }
257        }
258    }
259
260    /**
261     * Action for selecting the primitives contributed by the currently selected users.
262     *
263     */
264    private static class UserInfo implements Comparable<UserInfo> {
265        public final User user;
266        public final int count;
267        public final double percent;
268
269        UserInfo(User user, int count, double percent) {
270            this.user = user;
271            this.count = count;
272            this.percent = percent;
273        }
274
275        @Override
276        public int compareTo(UserInfo o) {
277            if (count < o.count)
278                return 1;
279            if (count > o.count)
280                return -1;
281            if (user == null || user.getName() == null)
282                return 1;
283            if (o.user == null || o.user.getName() == null)
284                return -1;
285            return user.getName().compareTo(o.user.getName());
286        }
287
288        public String getName() {
289            if (user == null)
290                return tr("<new object>");
291            return user.getName();
292        }
293    }
294
295    /**
296     * The table model for the users
297     *
298     */
299    static class UserTableModel extends DefaultTableModel {
300        private final transient List<UserInfo> data;
301
302        UserTableModel() {
303            setColumnIdentifiers(new String[]{tr("Author"), tr("# Objects"), "%"});
304            data = new ArrayList<>();
305        }
306
307        protected Map<User, Integer> computeStatistics(Collection<? extends OsmPrimitive> primitives) {
308            Map<User, Integer> ret = new HashMap<>();
309            if (primitives == null || primitives.isEmpty())
310                return ret;
311            for (OsmPrimitive primitive: primitives) {
312                if (ret.containsKey(primitive.getUser())) {
313                    ret.put(primitive.getUser(), ret.get(primitive.getUser()) + 1);
314                } else {
315                    ret.put(primitive.getUser(), 1);
316                }
317            }
318            return ret;
319        }
320
321        public void populate(Collection<? extends OsmPrimitive> primitives) {
322            Map<User, Integer> statistics = computeStatistics(primitives);
323            data.clear();
324            if (primitives != null) {
325                for (Map.Entry<User, Integer> entry: statistics.entrySet()) {
326                    data.add(new UserInfo(entry.getKey(), entry.getValue(), (double) entry.getValue() /  (double) primitives.size()));
327                }
328            }
329            Collections.sort(data);
330            GuiHelper.runInEDTAndWait(new Runnable() {
331                @Override
332                public void run() {
333                    fireTableDataChanged();
334                }
335            });
336        }
337
338        @Override
339        public int getRowCount() {
340            if (data == null)
341                return 0;
342            return data.size();
343        }
344
345        @Override
346        public Object getValueAt(int row, int column) {
347            UserInfo info = data.get(row);
348            switch(column) {
349            case 0: /* author */ return info.getName() == null ? "" : info.getName();
350            case 1: /* count */ return info.count;
351            case 2: /* percent */ return NumberFormat.getPercentInstance().format(info.percent);
352            default: return null;
353            }
354        }
355
356        @Override
357        public boolean isCellEditable(int row, int column) {
358            return false;
359        }
360
361        public void selectPrimitivesOwnedBy(int[] rows) {
362            Set<User> users = new HashSet<>();
363            for (int index: rows) {
364                users.add(data.get(index).user);
365            }
366            Collection<OsmPrimitive> selected = Main.main.getCurrentDataSet().getAllSelected();
367            Collection<OsmPrimitive> byUser = new LinkedList<>();
368            for (OsmPrimitive p : selected) {
369                if (users.contains(p.getUser())) {
370                    byUser.add(p);
371                }
372            }
373            Main.main.getCurrentDataSet().setSelected(byUser);
374        }
375
376        public List<User> getSelectedUsers(int[] rows) {
377            List<User> ret = new LinkedList<>();
378            if (rows == null || rows.length == 0)
379                return ret;
380            for (int row: rows) {
381                if (data.get(row).user == null) {
382                    continue;
383                }
384                ret.add(data.get(row).user);
385            }
386            return ret;
387        }
388    }
389}