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.OsmPrimitive;
025import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator;
026import org.openstreetmap.josm.gui.DefaultNameFormatter;
027import org.openstreetmap.josm.gui.ExtendedDialog;
028import org.openstreetmap.josm.gui.NavigatableComponent;
029import org.openstreetmap.josm.gui.layer.OsmDataLayer;
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.widgets.JosmTextArea;
041import org.openstreetmap.josm.tools.GBC;
042import org.openstreetmap.josm.tools.WindowGeometry;
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<OsmPrimitive> primitives;
054    protected transient OsmDataLayer layer;
055    private boolean mappaintTabLoaded;
056    private boolean editcountTabLoaded;
057
058    /**
059     * Constructs a new {@code InspectPrimitiveDialog}.
060     * @param primitives collection of primitives
061     * @param layer data layer
062     */
063    public InspectPrimitiveDialog(final Collection<OsmPrimitive> primitives, OsmDataLayer layer) {
064        super(Main.parent, tr("Advanced object info"), new String[] {tr("Close")});
065        this.primitives = new ArrayList<>(primitives);
066        this.layer = layer;
067        setRememberWindowGeometry(getClass().getName() + ".geometry",
068                WindowGeometry.centerInWindow(Main.parent, new Dimension(750, 550)));
069
070        setButtonIcons(new String[]{"ok.png"});
071        final JTabbedPane tabs = new JTabbedPane();
072
073        tabs.addTab(tr("data"), genericMonospacePanel(new JPanel(), buildDataText(layer, 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(OsmDataLayer layer, List<OsmPrimitive> primitives) {
108        InspectPrimitiveDataText dt = new InspectPrimitiveDataText(layer);
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 = Main.getLayerManager().getEditDataSet().getAllSelected();
117        ElemStyles elemstyles = MapPaintStyles.getStyles();
118        NavigatableComponent nc = Main.map.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.equals(sc2) && sc1 != 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}