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