001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.gui.dialogs;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    import static org.openstreetmap.josm.tools.I18n.trn;
006    
007    import java.awt.Component;
008    import java.awt.event.ActionEvent;
009    import java.awt.event.KeyEvent;
010    import java.awt.event.MouseAdapter;
011    import java.awt.event.MouseEvent;
012    import java.io.UnsupportedEncodingException;
013    import java.net.URLEncoder;
014    import java.text.NumberFormat;
015    import java.util.ArrayList;
016    import java.util.Arrays;
017    import java.util.Collection;
018    import java.util.Collections;
019    import java.util.HashMap;
020    import java.util.HashSet;
021    import java.util.Iterator;
022    import java.util.LinkedList;
023    import java.util.List;
024    import java.util.Map;
025    import java.util.Set;
026    
027    import javax.swing.AbstractAction;
028    import javax.swing.JLabel;
029    import javax.swing.JOptionPane;
030    import javax.swing.JTable;
031    import javax.swing.ListSelectionModel;
032    import javax.swing.event.ListSelectionEvent;
033    import javax.swing.event.ListSelectionListener;
034    import javax.swing.table.DefaultTableCellRenderer;
035    import javax.swing.table.DefaultTableModel;
036    import javax.swing.table.TableColumnModel;
037    
038    import org.openstreetmap.josm.Main;
039    import org.openstreetmap.josm.actions.AbstractInfoAction;
040    import org.openstreetmap.josm.data.SelectionChangedListener;
041    import org.openstreetmap.josm.data.osm.DataSet;
042    import org.openstreetmap.josm.data.osm.OsmPrimitive;
043    import org.openstreetmap.josm.data.osm.User;
044    import org.openstreetmap.josm.gui.MapView;
045    import org.openstreetmap.josm.gui.SideButton;
046    import org.openstreetmap.josm.gui.layer.Layer;
047    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
048    import org.openstreetmap.josm.gui.progress.ContributorTermsUpdateRunnable;
049    import org.openstreetmap.josm.tools.ImageProvider;
050    import org.openstreetmap.josm.tools.Shortcut;
051    
052    /**
053     * Displays a dialog with all users who have last edited something in the
054     * selection area, along with the number of objects.
055     *
056     */
057    public class UserListDialog extends ToggleDialog implements SelectionChangedListener, MapView.LayerChangeListener {
058    
059        /**
060         * The display list.
061         */
062        private JTable userTable;
063        private UserTableModel model;
064        private SelectUsersPrimitivesAction selectionUsersPrimitivesAction;
065        private ShowUserInfoAction showUserInfoAction;
066        private LoadRelicensingInformationAction loadRelicensingInformationAction;
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    
072            build();
073        }
074    
075        @Override
076        public void showNotify() {
077            DataSet.addSelectionListener(this);
078            MapView.addLayerChangeListener(this);
079        }
080    
081        @Override
082        public void hideNotify() {
083            MapView.removeLayerChangeListener(this);
084            DataSet.removeSelectionListener(this);
085        }
086    
087        protected void build() {
088            model = new UserTableModel();
089            userTable = new JTable(model);
090            userTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
091            userTable.addMouseListener(new DoubleClickAdapter());
092            TableColumnModel columnModel = userTable.getColumnModel();
093            columnModel.getColumn(3).setPreferredWidth(20);
094            columnModel.getColumn(3).setCellRenderer(new DefaultTableCellRenderer() {
095                @Override
096                public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
097                    // see http://download.oracle.com/javase/6/docs/api/javax/swing/table/DefaultTableCellRenderer.html#override
098                    // for why we don't use the label directly
099                    final JLabel renderLabel = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
100                    JLabel sourceLabel = (JLabel) value;
101                    renderLabel.setIcon(sourceLabel.getIcon());
102                    renderLabel.setText("");
103                    renderLabel.setToolTipText(sourceLabel.getToolTipText());
104                    return renderLabel;
105                }
106            });
107    
108            // -- select users primitives action
109            //
110            selectionUsersPrimitivesAction = new SelectUsersPrimitivesAction();
111            userTable.getSelectionModel().addListSelectionListener(selectionUsersPrimitivesAction);
112    
113            // -- info action
114            //
115            showUserInfoAction = new ShowUserInfoAction();
116            userTable.getSelectionModel().addListSelectionListener(showUserInfoAction);
117    
118            // -- load relicensing info action
119            loadRelicensingInformationAction = new LoadRelicensingInformationAction();
120    
121            createLayout(userTable, true, Arrays.asList(new SideButton[] {
122                new SideButton(selectionUsersPrimitivesAction),
123                new SideButton(showUserInfoAction),
124                new SideButton(loadRelicensingInformationAction)
125            }));
126        }
127    
128        /**
129         * Called when the selection in the dataset changed.
130         * @param newSelection The new selection array.
131         */
132        public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
133            refresh(newSelection);
134        }
135    
136        public void activeLayerChange(Layer oldLayer, Layer newLayer) {
137            if (newLayer instanceof OsmDataLayer) {
138                refresh(((OsmDataLayer) newLayer).data.getAllSelected());
139            } else {
140                refresh(null);
141            }
142        }
143    
144        public void layerAdded(Layer newLayer) {
145            // do nothing
146        }
147    
148        public void layerRemoved(Layer oldLayer) {
149            // do nothing
150        }
151    
152        public void refresh(Collection<? extends OsmPrimitive> fromPrimitives) {
153            model.populate(fromPrimitives);
154            if(model.getRowCount() != 0) {
155                setTitle(trn("{0} Author", "{0} Authors", model.getRowCount() , model.getRowCount()));
156            } else {
157                setTitle(tr("Authors"));
158            }
159        }
160    
161        @Override
162        public void showDialog() {
163            super.showDialog();
164            Main.worker.submit(new ContributorTermsUpdateRunnable());
165            Layer layer = Main.main.getActiveLayer();
166            if (layer instanceof OsmDataLayer) {
167                refresh(((OsmDataLayer)layer).data.getAllSelected());
168            }
169    
170        }
171    
172        class SelectUsersPrimitivesAction extends AbstractAction implements ListSelectionListener{
173            public SelectUsersPrimitivesAction() {
174                putValue(NAME, tr("Select"));
175                putValue(SHORT_DESCRIPTION, tr("Select objects submitted by this user"));
176                putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
177                updateEnabledState();
178            }
179    
180            public void select() {
181                int indexes[] = userTable.getSelectedRows();
182                if (indexes == null || indexes.length == 0) return;
183                model.selectPrimitivesOwnedBy(userTable.getSelectedRows());
184            }
185    
186            public void actionPerformed(ActionEvent e) {
187                select();
188            }
189    
190            protected void updateEnabledState() {
191                setEnabled(userTable != null && userTable.getSelectedRowCount() > 0);
192            }
193    
194            public void valueChanged(ListSelectionEvent e) {
195                updateEnabledState();
196            }
197        }
198    
199        /*
200         * Action for launching the info page of a user
201         */
202        class ShowUserInfoAction extends AbstractInfoAction implements ListSelectionListener {
203    
204            public ShowUserInfoAction() {
205                super(false);
206                putValue(NAME, tr("Show info"));
207                putValue(SHORT_DESCRIPTION, tr("Launches a browser with information about the user"));
208                putValue(SMALL_ICON, ImageProvider.get("about"));
209                updateEnabledState();
210            }
211    
212            @Override
213            public void actionPerformed(ActionEvent e) {
214                int rows[] = userTable.getSelectedRows();
215                if (rows == null || rows.length == 0) return;
216                List<User> users = model.getSelectedUsers(rows);
217                if (users.isEmpty()) return;
218                if (users.size() > 10) {
219                    System.out.println(tr("Warning: 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                    launchBrowser(url);
229                    num--;
230                }
231            }
232    
233            @Override
234            protected String createInfoUrl(Object infoObject) {
235                User user = (User)infoObject;
236                try {
237                    return getBaseUserUrl() + "/" + URLEncoder.encode(user.getName(), "UTF-8").replaceAll("\\+", "%20");
238                } catch(UnsupportedEncodingException e) {
239                    e.printStackTrace();
240                    JOptionPane.showMessageDialog(
241                            Main.parent,
242                            tr("<html>Failed to create an URL because the encoding ''{0}''<br>"
243                                    + "was missing on this system.</html>", "UTF-8"),
244                                    tr("Missing encoding"),
245                                    JOptionPane.ERROR_MESSAGE
246                    );
247                    return null;
248                }
249            }
250    
251            @Override
252            protected void updateEnabledState() {
253                setEnabled(userTable != null && userTable.getSelectedRowCount() > 0);
254            }
255    
256            public void valueChanged(ListSelectionEvent e) {
257                updateEnabledState();
258            }
259        }
260    
261        /*
262         */
263        class LoadRelicensingInformationAction extends AbstractAction {
264    
265            public LoadRelicensingInformationAction() {
266                super();
267                putValue(NAME, tr("Load CT"));
268                putValue(SHORT_DESCRIPTION, tr("Loads information about relicensing status from the server. Users having agreed to the new contributor terms will show a green check mark."));
269                putValue(SMALL_ICON, ImageProvider.get("about"));
270            }
271    
272            @Override
273            public void actionPerformed(ActionEvent e) {
274                Main.worker.submit(new ContributorTermsUpdateRunnable());
275                Layer layer = Main.main.getActiveLayer();
276                if (layer instanceof OsmDataLayer) {
277                    refresh(((OsmDataLayer)layer).data.getAllSelected());
278                }
279                setEnabled(false);
280            }
281        }
282    
283        class DoubleClickAdapter extends MouseAdapter {
284            @Override
285            public void mouseClicked(MouseEvent e) {
286                if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount()==2) {
287                    selectionUsersPrimitivesAction.select();
288                }
289            }
290        }
291    
292        /**
293         * Action for selecting the primitives contributed by the currently selected
294         * users.
295         *
296         */
297        private static class UserInfo implements Comparable<UserInfo> {
298            public User user;
299            public int count;
300            public double percent;
301            UserInfo(User user, int count, double percent) {
302                this.user=user;
303                this.count=count;
304                this.percent = percent;
305            }
306            public int compareTo(UserInfo o) {
307                if (count < o.count) return 1;
308                if (count > o.count) return -1;
309                if (user== null || user.getName() == null) return 1;
310                if (o.user == null || o.user.getName() == null) return -1;
311                return user.getName().compareTo(o.user.getName());
312            }
313    
314            public String getName() {
315                if (user == null)
316                    return tr("<new object>");
317                return user.getName();
318            }
319    
320            public int getRelicensingStatus() {
321                if (user == null)
322                    return User.STATUS_UNKNOWN;
323                return user.getRelicensingStatus();
324            }
325        }
326    
327        /**
328         * The table model for the users
329         *
330         */
331        static class UserTableModel extends DefaultTableModel {
332            private ArrayList<UserInfo> data;
333    
334            public UserTableModel() {
335                setColumnIdentifiers(new String[]{tr("Author"),tr("# Objects"),"%", tr("CT")});
336                data = new ArrayList<UserInfo>();
337            }
338    
339            protected Map<User, Integer> computeStatistics(Collection<? extends OsmPrimitive> primitives) {
340                HashMap<User, Integer> ret = new HashMap<User, Integer>();
341                if (primitives == null || primitives.isEmpty()) return ret;
342                for (OsmPrimitive primitive: primitives) {
343                    if (ret.containsKey(primitive.getUser())) {
344                        ret.put(primitive.getUser(), ret.get(primitive.getUser()) + 1);
345                    } else {
346                        ret.put(primitive.getUser(), 1);
347                    }
348                }
349                return ret;
350            }
351    
352            public void populate(Collection<? extends OsmPrimitive> primitives) {
353                Map<User,Integer> statistics = computeStatistics(primitives);
354                data.clear();
355                if (primitives != null) {
356                    for (Map.Entry<User, Integer> entry: statistics.entrySet()) {
357                        data.add(new UserInfo(entry.getKey(), entry.getValue(), (double)entry.getValue() /  (double)primitives.size()));
358                    }
359                }
360                Collections.sort(data);
361                fireTableDataChanged();
362            }
363    
364            @Override
365            public int getRowCount() {
366                if (data == null) return 0;
367                return data.size();
368            }
369    
370            @Override
371            public Object getValueAt(int row, int column) {
372                UserInfo info = data.get(row);
373                switch(column) {
374                case 0: /* author */ return info.getName() == null ? "" : info.getName();
375                case 1: /* count */ return info.count;
376                case 2: /* percent */ return NumberFormat.getPercentInstance().format(info.percent);
377                case 3: /* relicensing status */ return getRelicensingStatusIcon(info.getRelicensingStatus());
378                }
379                return null;
380            }
381    
382            @Override
383            public boolean isCellEditable(int row, int column) {
384                return false;
385            }
386    
387            public void selectPrimitivesOwnedBy(int [] rows) {
388                Set<User> users= new HashSet<User>();
389                for (int index: rows) {
390                    users.add(data.get(index).user);
391                }
392                Collection<OsmPrimitive> selected = Main.main.getCurrentDataSet().getAllSelected();
393                Collection<OsmPrimitive> byUser = new LinkedList<OsmPrimitive>();
394                for (OsmPrimitive p : selected) {
395                    if (users.contains(p.getUser())) {
396                        byUser.add(p);
397                    }
398                }
399                Main.main.getCurrentDataSet().setSelected(byUser);
400            }
401    
402            public List<User> getSelectedUsers(int rows[]) {
403                LinkedList<User> ret = new LinkedList<User>();
404                if (rows == null || rows.length == 0) return ret;
405                for (int row: rows) {
406                    if (data.get(row).user == null) {
407                        continue;
408                    }
409                    ret.add(data.get(row).user);
410                }
411                return ret;
412            }
413        }
414    
415        private static JLabel greenCheckmark;
416        private static JLabel greyCheckmark;
417        private static JLabel redX;
418        private static JLabel empty;
419    
420        public static JLabel getRelicensingStatusIcon(int status) {
421            switch(status) {
422            case User.STATUS_AGREED:
423                if (greenCheckmark == null) {
424                    greenCheckmark = new JLabel(ImageProvider.get("misc", "green_check.png"));
425                    greenCheckmark.setToolTipText(tr("Accepted"));
426                }
427                return greenCheckmark;
428            case User.STATUS_AUTO_AGREED:
429                if (greyCheckmark == null) {
430                    greyCheckmark = new JLabel(ImageProvider.get("misc", "grey_check.png"));
431                    greyCheckmark.setToolTipText(tr("Auto-accepted"));
432                }
433                return greyCheckmark;
434            case User.STATUS_NOT_AGREED:
435                if (redX == null) {
436                    redX = new JLabel(ImageProvider.get("misc", "red_x.png"));
437                    redX.setToolTipText(tr("Declined"));
438                }
439                return redX;
440            default:
441                if (empty == null) {
442                    empty = new JLabel("");
443                    empty.setToolTipText(tr("Undecided"));
444                }
445            }
446            return empty; // Undecided or unknown?
447        }
448    }