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.OpenBrowser; 047import org.openstreetmap.josm.tools.Shortcut; 048 049/** 050 * Displays a dialog with all users who have last edited something in the 051 * selection area, along with the number of objects. 052 * 053 */ 054public class UserListDialog extends ToggleDialog implements SelectionChangedListener, MapView.LayerChangeListener { 055 056 /** 057 * The display list. 058 */ 059 private JTable userTable; 060 private UserTableModel model; 061 private SelectUsersPrimitivesAction selectionUsersPrimitivesAction; 062 private ShowUserInfoAction showUserInfoAction; 063 064 public UserListDialog() { 065 super(tr("Authors"), "userlist", tr("Open a list of people working on the selected objects."), 066 Shortcut.registerShortcut("subwindow:authors", tr("Toggle: {0}", tr("Authors")), KeyEvent.VK_A, Shortcut.ALT_SHIFT), 150); 067 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 public SelectUsersPrimitivesAction() { 159 putValue(NAME, tr("Select")); 160 putValue(SHORT_DESCRIPTION, tr("Select objects submitted by this user")); 161 putValue(SMALL_ICON, ImageProvider.get("dialogs", "select")); 162 updateEnabledState(); 163 } 164 165 public void select() { 166 int[] indexes = userTable.getSelectedRows(); 167 if (indexes == null || indexes.length == 0) 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 public ShowUserInfoAction() { 192 super(false); 193 putValue(NAME, tr("Show info")); 194 putValue(SHORT_DESCRIPTION, tr("Launches a browser with information about the user")); 195 putValue(SMALL_ICON, ImageProvider.get("about")); 196 updateEnabledState(); 197 } 198 199 @Override 200 public void actionPerformed(ActionEvent e) { 201 int[] rows = userTable.getSelectedRows(); 202 if (rows == null || rows.length == 0) return; 203 List<User> users = model.getSelectedUsers(rows); 204 if (users.isEmpty()) return; 205 if (users.size() > 10) { 206 Main.warn(tr("Only launching info browsers for the first {0} of {1} selected users", 10, users.size())); 207 } 208 int num = Math.min(10, users.size()); 209 Iterator<User> it = users.iterator(); 210 while(it.hasNext() && num > 0) { 211 String url = createInfoUrl(it.next()); 212 if (url == null) { 213 break; 214 } 215 OpenBrowser.displayUrl(url); 216 num--; 217 } 218 } 219 220 @Override 221 protected String createInfoUrl(Object infoObject) { 222 User user = (User)infoObject; 223 try { 224 return getBaseUserUrl() + "/" + URLEncoder.encode(user.getName(), "UTF-8").replaceAll("\\+", "%20"); 225 } catch(UnsupportedEncodingException e) { 226 Main.error(e); 227 JOptionPane.showMessageDialog( 228 Main.parent, 229 tr("<html>Failed to create an URL because the encoding ''{0}''<br>" 230 + "was missing on this system.</html>", "UTF-8"), 231 tr("Missing encoding"), 232 JOptionPane.ERROR_MESSAGE 233 ); 234 return null; 235 } 236 } 237 238 @Override 239 protected void updateEnabledState() { 240 setEnabled(userTable != null && userTable.getSelectedRowCount() > 0); 241 } 242 243 @Override 244 public void valueChanged(ListSelectionEvent e) { 245 updateEnabledState(); 246 } 247 } 248 249 class DoubleClickAdapter extends MouseAdapter { 250 @Override 251 public void mouseClicked(MouseEvent e) { 252 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount()==2) { 253 selectionUsersPrimitivesAction.select(); 254 } 255 } 256 } 257 258 /** 259 * Action for selecting the primitives contributed by the currently selected 260 * users. 261 * 262 */ 263 private static class UserInfo implements Comparable<UserInfo> { 264 public User user; 265 public int count; 266 public double percent; 267 UserInfo(User user, int count, double percent) { 268 this.user=user; 269 this.count=count; 270 this.percent = percent; 271 } 272 @Override 273 public int compareTo(UserInfo o) { 274 if (count < o.count) return 1; 275 if (count > o.count) return -1; 276 if (user== null || user.getName() == null) return 1; 277 if (o.user == null || o.user.getName() == null) return -1; 278 return user.getName().compareTo(o.user.getName()); 279 } 280 281 public String getName() { 282 if (user == null) 283 return tr("<new object>"); 284 return user.getName(); 285 } 286 } 287 288 /** 289 * The table model for the users 290 * 291 */ 292 static class UserTableModel extends DefaultTableModel { 293 private List<UserInfo> data; 294 295 public UserTableModel() { 296 setColumnIdentifiers(new String[]{tr("Author"),tr("# Objects"),"%"}); 297 data = new ArrayList<>(); 298 } 299 300 protected Map<User, Integer> computeStatistics(Collection<? extends OsmPrimitive> primitives) { 301 HashMap<User, Integer> ret = new HashMap<>(); 302 if (primitives == null || primitives.isEmpty()) 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) return 0; 333 return data.size(); 334 } 335 336 @Override 337 public Object getValueAt(int row, int column) { 338 UserInfo info = data.get(row); 339 switch(column) { 340 case 0: /* author */ return info.getName() == null ? "" : info.getName(); 341 case 1: /* count */ return info.count; 342 case 2: /* percent */ return NumberFormat.getPercentInstance().format(info.percent); 343 } 344 return null; 345 } 346 347 @Override 348 public boolean isCellEditable(int row, int column) { 349 return false; 350 } 351 352 public void selectPrimitivesOwnedBy(int [] rows) { 353 Set<User> users= new HashSet<>(); 354 for (int index: rows) { 355 users.add(data.get(index).user); 356 } 357 Collection<OsmPrimitive> selected = Main.main.getCurrentDataSet().getAllSelected(); 358 Collection<OsmPrimitive> byUser = new LinkedList<>(); 359 for (OsmPrimitive p : selected) { 360 if (users.contains(p.getUser())) { 361 byUser.add(p); 362 } 363 } 364 Main.main.getCurrentDataSet().setSelected(byUser); 365 } 366 367 public List<User> getSelectedUsers(int[] rows) { 368 LinkedList<User> ret = new LinkedList<>(); 369 if (rows == null || rows.length == 0) return ret; 370 for (int row: rows) { 371 if (data.get(row).user == null) { 372 continue; 373 } 374 ret.add(data.get(row).user); 375 } 376 return ret; 377 } 378 } 379}