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