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