001 // License: GPL. Copyright 2007 by Immanuel Scholz and others
002 package org.openstreetmap.josm.gui.dialogs;
003
004 import static org.openstreetmap.josm.tools.I18n.tr;
005 import static org.openstreetmap.josm.tools.I18n.trn;
006
007 import java.awt.event.ActionEvent;
008 import java.awt.event.KeyEvent;
009 import java.awt.event.MouseAdapter;
010 import java.awt.event.MouseEvent;
011 import java.io.UnsupportedEncodingException;
012 import java.net.URLEncoder;
013 import java.text.NumberFormat;
014 import java.util.ArrayList;
015 import java.util.Arrays;
016 import java.util.Collection;
017 import java.util.Collections;
018 import java.util.HashMap;
019 import java.util.HashSet;
020 import java.util.Iterator;
021 import java.util.LinkedList;
022 import java.util.List;
023 import java.util.Map;
024 import java.util.Set;
025
026 import javax.swing.AbstractAction;
027 import javax.swing.JOptionPane;
028 import javax.swing.JTable;
029 import javax.swing.ListSelectionModel;
030 import javax.swing.event.ListSelectionEvent;
031 import javax.swing.event.ListSelectionListener;
032 import javax.swing.table.DefaultTableModel;
033
034 import org.openstreetmap.josm.Main;
035 import org.openstreetmap.josm.actions.AbstractInfoAction;
036 import org.openstreetmap.josm.data.SelectionChangedListener;
037 import org.openstreetmap.josm.data.osm.DataSet;
038 import org.openstreetmap.josm.data.osm.OsmPrimitive;
039 import org.openstreetmap.josm.data.osm.User;
040 import org.openstreetmap.josm.gui.MapView;
041 import org.openstreetmap.josm.gui.SideButton;
042 import org.openstreetmap.josm.gui.layer.Layer;
043 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
044 import org.openstreetmap.josm.tools.ImageProvider;
045 import org.openstreetmap.josm.tools.Shortcut;
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 */
052 public 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 public UserListDialog() {
063 super(tr("Authors"), "userlist", tr("Open a list of people working on the selected objects."),
064 Shortcut.registerShortcut("subwindow:authors", tr("Toggle: {0}", tr("Authors")), KeyEvent.VK_A, Shortcut.ALT_SHIFT), 150);
065
066 build();
067 }
068
069 @Override
070 public void showNotify() {
071 DataSet.addSelectionListener(this);
072 MapView.addLayerChangeListener(this);
073 }
074
075 @Override
076 public void hideNotify() {
077 MapView.removeLayerChangeListener(this);
078 DataSet.removeSelectionListener(this);
079 }
080
081 protected void build() {
082 model = new UserTableModel();
083 userTable = new JTable(model);
084 userTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
085 userTable.addMouseListener(new DoubleClickAdapter());
086
087 // -- select users primitives action
088 //
089 selectionUsersPrimitivesAction = new SelectUsersPrimitivesAction();
090 userTable.getSelectionModel().addListSelectionListener(selectionUsersPrimitivesAction);
091
092 // -- info action
093 //
094 showUserInfoAction = new ShowUserInfoAction();
095 userTable.getSelectionModel().addListSelectionListener(showUserInfoAction);
096
097 createLayout(userTable, true, Arrays.asList(new SideButton[] {
098 new SideButton(selectionUsersPrimitivesAction),
099 new SideButton(showUserInfoAction)
100 }));
101 }
102
103 /**
104 * Called when the selection in the dataset changed.
105 * @param newSelection The new selection array.
106 */
107 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
108 refresh(newSelection);
109 }
110
111 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
112 if (newLayer instanceof OsmDataLayer) {
113 refresh(((OsmDataLayer) newLayer).data.getAllSelected());
114 } else {
115 refresh(null);
116 }
117 }
118
119 public void layerAdded(Layer newLayer) {
120 // do nothing
121 }
122
123 public void layerRemoved(Layer oldLayer) {
124 // do nothing
125 }
126
127 public void refresh(Collection<? extends OsmPrimitive> fromPrimitives) {
128 model.populate(fromPrimitives);
129 if(model.getRowCount() != 0) {
130 setTitle(trn("{0} Author", "{0} Authors", model.getRowCount() , model.getRowCount()));
131 } else {
132 setTitle(tr("Authors"));
133 }
134 }
135
136 @Override
137 public void showDialog() {
138 super.showDialog();
139 Layer layer = Main.main.getActiveLayer();
140 if (layer instanceof OsmDataLayer) {
141 refresh(((OsmDataLayer)layer).data.getAllSelected());
142 }
143
144 }
145
146 class SelectUsersPrimitivesAction extends AbstractAction implements ListSelectionListener{
147 public SelectUsersPrimitivesAction() {
148 putValue(NAME, tr("Select"));
149 putValue(SHORT_DESCRIPTION, tr("Select objects submitted by this user"));
150 putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
151 updateEnabledState();
152 }
153
154 public void select() {
155 int indexes[] = userTable.getSelectedRows();
156 if (indexes == null || indexes.length == 0) return;
157 model.selectPrimitivesOwnedBy(userTable.getSelectedRows());
158 }
159
160 public void actionPerformed(ActionEvent e) {
161 select();
162 }
163
164 protected void updateEnabledState() {
165 setEnabled(userTable != null && userTable.getSelectedRowCount() > 0);
166 }
167
168 public void valueChanged(ListSelectionEvent e) {
169 updateEnabledState();
170 }
171 }
172
173 /*
174 * Action for launching the info page of a user
175 */
176 class ShowUserInfoAction extends AbstractInfoAction implements ListSelectionListener {
177
178 public ShowUserInfoAction() {
179 super(false);
180 putValue(NAME, tr("Show info"));
181 putValue(SHORT_DESCRIPTION, tr("Launches a browser with information about the user"));
182 putValue(SMALL_ICON, ImageProvider.get("about"));
183 updateEnabledState();
184 }
185
186 @Override
187 public void actionPerformed(ActionEvent e) {
188 int rows[] = userTable.getSelectedRows();
189 if (rows == null || rows.length == 0) return;
190 List<User> users = model.getSelectedUsers(rows);
191 if (users.isEmpty()) return;
192 if (users.size() > 10) {
193 System.out.println(tr("Warning: only launching info browsers for the first {0} of {1} selected users", 10, users.size()));
194 }
195 int num = Math.min(10, users.size());
196 Iterator<User> it = users.iterator();
197 while(it.hasNext() && num > 0) {
198 String url = createInfoUrl(it.next());
199 if (url == null) {
200 break;
201 }
202 launchBrowser(url);
203 num--;
204 }
205 }
206
207 @Override
208 protected String createInfoUrl(Object infoObject) {
209 User user = (User)infoObject;
210 try {
211 return getBaseUserUrl() + "/" + URLEncoder.encode(user.getName(), "UTF-8").replaceAll("\\+", "%20");
212 } catch(UnsupportedEncodingException e) {
213 e.printStackTrace();
214 JOptionPane.showMessageDialog(
215 Main.parent,
216 tr("<html>Failed to create an URL because the encoding ''{0}''<br>"
217 + "was missing on this system.</html>", "UTF-8"),
218 tr("Missing encoding"),
219 JOptionPane.ERROR_MESSAGE
220 );
221 return null;
222 }
223 }
224
225 @Override
226 protected void updateEnabledState() {
227 setEnabled(userTable != null && userTable.getSelectedRowCount() > 0);
228 }
229
230 public void valueChanged(ListSelectionEvent e) {
231 updateEnabledState();
232 }
233 }
234
235 class DoubleClickAdapter extends MouseAdapter {
236 @Override
237 public void mouseClicked(MouseEvent e) {
238 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount()==2) {
239 selectionUsersPrimitivesAction.select();
240 }
241 }
242 }
243
244 /**
245 * Action for selecting the primitives contributed by the currently selected
246 * users.
247 *
248 */
249 private static class UserInfo implements Comparable<UserInfo> {
250 public User user;
251 public int count;
252 public double percent;
253 UserInfo(User user, int count, double percent) {
254 this.user=user;
255 this.count=count;
256 this.percent = percent;
257 }
258 public int compareTo(UserInfo o) {
259 if (count < o.count) return 1;
260 if (count > o.count) return -1;
261 if (user== null || user.getName() == null) return 1;
262 if (o.user == null || o.user.getName() == null) return -1;
263 return user.getName().compareTo(o.user.getName());
264 }
265
266 public String getName() {
267 if (user == null)
268 return tr("<new object>");
269 return user.getName();
270 }
271 }
272
273 /**
274 * The table model for the users
275 *
276 */
277 static class UserTableModel extends DefaultTableModel {
278 private ArrayList<UserInfo> data;
279
280 public UserTableModel() {
281 setColumnIdentifiers(new String[]{tr("Author"),tr("# Objects"),"%"});
282 data = new ArrayList<UserInfo>();
283 }
284
285 protected Map<User, Integer> computeStatistics(Collection<? extends OsmPrimitive> primitives) {
286 HashMap<User, Integer> ret = new HashMap<User, Integer>();
287 if (primitives == null || primitives.isEmpty()) return ret;
288 for (OsmPrimitive primitive: primitives) {
289 if (ret.containsKey(primitive.getUser())) {
290 ret.put(primitive.getUser(), ret.get(primitive.getUser()) + 1);
291 } else {
292 ret.put(primitive.getUser(), 1);
293 }
294 }
295 return ret;
296 }
297
298 public void populate(Collection<? extends OsmPrimitive> primitives) {
299 Map<User,Integer> statistics = computeStatistics(primitives);
300 data.clear();
301 if (primitives != null) {
302 for (Map.Entry<User, Integer> entry: statistics.entrySet()) {
303 data.add(new UserInfo(entry.getKey(), entry.getValue(), (double)entry.getValue() / (double)primitives.size()));
304 }
305 }
306 Collections.sort(data);
307 fireTableDataChanged();
308 }
309
310 @Override
311 public int getRowCount() {
312 if (data == null) return 0;
313 return data.size();
314 }
315
316 @Override
317 public Object getValueAt(int row, int column) {
318 UserInfo info = data.get(row);
319 switch(column) {
320 case 0: /* author */ return info.getName() == null ? "" : info.getName();
321 case 1: /* count */ return info.count;
322 case 2: /* percent */ return NumberFormat.getPercentInstance().format(info.percent);
323 }
324 return null;
325 }
326
327 @Override
328 public boolean isCellEditable(int row, int column) {
329 return false;
330 }
331
332 public void selectPrimitivesOwnedBy(int [] rows) {
333 Set<User> users= new HashSet<User>();
334 for (int index: rows) {
335 users.add(data.get(index).user);
336 }
337 Collection<OsmPrimitive> selected = Main.main.getCurrentDataSet().getAllSelected();
338 Collection<OsmPrimitive> byUser = new LinkedList<OsmPrimitive>();
339 for (OsmPrimitive p : selected) {
340 if (users.contains(p.getUser())) {
341 byUser.add(p);
342 }
343 }
344 Main.main.getCurrentDataSet().setSelected(byUser);
345 }
346
347 public List<User> getSelectedUsers(int rows[]) {
348 LinkedList<User> ret = new LinkedList<User>();
349 if (rows == null || rows.length == 0) return ret;
350 for (int row: rows) {
351 if (data.get(row).user == null) {
352 continue;
353 }
354 ret.add(data.get(row).user);
355 }
356 return ret;
357 }
358 }
359 }