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