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.io.UnsupportedEncodingException;
012import java.net.URLEncoder;
013import java.text.NumberFormat;
014import java.util.ArrayList;
015import java.util.Arrays;
016import java.util.Collection;
017import java.util.Collections;
018import java.util.HashMap;
019import java.util.HashSet;
020import java.util.Iterator;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025
026import javax.swing.AbstractAction;
027import javax.swing.JOptionPane;
028import javax.swing.JTable;
029import javax.swing.ListSelectionModel;
030import javax.swing.event.ListSelectionEvent;
031import javax.swing.event.ListSelectionListener;
032import javax.swing.table.DefaultTableModel;
033
034import org.openstreetmap.josm.Main;
035import org.openstreetmap.josm.actions.AbstractInfoAction;
036import org.openstreetmap.josm.data.SelectionChangedListener;
037import org.openstreetmap.josm.data.osm.DataSet;
038import org.openstreetmap.josm.data.osm.OsmPrimitive;
039import org.openstreetmap.josm.data.osm.User;
040import org.openstreetmap.josm.gui.MapView;
041import org.openstreetmap.josm.gui.SideButton;
042import org.openstreetmap.josm.gui.layer.Layer;
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.OpenBrowser;
047import org.openstreetmap.josm.tools.Shortcut;
048
049/**
050 * Displays a dialog with all users who have last edited something in the
051 * selection area, along with the number of objects.
052 *
053 */
054public class UserListDialog extends ToggleDialog implements SelectionChangedListener, MapView.LayerChangeListener {
055
056    /**
057     * The display list.
058     */
059    private JTable userTable;
060    private UserTableModel model;
061    private SelectUsersPrimitivesAction selectionUsersPrimitivesAction;
062    private ShowUserInfoAction showUserInfoAction;
063
064    public UserListDialog() {
065        super(tr("Authors"), "userlist", tr("Open a list of people working on the selected objects."),
066                Shortcut.registerShortcut("subwindow:authors", tr("Toggle: {0}", tr("Authors")), KeyEvent.VK_A, Shortcut.ALT_SHIFT), 150);
067
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    public void refresh(Collection<? extends OsmPrimitive> fromPrimitives) {
134        model.populate(fromPrimitives);
135        GuiHelper.runInEDT(new Runnable() {
136            @Override
137            public void run() {
138                if (model.getRowCount() != 0) {
139                    setTitle(trn("{0} Author", "{0} Authors", model.getRowCount() , model.getRowCount()));
140                } else {
141                    setTitle(tr("Authors"));
142                }
143            }
144        });
145    }
146
147    @Override
148    public void showDialog() {
149        super.showDialog();
150        Layer layer = Main.main.getActiveLayer();
151        if (layer instanceof OsmDataLayer) {
152            refresh(((OsmDataLayer)layer).data.getAllSelected());
153        }
154
155    }
156
157    class SelectUsersPrimitivesAction extends AbstractAction implements ListSelectionListener{
158        public SelectUsersPrimitivesAction() {
159            putValue(NAME, tr("Select"));
160            putValue(SHORT_DESCRIPTION, tr("Select objects submitted by this user"));
161            putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
162            updateEnabledState();
163        }
164
165        public void select() {
166            int[] indexes = userTable.getSelectedRows();
167            if (indexes == null || indexes.length == 0) 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        public ShowUserInfoAction() {
192            super(false);
193            putValue(NAME, tr("Show info"));
194            putValue(SHORT_DESCRIPTION, tr("Launches a browser with information about the user"));
195            putValue(SMALL_ICON, ImageProvider.get("about"));
196            updateEnabledState();
197        }
198
199        @Override
200        public void actionPerformed(ActionEvent e) {
201            int[] rows = userTable.getSelectedRows();
202            if (rows == null || rows.length == 0) return;
203            List<User> users = model.getSelectedUsers(rows);
204            if (users.isEmpty()) return;
205            if (users.size() > 10) {
206                Main.warn(tr("Only launching info browsers for the first {0} of {1} selected users", 10, users.size()));
207            }
208            int num = Math.min(10, users.size());
209            Iterator<User> it = users.iterator();
210            while(it.hasNext() && num > 0) {
211                String url = createInfoUrl(it.next());
212                if (url == null) {
213                    break;
214                }
215                OpenBrowser.displayUrl(url);
216                num--;
217            }
218        }
219
220        @Override
221        protected String createInfoUrl(Object infoObject) {
222            User user = (User)infoObject;
223            try {
224                return getBaseUserUrl() + "/" + URLEncoder.encode(user.getName(), "UTF-8").replaceAll("\\+", "%20");
225            } catch(UnsupportedEncodingException e) {
226                Main.error(e);
227                JOptionPane.showMessageDialog(
228                        Main.parent,
229                        tr("<html>Failed to create an URL because the encoding ''{0}''<br>"
230                                + "was missing on this system.</html>", "UTF-8"),
231                                tr("Missing encoding"),
232                                JOptionPane.ERROR_MESSAGE
233                );
234                return null;
235            }
236        }
237
238        @Override
239        protected void updateEnabledState() {
240            setEnabled(userTable != null && userTable.getSelectedRowCount() > 0);
241        }
242
243        @Override
244        public void valueChanged(ListSelectionEvent e) {
245            updateEnabledState();
246        }
247    }
248
249    class DoubleClickAdapter extends MouseAdapter {
250        @Override
251        public void mouseClicked(MouseEvent e) {
252            if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount()==2) {
253                selectionUsersPrimitivesAction.select();
254            }
255        }
256    }
257
258    /**
259     * Action for selecting the primitives contributed by the currently selected
260     * users.
261     *
262     */
263    private static class UserInfo implements Comparable<UserInfo> {
264        public User user;
265        public int count;
266        public double percent;
267        UserInfo(User user, int count, double percent) {
268            this.user=user;
269            this.count=count;
270            this.percent = percent;
271        }
272        @Override
273        public int compareTo(UserInfo o) {
274            if (count < o.count) return 1;
275            if (count > o.count) return -1;
276            if (user== null || user.getName() == null) return 1;
277            if (o.user == null || o.user.getName() == null) return -1;
278            return user.getName().compareTo(o.user.getName());
279        }
280
281        public String getName() {
282            if (user == null)
283                return tr("<new object>");
284            return user.getName();
285        }
286    }
287
288    /**
289     * The table model for the users
290     *
291     */
292    static class UserTableModel extends DefaultTableModel {
293        private List<UserInfo> data;
294
295        public UserTableModel() {
296            setColumnIdentifiers(new String[]{tr("Author"),tr("# Objects"),"%"});
297            data = new ArrayList<>();
298        }
299
300        protected Map<User, Integer> computeStatistics(Collection<? extends OsmPrimitive> primitives) {
301            HashMap<User, Integer> ret = new HashMap<>();
302            if (primitives == null || primitives.isEmpty()) 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) return 0;
333            return data.size();
334        }
335
336        @Override
337        public Object getValueAt(int row, int column) {
338            UserInfo info = data.get(row);
339            switch(column) {
340            case 0: /* author */ return info.getName() == null ? "" : info.getName();
341            case 1: /* count */ return info.count;
342            case 2: /* percent */ return NumberFormat.getPercentInstance().format(info.percent);
343            }
344            return null;
345        }
346
347        @Override
348        public boolean isCellEditable(int row, int column) {
349            return false;
350        }
351
352        public void selectPrimitivesOwnedBy(int [] rows) {
353            Set<User> users= new HashSet<>();
354            for (int index: rows) {
355                users.add(data.get(index).user);
356            }
357            Collection<OsmPrimitive> selected = Main.main.getCurrentDataSet().getAllSelected();
358            Collection<OsmPrimitive> byUser = new LinkedList<>();
359            for (OsmPrimitive p : selected) {
360                if (users.contains(p.getUser())) {
361                    byUser.add(p);
362                }
363            }
364            Main.main.getCurrentDataSet().setSelected(byUser);
365        }
366
367        public List<User> getSelectedUsers(int[] rows) {
368            LinkedList<User> ret = new LinkedList<>();
369            if (rows == null || rows.length == 0) return ret;
370            for (int row: rows) {
371                if (data.get(row).user == null) {
372                    continue;
373                }
374                ret.add(data.get(row).user);
375            }
376            return ret;
377        }
378    }
379}