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.text.NumberFormat; 012import java.util.ArrayList; 013import java.util.Arrays; 014import java.util.Collection; 015import java.util.Collections; 016import java.util.HashMap; 017import java.util.HashSet; 018import java.util.Iterator; 019import java.util.LinkedList; 020import java.util.List; 021import java.util.Map; 022import java.util.Set; 023 024import javax.swing.AbstractAction; 025import javax.swing.JTable; 026import javax.swing.ListSelectionModel; 027import javax.swing.event.ListSelectionEvent; 028import javax.swing.event.ListSelectionListener; 029import javax.swing.table.DefaultTableModel; 030 031import org.openstreetmap.josm.Main; 032import org.openstreetmap.josm.actions.AbstractInfoAction; 033import org.openstreetmap.josm.data.SelectionChangedListener; 034import org.openstreetmap.josm.data.osm.DataSet; 035import org.openstreetmap.josm.data.osm.OsmPrimitive; 036import org.openstreetmap.josm.data.osm.User; 037import org.openstreetmap.josm.gui.MapView; 038import org.openstreetmap.josm.gui.SideButton; 039import org.openstreetmap.josm.gui.layer.Layer; 040import org.openstreetmap.josm.gui.layer.OsmDataLayer; 041import org.openstreetmap.josm.gui.util.GuiHelper; 042import org.openstreetmap.josm.tools.ImageProvider; 043import org.openstreetmap.josm.tools.OpenBrowser; 044import org.openstreetmap.josm.tools.Shortcut; 045import org.openstreetmap.josm.tools.Utils; 046 047/** 048 * Displays a dialog with all users who have last edited something in the 049 * selection area, along with the number of objects. 050 * 051 */ 052public class UserListDialog extends ToggleDialog implements SelectionChangedListener, MapView.LayerChangeListener { 053 054 /** 055 * The display list. 056 */ 057 private JTable userTable; 058 private UserTableModel model; 059 private SelectUsersPrimitivesAction selectionUsersPrimitivesAction; 060 private ShowUserInfoAction showUserInfoAction; 061 062 /** 063 * Constructs a new {@code UserListDialog}. 064 */ 065 public UserListDialog() { 066 super(tr("Authors"), "userlist", tr("Open a list of people working on the selected objects."), 067 Shortcut.registerShortcut("subwindow:authors", tr("Toggle: {0}", tr("Authors")), KeyEvent.VK_A, Shortcut.ALT_SHIFT), 150); 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 159 /** 160 * Constructs a new {@code SelectUsersPrimitivesAction}. 161 */ 162 SelectUsersPrimitivesAction() { 163 putValue(NAME, tr("Select")); 164 putValue(SHORT_DESCRIPTION, tr("Select objects submitted by this user")); 165 putValue(SMALL_ICON, ImageProvider.get("dialogs", "select")); 166 updateEnabledState(); 167 } 168 169 public void select() { 170 int[] indexes = userTable.getSelectedRows(); 171 if (indexes == null || indexes.length == 0) return; 172 model.selectPrimitivesOwnedBy(userTable.getSelectedRows()); 173 } 174 175 @Override 176 public void actionPerformed(ActionEvent e) { 177 select(); 178 } 179 180 protected void updateEnabledState() { 181 setEnabled(userTable != null && userTable.getSelectedRowCount() > 0); 182 } 183 184 @Override 185 public void valueChanged(ListSelectionEvent e) { 186 updateEnabledState(); 187 } 188 } 189 190 /* 191 * Action for launching the info page of a user 192 */ 193 class ShowUserInfoAction extends AbstractInfoAction implements ListSelectionListener { 194 195 ShowUserInfoAction() { 196 super(false); 197 putValue(NAME, tr("Show info")); 198 putValue(SHORT_DESCRIPTION, tr("Launches a browser with information about the user")); 199 putValue(SMALL_ICON, ImageProvider.get("help/internet")); 200 updateEnabledState(); 201 } 202 203 @Override 204 public void actionPerformed(ActionEvent e) { 205 int[] rows = userTable.getSelectedRows(); 206 if (rows == null || rows.length == 0) return; 207 List<User> users = model.getSelectedUsers(rows); 208 if (users.isEmpty()) return; 209 if (users.size() > 10) { 210 Main.warn(tr("Only launching info browsers for the first {0} of {1} selected users", 10, users.size())); 211 } 212 int num = Math.min(10, users.size()); 213 Iterator<User> it = users.iterator(); 214 while (it.hasNext() && num > 0) { 215 String url = createInfoUrl(it.next()); 216 if (url == null) { 217 break; 218 } 219 OpenBrowser.displayUrl(url); 220 num--; 221 } 222 } 223 224 @Override 225 protected String createInfoUrl(Object infoObject) { 226 if (infoObject instanceof User) { 227 User user = (User) infoObject; 228 return Main.getBaseUserUrl() + '/' + Utils.encodeUrl(user.getName()).replaceAll("\\+", "%20"); 229 } else { 230 return null; 231 } 232 } 233 234 @Override 235 protected void updateEnabledState() { 236 setEnabled(userTable != null && userTable.getSelectedRowCount() > 0); 237 } 238 239 @Override 240 public void valueChanged(ListSelectionEvent e) { 241 updateEnabledState(); 242 } 243 } 244 245 class DoubleClickAdapter extends MouseAdapter { 246 @Override 247 public void mouseClicked(MouseEvent e) { 248 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { 249 selectionUsersPrimitivesAction.select(); 250 } 251 } 252 } 253 254 /** 255 * Action for selecting the primitives contributed by the currently selected users. 256 * 257 */ 258 private static class UserInfo implements Comparable<UserInfo> { 259 public User user; 260 public int count; 261 public double percent; 262 263 UserInfo(User user, int count, double percent) { 264 this.user = user; 265 this.count = count; 266 this.percent = percent; 267 } 268 269 @Override 270 public int compareTo(UserInfo o) { 271 if (count < o.count) return 1; 272 if (count > o.count) return -1; 273 if (user == null || user.getName() == null) return 1; 274 if (o.user == null || o.user.getName() == null) return -1; 275 return user.getName().compareTo(o.user.getName()); 276 } 277 278 public String getName() { 279 if (user == null) 280 return tr("<new object>"); 281 return user.getName(); 282 } 283 } 284 285 /** 286 * The table model for the users 287 * 288 */ 289 static class UserTableModel extends DefaultTableModel { 290 private transient List<UserInfo> data; 291 292 UserTableModel() { 293 setColumnIdentifiers(new String[]{tr("Author"), tr("# Objects"), "%"}); 294 data = new ArrayList<>(); 295 } 296 297 protected Map<User, Integer> computeStatistics(Collection<? extends OsmPrimitive> primitives) { 298 Map<User, Integer> ret = new HashMap<>(); 299 if (primitives == null || primitives.isEmpty()) return ret; 300 for (OsmPrimitive primitive: primitives) { 301 if (ret.containsKey(primitive.getUser())) { 302 ret.put(primitive.getUser(), ret.get(primitive.getUser()) + 1); 303 } else { 304 ret.put(primitive.getUser(), 1); 305 } 306 } 307 return ret; 308 } 309 310 public void populate(Collection<? extends OsmPrimitive> primitives) { 311 Map<User, Integer> statistics = computeStatistics(primitives); 312 data.clear(); 313 if (primitives != null) { 314 for (Map.Entry<User, Integer> entry: statistics.entrySet()) { 315 data.add(new UserInfo(entry.getKey(), entry.getValue(), (double) entry.getValue() / (double) primitives.size())); 316 } 317 } 318 Collections.sort(data); 319 GuiHelper.runInEDTAndWait(new Runnable() { 320 @Override 321 public void run() { 322 fireTableDataChanged(); 323 } 324 }); 325 } 326 327 @Override 328 public int getRowCount() { 329 if (data == null) return 0; 330 return data.size(); 331 } 332 333 @Override 334 public Object getValueAt(int row, int column) { 335 UserInfo info = data.get(row); 336 switch(column) { 337 case 0: /* author */ return info.getName() == null ? "" : info.getName(); 338 case 1: /* count */ return info.count; 339 case 2: /* percent */ return NumberFormat.getPercentInstance().format(info.percent); 340 } 341 return null; 342 } 343 344 @Override 345 public boolean isCellEditable(int row, int column) { 346 return false; 347 } 348 349 public void selectPrimitivesOwnedBy(int[] rows) { 350 Set<User> users = new HashSet<>(); 351 for (int index: rows) { 352 users.add(data.get(index).user); 353 } 354 Collection<OsmPrimitive> selected = Main.main.getCurrentDataSet().getAllSelected(); 355 Collection<OsmPrimitive> byUser = new LinkedList<>(); 356 for (OsmPrimitive p : selected) { 357 if (users.contains(p.getUser())) { 358 byUser.add(p); 359 } 360 } 361 Main.main.getCurrentDataSet().setSelected(byUser); 362 } 363 364 public List<User> getSelectedUsers(int[] rows) { 365 List<User> ret = new LinkedList<>(); 366 if (rows == null || rows.length == 0) return ret; 367 for (int row: rows) { 368 if (data.get(row).user == null) { 369 continue; 370 } 371 ret.add(data.get(row).user); 372 } 373 return ret; 374 } 375 } 376}