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