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