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