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.Dimension; 008import java.awt.GridBagLayout; 009import java.text.Collator; 010import java.util.ArrayList; 011import java.util.Collection; 012import java.util.Collections; 013import java.util.List; 014import java.util.Locale; 015import java.util.Map; 016import java.util.Map.Entry; 017import java.util.TreeMap; 018 019import javax.swing.JPanel; 020import javax.swing.JScrollPane; 021import javax.swing.JTabbedPane; 022import javax.swing.SingleSelectionModel; 023import javax.swing.event.ChangeEvent; 024import javax.swing.event.ChangeListener; 025 026import org.openstreetmap.josm.Main; 027import org.openstreetmap.josm.data.conflict.Conflict; 028import org.openstreetmap.josm.data.coor.EastNorth; 029import org.openstreetmap.josm.data.osm.BBox; 030import org.openstreetmap.josm.data.osm.Node; 031import org.openstreetmap.josm.data.osm.OsmPrimitive; 032import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator; 033import org.openstreetmap.josm.data.osm.Relation; 034import org.openstreetmap.josm.data.osm.RelationMember; 035import org.openstreetmap.josm.data.osm.Way; 036import org.openstreetmap.josm.gui.DefaultNameFormatter; 037import org.openstreetmap.josm.gui.ExtendedDialog; 038import org.openstreetmap.josm.gui.NavigatableComponent; 039import org.openstreetmap.josm.gui.layer.OsmDataLayer; 040import org.openstreetmap.josm.gui.mappaint.Cascade; 041import org.openstreetmap.josm.gui.mappaint.ElemStyle; 042import org.openstreetmap.josm.gui.mappaint.ElemStyles; 043import org.openstreetmap.josm.gui.mappaint.MapPaintStyles; 044import org.openstreetmap.josm.gui.mappaint.MultiCascade; 045import org.openstreetmap.josm.gui.mappaint.StyleCache; 046import org.openstreetmap.josm.gui.mappaint.StyleCache.StyleList; 047import org.openstreetmap.josm.gui.mappaint.StyleSource; 048import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 049import org.openstreetmap.josm.gui.mappaint.xml.XmlStyleSource; 050import org.openstreetmap.josm.gui.util.GuiHelper; 051import org.openstreetmap.josm.gui.widgets.JosmTextArea; 052import org.openstreetmap.josm.tools.GBC; 053import org.openstreetmap.josm.tools.Geometry; 054import org.openstreetmap.josm.tools.WindowGeometry; 055import org.openstreetmap.josm.tools.date.DateUtils; 056 057/** 058 * Panel to inspect one or more OsmPrimitives. 059 * 060 * Gives an unfiltered view of the object's internal state. 061 * Might be useful for power users to give more detailed bug reports and 062 * to better understand the JOSM data representation. 063 */ 064public class InspectPrimitiveDialog extends ExtendedDialog { 065 066 protected transient List<OsmPrimitive> primitives; 067 protected transient OsmDataLayer layer; 068 private boolean mappaintTabLoaded; 069 private boolean editcountTabLoaded; 070 071 public InspectPrimitiveDialog(Collection<OsmPrimitive> primitives, OsmDataLayer layer) { 072 super(Main.parent, tr("Advanced object info"), new String[] {tr("Close")}); 073 this.primitives = new ArrayList<>(primitives); 074 this.layer = layer; 075 setRememberWindowGeometry(getClass().getName() + ".geometry", 076 WindowGeometry.centerInWindow(Main.parent, new Dimension(750, 550))); 077 078 setButtonIcons(new String[]{"ok.png"}); 079 final JTabbedPane tabs = new JTabbedPane(); 080 081 tabs.addTab(tr("data"), genericMonospacePanel(new JPanel(), buildDataText())); 082 083 final JPanel pMapPaint = new JPanel(); 084 tabs.addTab(tr("map style"), pMapPaint); 085 tabs.getModel().addChangeListener(new ChangeListener() { 086 087 @Override 088 public void stateChanged(ChangeEvent e) { 089 if (!mappaintTabLoaded && ((SingleSelectionModel) e.getSource()).getSelectedIndex() == 1) { 090 mappaintTabLoaded = true; 091 genericMonospacePanel(pMapPaint, buildMapPaintText()); 092 } 093 } 094 }); 095 096 final JPanel pEditCounts = new JPanel(); 097 tabs.addTab(tr("edit counts"), pEditCounts); 098 tabs.getModel().addChangeListener(new ChangeListener() { 099 100 @Override 101 public void stateChanged(ChangeEvent e) { 102 if (!editcountTabLoaded && ((SingleSelectionModel) e.getSource()).getSelectedIndex() == 2) { 103 editcountTabLoaded = true; 104 genericMonospacePanel(pEditCounts, buildListOfEditorsText()); 105 } 106 } 107 }); 108 109 setContent(tabs, false); 110 } 111 112 protected JPanel genericMonospacePanel(JPanel p, String s) { 113 p.setLayout(new GridBagLayout()); 114 JosmTextArea jte = new JosmTextArea(); 115 jte.setFont(GuiHelper.getMonospacedFont(jte)); 116 jte.setEditable(false); 117 jte.append(s); 118 p.add(new JScrollPane(jte), GBC.std().fill()); 119 return p; 120 } 121 122 protected String buildDataText() { 123 DataText dt = new DataText(); 124 Collections.sort(primitives, new OsmPrimitiveComparator()); 125 for (OsmPrimitive o : primitives) { 126 dt.addPrimitive(o); 127 } 128 return dt.toString(); 129 } 130 131 class DataText { 132 private static final String INDENT = " "; 133 private static final char NL = '\n'; 134 135 private StringBuilder s = new StringBuilder(); 136 137 private DataText add(String title, String... values) { 138 s.append(INDENT).append(title); 139 for (String v : values) { 140 s.append(v); 141 } 142 s.append(NL); 143 return this; 144 } 145 146 private String getNameAndId(String name, long id) { 147 if (name != null) { 148 return name + tr(" ({0})", /* sic to avoid thousand seperators */ Long.toString(id)); 149 } else { 150 return Long.toString(id); 151 } 152 } 153 154 public void addPrimitive(OsmPrimitive o) { 155 156 addHeadline(o); 157 158 if (!(o.getDataSet() != null && o.getDataSet().getPrimitiveById(o) != null)) { 159 s.append(NL).append(INDENT).append(tr("not in data set")).append(NL); 160 return; 161 } 162 if (o.isIncomplete()) { 163 s.append(NL).append(INDENT).append(tr("incomplete")).append(NL); 164 return; 165 } 166 s.append(NL); 167 168 addState(o); 169 addCommon(o); 170 addAttributes(o); 171 addSpecial(o); 172 addReferrers(s, o); 173 addConflicts(o); 174 s.append(NL); 175 } 176 177 void addHeadline(OsmPrimitive o) { 178 addType(o); 179 addNameAndId(o); 180 } 181 182 void addType(OsmPrimitive o) { 183 if (o instanceof Node) { 184 s.append(tr("Node: ")); 185 } else if (o instanceof Way) { 186 s.append(tr("Way: ")); 187 } else if (o instanceof Relation) { 188 s.append(tr("Relation: ")); 189 } 190 } 191 192 void addNameAndId(OsmPrimitive o) { 193 String name = o.get("name"); 194 if (name == null) { 195 s.append(o.getUniqueId()); 196 } else { 197 s.append(getNameAndId(name, o.getUniqueId())); 198 } 199 } 200 201 void addState(OsmPrimitive o) { 202 StringBuilder sb = new StringBuilder(INDENT); 203 /* selected state is left out: not interesting as it is always selected */ 204 if (o.isDeleted()) { 205 sb.append(tr("deleted")).append(INDENT); 206 } 207 if (!o.isVisible()) { 208 sb.append(tr("deleted-on-server")).append(INDENT); 209 } 210 if (o.isModified()) { 211 sb.append(tr("modified")).append(INDENT); 212 } 213 if (o.isDisabledAndHidden()) { 214 sb.append(tr("filtered/hidden")).append(INDENT); 215 } 216 if (o.isDisabled()) { 217 sb.append(tr("filtered/disabled")).append(INDENT); 218 } 219 if (o.hasDirectionKeys()) { 220 if (o.reversedDirection()) { 221 sb.append(tr("has direction keys (reversed)")).append(INDENT); 222 } else { 223 sb.append(tr("has direction keys")).append(INDENT); 224 } 225 } 226 String state = sb.toString().trim(); 227 if (!state.isEmpty()) { 228 add(tr("State: "), sb.toString().trim()); 229 } 230 } 231 232 void addCommon(OsmPrimitive o) { 233 add(tr("Data Set: "), Integer.toHexString(o.getDataSet().hashCode())); 234 add(tr("Edited at: "), o.isTimestampEmpty() ? tr("<new object>") 235 : DateUtils.fromTimestamp(o.getRawTimestamp())); 236 add(tr("Edited by: "), o.getUser() == null ? tr("<new object>") 237 : getNameAndId(o.getUser().getName(), o.getUser().getId())); 238 add(tr("Version: "), Integer.toString(o.getVersion())); 239 add(tr("In changeset: "), Integer.toString(o.getChangesetId())); 240 } 241 242 void addAttributes(OsmPrimitive o) { 243 if (o.hasKeys()) { 244 add(tr("Tags: ")); 245 for (String key : o.keySet()) { 246 s.append(INDENT).append(INDENT); 247 s.append(String.format("\"%s\"=\"%s\"%n", key, o.get(key))); 248 } 249 } 250 } 251 252 void addSpecial(OsmPrimitive o) { 253 if (o instanceof Node) { 254 addCoordinates((Node) o); 255 } else if (o instanceof Way) { 256 addBbox(o); 257 add(tr("Centroid: "), Main.getProjection().eastNorth2latlon( 258 Geometry.getCentroid(((Way) o).getNodes())).toStringCSV(", ")); 259 addWayNodes((Way) o); 260 } else if (o instanceof Relation) { 261 addBbox(o); 262 addRelationMembers((Relation) o); 263 } 264 } 265 266 void addRelationMembers(Relation r) { 267 add(trn("{0} Member: ", "{0} Members: ", r.getMembersCount(), r.getMembersCount())); 268 for (RelationMember m : r.getMembers()) { 269 s.append(INDENT).append(INDENT); 270 addHeadline(m.getMember()); 271 s.append(tr(" as \"{0}\"", m.getRole())); 272 s.append(NL); 273 } 274 } 275 276 void addWayNodes(Way w) { 277 add(tr("{0} Nodes: ", w.getNodesCount())); 278 for (Node n : w.getNodes()) { 279 s.append(INDENT).append(INDENT); 280 addNameAndId(n); 281 s.append(NL); 282 } 283 } 284 285 void addBbox(OsmPrimitive o) { 286 BBox bbox = o.getBBox(); 287 if (bbox != null) { 288 add(tr("Bounding box: "), bbox.toStringCSV(", ")); 289 EastNorth bottomRigth = Main.getProjection().latlon2eastNorth(bbox.getBottomRight()); 290 EastNorth topLeft = Main.getProjection().latlon2eastNorth(bbox.getTopLeft()); 291 add(tr("Bounding box (projected): "), 292 Double.toString(topLeft.east()), ", ", 293 Double.toString(bottomRigth.north()), ", ", 294 Double.toString(bottomRigth.east()), ", ", 295 Double.toString(topLeft.north())); 296 add(tr("Center of bounding box: "), bbox.getCenter().toStringCSV(", ")); 297 } 298 } 299 300 void addCoordinates(Node n) { 301 if (n.getCoor() != null) { 302 add(tr("Coordinates: "), 303 Double.toString(n.getCoor().lat()), ", ", 304 Double.toString(n.getCoor().lon())); 305 add(tr("Coordinates (projected): "), 306 Double.toString(n.getEastNorth().east()), ", ", 307 Double.toString(n.getEastNorth().north())); 308 } 309 } 310 311 void addReferrers(StringBuilder s, OsmPrimitive o) { 312 List<OsmPrimitive> refs = o.getReferrers(); 313 if (!refs.isEmpty()) { 314 add(tr("Part of: ")); 315 for (OsmPrimitive p : refs) { 316 s.append(INDENT).append(INDENT); 317 addHeadline(p); 318 s.append(NL); 319 } 320 } 321 } 322 323 void addConflicts(OsmPrimitive o) { 324 Conflict<?> c = layer.getConflicts().getConflictForMy(o); 325 if (c != null) { 326 add(tr("In conflict with: ")); 327 addNameAndId(c.getTheir()); 328 } 329 } 330 331 @Override 332 public String toString() { 333 return s.toString(); 334 } 335 } 336 337 protected String buildMapPaintText() { 338 final Collection<OsmPrimitive> sel = Main.main.getCurrentDataSet().getAllSelected(); 339 ElemStyles elemstyles = MapPaintStyles.getStyles(); 340 NavigatableComponent nc = Main.map.mapView; 341 double scale = nc.getDist100Pixel(); 342 343 final StringBuilder txtMappaint = new StringBuilder(); 344 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock(); 345 try { 346 for (OsmPrimitive osm : sel) { 347 txtMappaint.append(tr("Styles Cache for \"{0}\":", osm.getDisplayName(DefaultNameFormatter.getInstance()))); 348 349 MultiCascade mc = new MultiCascade(); 350 351 for (StyleSource s : elemstyles.getStyleSources()) { 352 if (s.active) { 353 txtMappaint.append(tr("\n\n> applying {0} style \"{1}\"\n", getSort(s), s.getDisplayString())); 354 s.apply(mc, osm, scale, false); 355 txtMappaint.append(tr("\nRange:{0}", mc.range)); 356 for (Entry<String, Cascade> e : mc.getLayers()) { 357 txtMappaint.append("\n ").append(e.getKey()).append(": \n").append(e.getValue()); 358 } 359 } else { 360 txtMappaint.append(tr("\n\n> skipping \"{0}\" (not active)", s.getDisplayString())); 361 } 362 } 363 txtMappaint.append(tr("\n\nList of generated Styles:\n")); 364 StyleList sl = elemstyles.get(osm, scale, nc); 365 for (ElemStyle s : sl) { 366 txtMappaint.append(" * ").append(s).append('\n'); 367 } 368 txtMappaint.append("\n\n"); 369 } 370 } finally { 371 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock(); 372 } 373 if (sel.size() == 2) { 374 List<OsmPrimitive> selList = new ArrayList<>(sel); 375 StyleCache sc1 = selList.get(0).mappaintStyle; 376 StyleCache sc2 = selList.get(1).mappaintStyle; 377 if (sc1 == sc2) { 378 txtMappaint.append(tr("The 2 selected objects have identical style caches.")); 379 } 380 if (!sc1.equals(sc2)) { 381 txtMappaint.append(tr("The 2 selected objects have different style caches.")); 382 } 383 if (sc1.equals(sc2) && sc1 != sc2) { 384 txtMappaint.append(tr("Warning: The 2 selected objects have equal, but not identical style caches.")); 385 } 386 } 387 return txtMappaint.toString(); 388 } 389 390 /* Future Ideas: 391 Calculate the most recent edit date from o.getTimestamp(). 392 Sort by the count for presentation, so the most active editors are on top. 393 Count only tagged nodes (so empty way nodes don't inflate counts). 394 */ 395 protected String buildListOfEditorsText() { 396 final StringBuilder s = new StringBuilder(); 397 final Map<String, Integer> editCountByUser = new TreeMap<>(Collator.getInstance(Locale.getDefault())); 398 399 // Count who edited each selected object 400 for (OsmPrimitive o : primitives) { 401 if (o.getUser() != null) { 402 String username = o.getUser().getName(); 403 Integer oldCount = editCountByUser.get(username); 404 if (oldCount == null) { 405 editCountByUser.put(username, 1); 406 } else { 407 editCountByUser.put(username, oldCount + 1); 408 } 409 } 410 } 411 412 // Print the count in sorted order 413 s.append(trn("{0} user last edited the selection:", "{0} users last edited the selection:", 414 editCountByUser.size(), editCountByUser.size())); 415 s.append("\n\n"); 416 for (Map.Entry<String, Integer> entry : editCountByUser.entrySet()) { 417 final String username = entry.getKey(); 418 final Integer editCount = entry.getValue(); 419 s.append(String.format("%6d %s%n", editCount, username)); 420 } 421 return s.toString(); 422 } 423 424 private static String getSort(StyleSource s) { 425 if (s instanceof XmlStyleSource) { 426 return tr("xml"); 427 } else if (s instanceof MapCSSStyleSource) { 428 return tr("mapcss"); 429 } else { 430 return tr("unknown"); 431 } 432 } 433}