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