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.SideButton; 038import org.openstreetmap.josm.gui.layer.Layer; 039import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 040import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 041import org.openstreetmap.josm.gui.layer.OsmDataLayer; 042import org.openstreetmap.josm.gui.util.GuiHelper; 043import org.openstreetmap.josm.tools.ImageProvider; 044import org.openstreetmap.josm.tools.OpenBrowser; 045import org.openstreetmap.josm.tools.Shortcut; 046import org.openstreetmap.josm.tools.Utils; 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, ActiveLayerChangeListener { 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 /** 064 * Constructs a new {@code UserListDialog}. 065 */ 066 public UserListDialog() { 067 super(tr("Authors"), "userlist", tr("Open a list of people working on the selected objects."), 068 Shortcut.registerShortcut("subwindow:authors", tr("Toggle: {0}", tr("Authors")), KeyEvent.VK_A, Shortcut.ALT_SHIFT), 150); 069 build(); 070 } 071 072 @Override 073 public void showNotify() { 074 DataSet.addSelectionListener(this); 075 Main.getLayerManager().addActiveLayerChangeListener(this); 076 } 077 078 @Override 079 public void hideNotify() { 080 Main.getLayerManager().removeActiveLayerChangeListener(this); 081 DataSet.removeSelectionListener(this); 082 } 083 084 protected void build() { 085 model = new UserTableModel(); 086 userTable = new JTable(model); 087 userTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 088 userTable.addMouseListener(new DoubleClickAdapter()); 089 090 // -- select users primitives action 091 // 092 selectionUsersPrimitivesAction = new SelectUsersPrimitivesAction(); 093 userTable.getSelectionModel().addListSelectionListener(selectionUsersPrimitivesAction); 094 095 // -- info action 096 // 097 showUserInfoAction = new ShowUserInfoAction(); 098 userTable.getSelectionModel().addListSelectionListener(showUserInfoAction); 099 100 createLayout(userTable, true, Arrays.asList(new SideButton[] { 101 new SideButton(selectionUsersPrimitivesAction), 102 new SideButton(showUserInfoAction) 103 })); 104 } 105 106 /** 107 * Called when the selection in the dataset changed. 108 * @param newSelection The new selection array. 109 */ 110 @Override 111 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 112 refresh(newSelection); 113 } 114 115 @Override 116 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 117 Layer activeLayer = e.getSource().getActiveLayer(); 118 if (activeLayer instanceof OsmDataLayer) { 119 refresh(((OsmDataLayer) activeLayer).data.getAllSelected()); 120 } else { 121 refresh(null); 122 } 123 } 124 125 /** 126 * Refreshes user list from given collection of OSM primitives. 127 * @param fromPrimitives OSM primitives to fetch users from 128 */ 129 public void refresh(Collection<? extends OsmPrimitive> fromPrimitives) { 130 model.populate(fromPrimitives); 131 GuiHelper.runInEDT(new Runnable() { 132 @Override 133 public void run() { 134 if (model.getRowCount() != 0) { 135 setTitle(trn("{0} Author", "{0} Authors", model.getRowCount(), model.getRowCount())); 136 } else { 137 setTitle(tr("Authors")); 138 } 139 } 140 }); 141 } 142 143 @Override 144 public void showDialog() { 145 super.showDialog(); 146 Layer layer = Main.getLayerManager().getActiveLayer(); 147 if (layer instanceof OsmDataLayer) { 148 refresh(((OsmDataLayer) layer).data.getAllSelected()); 149 } 150 } 151 152 class SelectUsersPrimitivesAction extends AbstractAction implements ListSelectionListener { 153 154 /** 155 * Constructs a new {@code SelectUsersPrimitivesAction}. 156 */ 157 SelectUsersPrimitivesAction() { 158 putValue(NAME, tr("Select")); 159 putValue(SHORT_DESCRIPTION, tr("Select objects submitted by this user")); 160 new ImageProvider("dialogs", "select").getResource().attachImageIcon(this, true); 161 updateEnabledState(); 162 } 163 164 public void select() { 165 int[] indexes = userTable.getSelectedRows(); 166 if (indexes == null || indexes.length == 0) 167 return; 168 model.selectPrimitivesOwnedBy(userTable.getSelectedRows()); 169 } 170 171 @Override 172 public void actionPerformed(ActionEvent e) { 173 select(); 174 } 175 176 protected void updateEnabledState() { 177 setEnabled(userTable != null && userTable.getSelectedRowCount() > 0); 178 } 179 180 @Override 181 public void valueChanged(ListSelectionEvent e) { 182 updateEnabledState(); 183 } 184 } 185 186 /** 187 * Action for launching the info page of a user. 188 */ 189 class ShowUserInfoAction extends AbstractInfoAction implements ListSelectionListener { 190 191 ShowUserInfoAction() { 192 super(false); 193 putValue(NAME, tr("Show info")); 194 putValue(SHORT_DESCRIPTION, tr("Launches a browser with information about the user")); 195 new ImageProvider("help/internet").getResource().attachImageIcon(this, true); 196 updateEnabledState(); 197 } 198 199 @Override 200 public void actionPerformed(ActionEvent e) { 201 int[] rows = userTable.getSelectedRows(); 202 if (rows == null || rows.length == 0) 203 return; 204 List<User> users = model.getSelectedUsers(rows); 205 if (users.isEmpty()) 206 return; 207 if (users.size() > 10) { 208 Main.warn(tr("Only launching info browsers for the first {0} of {1} selected users", 10, users.size())); 209 } 210 int num = Math.min(10, users.size()); 211 Iterator<User> it = users.iterator(); 212 while (it.hasNext() && num > 0) { 213 String url = createInfoUrl(it.next()); 214 if (url == null) { 215 break; 216 } 217 OpenBrowser.displayUrl(url); 218 num--; 219 } 220 } 221 222 @Override 223 protected String createInfoUrl(Object infoObject) { 224 if (infoObject instanceof User) { 225 User user = (User) infoObject; 226 return Main.getBaseUserUrl() + '/' + Utils.encodeUrl(user.getName()).replaceAll("\\+", "%20"); 227 } else { 228 return null; 229 } 230 } 231 232 @Override 233 protected void updateEnabledState() { 234 setEnabled(userTable != null && userTable.getSelectedRowCount() > 0); 235 } 236 237 @Override 238 public void valueChanged(ListSelectionEvent e) { 239 updateEnabledState(); 240 } 241 } 242 243 class DoubleClickAdapter extends MouseAdapter { 244 @Override 245 public void mouseClicked(MouseEvent e) { 246 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { 247 selectionUsersPrimitivesAction.select(); 248 } 249 } 250 } 251 252 /** 253 * Action for selecting the primitives contributed by the currently selected users. 254 * 255 */ 256 private static class UserInfo implements Comparable<UserInfo> { 257 public final User user; 258 public final int count; 259 public final double percent; 260 261 UserInfo(User user, int count, double percent) { 262 this.user = user; 263 this.count = count; 264 this.percent = percent; 265 } 266 267 @Override 268 public int compareTo(UserInfo o) { 269 if (count < o.count) 270 return 1; 271 if (count > o.count) 272 return -1; 273 if (user == null || user.getName() == null) 274 return 1; 275 if (o.user == null || o.user.getName() == null) 276 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 final transient List<UserInfo> data; 293 294 UserTableModel() { 295 setColumnIdentifiers(new String[]{tr("Author"), tr("# Objects"), "%"}); 296 data = new ArrayList<>(); 297 } 298 299 protected Map<User, Integer> computeStatistics(Collection<? extends OsmPrimitive> primitives) { 300 Map<User, Integer> ret = new HashMap<>(); 301 if (primitives == null || primitives.isEmpty()) 302 return ret; 303 for (OsmPrimitive primitive: primitives) { 304 if (ret.containsKey(primitive.getUser())) { 305 ret.put(primitive.getUser(), ret.get(primitive.getUser()) + 1); 306 } else { 307 ret.put(primitive.getUser(), 1); 308 } 309 } 310 return ret; 311 } 312 313 public void populate(Collection<? extends OsmPrimitive> primitives) { 314 Map<User, Integer> statistics = computeStatistics(primitives); 315 data.clear(); 316 if (primitives != null) { 317 for (Map.Entry<User, Integer> entry: statistics.entrySet()) { 318 data.add(new UserInfo(entry.getKey(), entry.getValue(), (double) entry.getValue() / (double) primitives.size())); 319 } 320 } 321 Collections.sort(data); 322 GuiHelper.runInEDTAndWait(new Runnable() { 323 @Override 324 public void run() { 325 fireTableDataChanged(); 326 } 327 }); 328 } 329 330 @Override 331 public int getRowCount() { 332 if (data == null) 333 return 0; 334 return data.size(); 335 } 336 337 @Override 338 public Object getValueAt(int row, int column) { 339 UserInfo info = data.get(row); 340 switch(column) { 341 case 0: /* author */ return info.getName() == null ? "" : info.getName(); 342 case 1: /* count */ return info.count; 343 case 2: /* percent */ return NumberFormat.getPercentInstance().format(info.percent); 344 default: return null; 345 } 346 } 347 348 @Override 349 public boolean isCellEditable(int row, int column) { 350 return false; 351 } 352 353 public void selectPrimitivesOwnedBy(int[] rows) { 354 Set<User> users = new HashSet<>(); 355 for (int index: rows) { 356 users.add(data.get(index).user); 357 } 358 Collection<OsmPrimitive> selected = Main.getLayerManager().getEditDataSet().getAllSelected(); 359 Collection<OsmPrimitive> byUser = new LinkedList<>(); 360 for (OsmPrimitive p : selected) { 361 if (users.contains(p.getUser())) { 362 byUser.add(p); 363 } 364 } 365 Main.getLayerManager().getEditDataSet().setSelected(byUser); 366 } 367 368 public List<User> getSelectedUsers(int[] rows) { 369 List<User> ret = new LinkedList<>(); 370 if (rows == null || rows.length == 0) 371 return ret; 372 for (int row: rows) { 373 if (data.get(row).user == null) { 374 continue; 375 } 376 ret.add(data.get(row).user); 377 } 378 return ret; 379 } 380 } 381}