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 }