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