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