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}