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.List; 013import java.util.Locale; 014import java.util.Map; 015import java.util.Map.Entry; 016import java.util.TreeMap; 017 018import javax.swing.JPanel; 019import javax.swing.JScrollPane; 020import javax.swing.JTabbedPane; 021import javax.swing.SingleSelectionModel; 022 023import org.openstreetmap.josm.Main; 024import org.openstreetmap.josm.data.osm.DataSet; 025import org.openstreetmap.josm.data.osm.DefaultNameFormatter; 026import org.openstreetmap.josm.data.osm.OsmPrimitive; 027import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator; 028import org.openstreetmap.josm.gui.ExtendedDialog; 029import org.openstreetmap.josm.gui.MainApplication; 030import org.openstreetmap.josm.gui.NavigatableComponent; 031import org.openstreetmap.josm.gui.mappaint.Cascade; 032import org.openstreetmap.josm.gui.mappaint.ElemStyles; 033import org.openstreetmap.josm.gui.mappaint.MapPaintStyles; 034import org.openstreetmap.josm.gui.mappaint.MultiCascade; 035import org.openstreetmap.josm.gui.mappaint.StyleCache; 036import org.openstreetmap.josm.gui.mappaint.StyleElementList; 037import org.openstreetmap.josm.gui.mappaint.StyleSource; 038import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 039import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement; 040import org.openstreetmap.josm.gui.util.GuiHelper; 041import org.openstreetmap.josm.gui.util.WindowGeometry; 042import org.openstreetmap.josm.gui.widgets.JosmTextArea; 043import org.openstreetmap.josm.tools.GBC; 044 045/** 046 * Panel to inspect one or more OsmPrimitives. 047 * 048 * Gives an unfiltered view of the object's internal state. 049 * Might be useful for power users to give more detailed bug reports and 050 * to better understand the JOSM data representation. 051 */ 052public class InspectPrimitiveDialog extends ExtendedDialog { 053 054 protected transient List<OsmPrimitive> primitives; 055 private boolean mappaintTabLoaded; 056 private boolean editcountTabLoaded; 057 058 /** 059 * Constructs a new {@code InspectPrimitiveDialog}. 060 * @param primitives collection of primitives 061 * @param data data set 062 * @since 12672 (signature) 063 */ 064 public InspectPrimitiveDialog(final Collection<OsmPrimitive> primitives, DataSet data) { 065 super(Main.parent, tr("Advanced object info"), tr("Close")); 066 this.primitives = new ArrayList<>(primitives); 067 setRememberWindowGeometry(getClass().getName() + ".geometry", 068 WindowGeometry.centerInWindow(Main.parent, new Dimension(750, 550))); 069 070 setButtonIcons("ok"); 071 final JTabbedPane tabs = new JTabbedPane(); 072 073 tabs.addTab(tr("data"), genericMonospacePanel(new JPanel(), buildDataText(data, this.primitives))); 074 075 final JPanel pMapPaint = new JPanel(); 076 tabs.addTab(tr("map style"), pMapPaint); 077 tabs.getModel().addChangeListener(e -> { 078 if (!mappaintTabLoaded && ((SingleSelectionModel) e.getSource()).getSelectedIndex() == 1) { 079 mappaintTabLoaded = true; 080 genericMonospacePanel(pMapPaint, buildMapPaintText()); 081 } 082 }); 083 084 final JPanel pEditCounts = new JPanel(); 085 tabs.addTab(tr("edit counts"), pEditCounts); 086 tabs.getModel().addChangeListener(e -> { 087 if (!editcountTabLoaded && ((SingleSelectionModel) e.getSource()).getSelectedIndex() == 2) { 088 editcountTabLoaded = true; 089 genericMonospacePanel(pEditCounts, buildListOfEditorsText(primitives)); 090 } 091 }); 092 093 setContent(tabs, false); 094 } 095 096 protected static JPanel genericMonospacePanel(JPanel p, String s) { 097 p.setLayout(new GridBagLayout()); 098 JosmTextArea jte = new JosmTextArea(); 099 jte.setFont(GuiHelper.getMonospacedFont(jte)); 100 jte.setEditable(false); 101 jte.append(s); 102 jte.setCaretPosition(0); 103 p.add(new JScrollPane(jte), GBC.std().fill()); 104 return p; 105 } 106 107 protected static String buildDataText(DataSet data, List<OsmPrimitive> primitives) { 108 InspectPrimitiveDataText dt = new InspectPrimitiveDataText(data); 109 primitives.stream() 110 .sorted(OsmPrimitiveComparator.orderingWaysRelationsNodes().thenComparing(OsmPrimitiveComparator.comparingNames())) 111 .forEachOrdered(dt::addPrimitive); 112 return dt.toString(); 113 } 114 115 protected static String buildMapPaintText() { 116 final Collection<OsmPrimitive> sel = MainApplication.getLayerManager().getActiveDataSet().getAllSelected(); 117 ElemStyles elemstyles = MapPaintStyles.getStyles(); 118 NavigatableComponent nc = MainApplication.getMap().mapView; 119 double scale = nc.getDist100Pixel(); 120 121 final StringBuilder txtMappaint = new StringBuilder(); 122 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock(); 123 try { 124 for (OsmPrimitive osm : sel) { 125 txtMappaint.append(tr("Styles Cache for \"{0}\":", osm.getDisplayName(DefaultNameFormatter.getInstance()))); 126 127 MultiCascade mc = new MultiCascade(); 128 129 for (StyleSource s : elemstyles.getStyleSources()) { 130 if (s.active) { 131 txtMappaint.append(tr("\n\n> applying {0} style \"{1}\"\n", getSort(s), s.getDisplayString())); 132 s.apply(mc, osm, scale, false); 133 txtMappaint.append(tr("\nRange:{0}", mc.range)); 134 for (Entry<String, Cascade> e : mc.getLayers()) { 135 txtMappaint.append("\n ").append(e.getKey()).append(": \n").append(e.getValue()); 136 } 137 } else { 138 txtMappaint.append(tr("\n\n> skipping \"{0}\" (not active)", s.getDisplayString())); 139 } 140 } 141 txtMappaint.append(tr("\n\nList of generated Styles:\n")); 142 StyleElementList sl = elemstyles.get(osm, scale, nc); 143 for (StyleElement s : sl) { 144 txtMappaint.append(" * ").append(s).append('\n'); 145 } 146 txtMappaint.append("\n\n"); 147 } 148 } finally { 149 MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock(); 150 } 151 if (sel.size() == 2) { 152 List<OsmPrimitive> selList = new ArrayList<>(sel); 153 StyleCache sc1 = selList.get(0).mappaintStyle; 154 StyleCache sc2 = selList.get(1).mappaintStyle; 155 if (sc1 == sc2) { 156 txtMappaint.append(tr("The 2 selected objects have identical style caches.")); 157 } 158 if (!sc1.equals(sc2)) { 159 txtMappaint.append(tr("The 2 selected objects have different style caches.")); 160 } 161 if (sc1 != sc2 && sc1.equals(sc2)) { 162 txtMappaint.append(tr("Warning: The 2 selected objects have equal, but not identical style caches.")); 163 } 164 } 165 return txtMappaint.toString(); 166 } 167 168 /* Future Ideas: 169 Calculate the most recent edit date from o.getTimestamp(). 170 Sort by the count for presentation, so the most active editors are on top. 171 Count only tagged nodes (so empty way nodes don't inflate counts). 172 */ 173 protected static String buildListOfEditorsText(Iterable<OsmPrimitive> primitives) { 174 final Map<String, Integer> editCountByUser = new TreeMap<>(Collator.getInstance(Locale.getDefault())); 175 176 // Count who edited each selected object 177 for (OsmPrimitive o : primitives) { 178 if (o.getUser() != null) { 179 String username = o.getUser().getName(); 180 Integer oldCount = editCountByUser.get(username); 181 if (oldCount == null) { 182 editCountByUser.put(username, 1); 183 } else { 184 editCountByUser.put(username, oldCount + 1); 185 } 186 } 187 } 188 189 // Print the count in sorted order 190 final StringBuilder s = new StringBuilder(48) 191 .append(trn("{0} user last edited the selection:", "{0} users last edited the selection:", 192 editCountByUser.size(), editCountByUser.size())) 193 .append("\n\n"); 194 for (Map.Entry<String, Integer> entry : editCountByUser.entrySet()) { 195 final String username = entry.getKey(); 196 final Integer editCount = entry.getValue(); 197 s.append(String.format("%6d %s", editCount, username)).append('\n'); 198 } 199 return s.toString(); 200 } 201 202 private static String getSort(StyleSource s) { 203 if (s instanceof MapCSSStyleSource) { 204 return tr("mapcss"); 205 } else { 206 return tr("unknown"); 207 } 208 } 209}