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