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 /** 134 * Refreshes user list from given collection of OSM primitives. 135 * @param fromPrimitives OSM primitives to fetch users from 136 */ 137 public void refresh(Collection<? extends OsmPrimitive> fromPrimitives) { 138 model.populate(fromPrimitives); 139 GuiHelper.runInEDT(new Runnable() { 140 @Override 141 public void run() { 142 if (model.getRowCount() != 0) { 143 setTitle(trn("{0} Author", "{0} Authors", model.getRowCount(), model.getRowCount())); 144 } else { 145 setTitle(tr("Authors")); 146 } 147 } 148 }); 149 } 150 151 @Override 152 public void showDialog() { 153 super.showDialog(); 154 Layer layer = Main.main.getActiveLayer(); 155 if (layer instanceof OsmDataLayer) { 156 refresh(((OsmDataLayer) layer).data.getAllSelected()); 157 } 158 } 159 160 class SelectUsersPrimitivesAction extends AbstractAction implements ListSelectionListener { 161 162 /** 163 * Constructs a new {@code SelectUsersPrimitivesAction}. 164 */ 165 SelectUsersPrimitivesAction() { 166 putValue(NAME, tr("Select")); 167 putValue(SHORT_DESCRIPTION, tr("Select objects submitted by this user")); 168 putValue(SMALL_ICON, ImageProvider.get("dialogs", "select")); 169 updateEnabledState(); 170 } 171 172 public void select() { 173 int[] indexes = userTable.getSelectedRows(); 174 if (indexes == null || indexes.length == 0) 175 return; 176 model.selectPrimitivesOwnedBy(userTable.getSelectedRows()); 177 } 178 179 @Override 180 public void actionPerformed(ActionEvent e) { 181 select(); 182 } 183 184 protected void updateEnabledState() { 185 setEnabled(userTable != null && userTable.getSelectedRowCount() > 0); 186 } 187 188 @Override 189 public void valueChanged(ListSelectionEvent e) { 190 updateEnabledState(); 191 } 192 } 193 194 /** 195 * Action for launching the info page of a user. 196 */ 197 class ShowUserInfoAction extends AbstractInfoAction implements ListSelectionListener { 198 199 ShowUserInfoAction() { 200 super(false); 201 putValue(NAME, tr("Show info")); 202 putValue(SHORT_DESCRIPTION, tr("Launches a browser with information about the user")); 203 putValue(SMALL_ICON, ImageProvider.get("help/internet")); 204 updateEnabledState(); 205 } 206 207 @Override 208 public void actionPerformed(ActionEvent e) { 209 int[] rows = userTable.getSelectedRows(); 210 if (rows == null || rows.length == 0) 211 return; 212 List<User> users = model.getSelectedUsers(rows); 213 if (users.isEmpty()) 214 return; 215 if (users.size() > 10) { 216 Main.warn(tr("Only launching info browsers for the first {0} of {1} selected users", 10, users.size())); 217 } 218 int num = Math.min(10, users.size()); 219 Iterator<User> it = users.iterator(); 220 while (it.hasNext() && num > 0) { 221 String url = createInfoUrl(it.next()); 222 if (url == null) { 223 break; 224 } 225 OpenBrowser.displayUrl(url); 226 num--; 227 } 228 } 229 230 @Override 231 protected String createInfoUrl(Object infoObject) { 232 if (infoObject instanceof User) { 233 User user = (User) infoObject; 234 return Main.getBaseUserUrl() + '/' + Utils.encodeUrl(user.getName()).replaceAll("\\+", "%20"); 235 } else { 236 return null; 237 } 238 } 239 240 @Override 241 protected void updateEnabledState() { 242 setEnabled(userTable != null && userTable.getSelectedRowCount() > 0); 243 } 244 245 @Override 246 public void valueChanged(ListSelectionEvent e) { 247 updateEnabledState(); 248 } 249 } 250 251 class DoubleClickAdapter extends MouseAdapter { 252 @Override 253 public void mouseClicked(MouseEvent e) { 254 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { 255 selectionUsersPrimitivesAction.select(); 256 } 257 } 258 } 259 260 /** 261 * Action for selecting the primitives contributed by the currently selected users. 262 * 263 */ 264 private static class UserInfo implements Comparable<UserInfo> { 265 public final User user; 266 public final int count; 267 public final double percent; 268 269 UserInfo(User user, int count, double percent) { 270 this.user = user; 271 this.count = count; 272 this.percent = percent; 273 } 274 275 @Override 276 public int compareTo(UserInfo o) { 277 if (count < o.count) 278 return 1; 279 if (count > o.count) 280 return -1; 281 if (user == null || user.getName() == null) 282 return 1; 283 if (o.user == null || o.user.getName() == null) 284 return -1; 285 return user.getName().compareTo(o.user.getName()); 286 } 287 288 public String getName() { 289 if (user == null) 290 return tr("<new object>"); 291 return user.getName(); 292 } 293 } 294 295 /** 296 * The table model for the users 297 * 298 */ 299 static class UserTableModel extends DefaultTableModel { 300 private final transient List<UserInfo> data; 301 302 UserTableModel() { 303 setColumnIdentifiers(new String[]{tr("Author"), tr("# Objects"), "%"}); 304 data = new ArrayList<>(); 305 } 306 307 protected Map<User, Integer> computeStatistics(Collection<? extends OsmPrimitive> primitives) { 308 Map<User, Integer> ret = new HashMap<>(); 309 if (primitives == null || primitives.isEmpty()) 310 return ret; 311 for (OsmPrimitive primitive: primitives) { 312 if (ret.containsKey(primitive.getUser())) { 313 ret.put(primitive.getUser(), ret.get(primitive.getUser()) + 1); 314 } else { 315 ret.put(primitive.getUser(), 1); 316 } 317 } 318 return ret; 319 } 320 321 public void populate(Collection<? extends OsmPrimitive> primitives) { 322 Map<User, Integer> statistics = computeStatistics(primitives); 323 data.clear(); 324 if (primitives != null) { 325 for (Map.Entry<User, Integer> entry: statistics.entrySet()) { 326 data.add(new UserInfo(entry.getKey(), entry.getValue(), (double) entry.getValue() / (double) primitives.size())); 327 } 328 } 329 Collections.sort(data); 330 GuiHelper.runInEDTAndWait(new Runnable() { 331 @Override 332 public void run() { 333 fireTableDataChanged(); 334 } 335 }); 336 } 337 338 @Override 339 public int getRowCount() { 340 if (data == null) 341 return 0; 342 return data.size(); 343 } 344 345 @Override 346 public Object getValueAt(int row, int column) { 347 UserInfo info = data.get(row); 348 switch(column) { 349 case 0: /* author */ return info.getName() == null ? "" : info.getName(); 350 case 1: /* count */ return info.count; 351 case 2: /* percent */ return NumberFormat.getPercentInstance().format(info.percent); 352 default: return null; 353 } 354 } 355 356 @Override 357 public boolean isCellEditable(int row, int column) { 358 return false; 359 } 360 361 public void selectPrimitivesOwnedBy(int[] rows) { 362 Set<User> users = new HashSet<>(); 363 for (int index: rows) { 364 users.add(data.get(index).user); 365 } 366 Collection<OsmPrimitive> selected = Main.main.getCurrentDataSet().getAllSelected(); 367 Collection<OsmPrimitive> byUser = new LinkedList<>(); 368 for (OsmPrimitive p : selected) { 369 if (users.contains(p.getUser())) { 370 byUser.add(p); 371 } 372 } 373 Main.main.getCurrentDataSet().setSelected(byUser); 374 } 375 376 public List<User> getSelectedUsers(int[] rows) { 377 List<User> ret = new LinkedList<>(); 378 if (rows == null || rows.length == 0) 379 return ret; 380 for (int row: rows) { 381 if (data.get(row).user == null) { 382 continue; 383 } 384 ret.add(data.get(row).user); 385 } 386 return ret; 387 } 388 } 389}