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.conflict.Conflict;
028import org.openstreetmap.josm.data.coor.EastNorth;
029import org.openstreetmap.josm.data.osm.BBox;
030import org.openstreetmap.josm.data.osm.Node;
031import org.openstreetmap.josm.data.osm.OsmPrimitive;
032import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator;
033import org.openstreetmap.josm.data.osm.Relation;
034import org.openstreetmap.josm.data.osm.RelationMember;
035import org.openstreetmap.josm.data.osm.Way;
036import org.openstreetmap.josm.gui.DefaultNameFormatter;
037import org.openstreetmap.josm.gui.ExtendedDialog;
038import org.openstreetmap.josm.gui.NavigatableComponent;
039import org.openstreetmap.josm.gui.layer.OsmDataLayer;
040import org.openstreetmap.josm.gui.mappaint.Cascade;
041import org.openstreetmap.josm.gui.mappaint.ElemStyles;
042import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
043import org.openstreetmap.josm.gui.mappaint.MultiCascade;
044import org.openstreetmap.josm.gui.mappaint.StyleCache;
045import org.openstreetmap.josm.gui.mappaint.StyleElementList;
046import org.openstreetmap.josm.gui.mappaint.StyleSource;
047import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
048import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
049import org.openstreetmap.josm.gui.util.GuiHelper;
050import org.openstreetmap.josm.gui.widgets.JosmTextArea;
051import org.openstreetmap.josm.tools.GBC;
052import org.openstreetmap.josm.tools.Geometry;
053import org.openstreetmap.josm.tools.WindowGeometry;
054import org.openstreetmap.josm.tools.date.DateUtils;
055
056/**
057 * Panel to inspect one or more OsmPrimitives.
058 *
059 * Gives an unfiltered view of the object's internal state.
060 * Might be useful for power users to give more detailed bug reports and
061 * to better understand the JOSM data representation.
062 */
063public class InspectPrimitiveDialog extends ExtendedDialog {
064
065    protected transient List<OsmPrimitive> primitives;
066    protected transient OsmDataLayer layer;
067    private boolean mappaintTabLoaded;
068    private boolean editcountTabLoaded;
069
070    public InspectPrimitiveDialog(Collection<OsmPrimitive> primitives, OsmDataLayer layer) {
071        super(Main.parent, tr("Advanced object info"), new String[] {tr("Close")});
072        this.primitives = new ArrayList<>(primitives);
073        this.layer = layer;
074        setRememberWindowGeometry(getClass().getName() + ".geometry",
075                WindowGeometry.centerInWindow(Main.parent, new Dimension(750, 550)));
076
077        setButtonIcons(new String[]{"ok.png"});
078        final JTabbedPane tabs = new JTabbedPane();
079
080        tabs.addTab(tr("data"), genericMonospacePanel(new JPanel(), buildDataText()));
081
082        final JPanel pMapPaint = new JPanel();
083        tabs.addTab(tr("map style"), pMapPaint);
084        tabs.getModel().addChangeListener(new ChangeListener() {
085
086            @Override
087            public void stateChanged(ChangeEvent e) {
088                if (!mappaintTabLoaded && ((SingleSelectionModel) e.getSource()).getSelectedIndex() == 1) {
089                    mappaintTabLoaded = true;
090                    genericMonospacePanel(pMapPaint, buildMapPaintText());
091                }
092            }
093        });
094
095        final JPanel pEditCounts = new JPanel();
096        tabs.addTab(tr("edit counts"), pEditCounts);
097        tabs.getModel().addChangeListener(new ChangeListener() {
098
099            @Override
100            public void stateChanged(ChangeEvent e) {
101                if (!editcountTabLoaded && ((SingleSelectionModel) e.getSource()).getSelectedIndex() == 2) {
102                    editcountTabLoaded = true;
103                    genericMonospacePanel(pEditCounts, buildListOfEditorsText());
104                }
105            }
106        });
107
108        setContent(tabs, false);
109    }
110
111    protected JPanel genericMonospacePanel(JPanel p, String s) {
112        p.setLayout(new GridBagLayout());
113        JosmTextArea jte = new JosmTextArea();
114        jte.setFont(GuiHelper.getMonospacedFont(jte));
115        jte.setEditable(false);
116        jte.append(s);
117        p.add(new JScrollPane(jte), GBC.std().fill());
118        return p;
119    }
120
121    protected String buildDataText() {
122        DataText dt = new DataText();
123        Collections.sort(primitives, new OsmPrimitiveComparator());
124        for (OsmPrimitive o : primitives) {
125            dt.addPrimitive(o);
126        }
127        return dt.toString();
128    }
129
130    class DataText {
131        private static final String INDENT = "  ";
132        private static final char NL = '\n';
133
134        private final StringBuilder s = new StringBuilder();
135
136        private DataText add(String title, String... values) {
137            s.append(INDENT).append(title);
138            for (String v : values) {
139                s.append(v);
140            }
141            s.append(NL);
142            return this;
143        }
144
145        private String getNameAndId(String name, long id) {
146            if (name != null) {
147                return name + tr(" ({0})", /* sic to avoid thousand seperators */ Long.toString(id));
148            } else {
149                return Long.toString(id);
150            }
151        }
152
153        public void addPrimitive(OsmPrimitive o) {
154
155            addHeadline(o);
156
157            if (!(o.getDataSet() != null && o.getDataSet().getPrimitiveById(o) != null)) {
158                s.append(NL).append(INDENT).append(tr("not in data set")).append(NL);
159                return;
160            }
161            if (o.isIncomplete()) {
162                s.append(NL).append(INDENT).append(tr("incomplete")).append(NL);
163                return;
164            }
165            s.append(NL);
166
167            addState(o);
168            addCommon(o);
169            addAttributes(o);
170            addSpecial(o);
171            addReferrers(s, o);
172            addConflicts(o);
173            s.append(NL);
174        }
175
176        void addHeadline(OsmPrimitive o) {
177            addType(o);
178            addNameAndId(o);
179        }
180
181        void addType(OsmPrimitive o) {
182            if (o instanceof Node) {
183                s.append(tr("Node: "));
184            } else if (o instanceof Way) {
185                s.append(tr("Way: "));
186            } else if (o instanceof Relation) {
187                s.append(tr("Relation: "));
188            }
189        }
190
191        void addNameAndId(OsmPrimitive o) {
192            String name = o.get("name");
193            if (name == null) {
194                s.append(o.getUniqueId());
195            } else {
196                s.append(getNameAndId(name, o.getUniqueId()));
197            }
198        }
199
200        void addState(OsmPrimitive o) {
201            StringBuilder sb = new StringBuilder(INDENT);
202            /* selected state is left out: not interesting as it is always selected */
203            if (o.isDeleted()) {
204                sb.append(tr("deleted")).append(INDENT);
205            }
206            if (!o.isVisible()) {
207                sb.append(tr("deleted-on-server")).append(INDENT);
208            }
209            if (o.isModified()) {
210                sb.append(tr("modified")).append(INDENT);
211            }
212            if (o.isDisabledAndHidden()) {
213                sb.append(tr("filtered/hidden")).append(INDENT);
214            }
215            if (o.isDisabled()) {
216                sb.append(tr("filtered/disabled")).append(INDENT);
217            }
218            if (o.hasDirectionKeys()) {
219                if (o.reversedDirection()) {
220                    sb.append(tr("has direction keys (reversed)")).append(INDENT);
221                } else {
222                    sb.append(tr("has direction keys")).append(INDENT);
223                }
224            }
225            String state = sb.toString().trim();
226            if (!state.isEmpty()) {
227                add(tr("State: "), sb.toString().trim());
228            }
229        }
230
231        void addCommon(OsmPrimitive o) {
232            add(tr("Data Set: "), Integer.toHexString(o.getDataSet().hashCode()));
233            add(tr("Edited at: "), o.isTimestampEmpty() ? tr("<new object>")
234                    : DateUtils.fromTimestamp(o.getRawTimestamp()));
235            add(tr("Edited by: "), o.getUser() == null ? tr("<new object>")
236                    : getNameAndId(o.getUser().getName(), o.getUser().getId()));
237            add(tr("Version: "), Integer.toString(o.getVersion()));
238            add(tr("In changeset: "), Integer.toString(o.getChangesetId()));
239        }
240
241        void addAttributes(OsmPrimitive o) {
242            if (o.hasKeys()) {
243                add(tr("Tags: "));
244                for (String key : o.keySet()) {
245                    s.append(INDENT).append(INDENT);
246                    s.append(String.format("\"%s\"=\"%s\"%n", key, o.get(key)));
247                }
248            }
249        }
250
251        void addSpecial(OsmPrimitive o) {
252            if (o instanceof Node) {
253                addCoordinates((Node) o);
254            } else if (o instanceof Way) {
255                addBbox(o);
256                add(tr("Centroid: "), Main.getProjection().eastNorth2latlon(
257                        Geometry.getCentroid(((Way) o).getNodes())).toStringCSV(", "));
258                addWayNodes((Way) o);
259            } else if (o instanceof Relation) {
260                addBbox(o);
261                addRelationMembers((Relation) o);
262            }
263        }
264
265        void addRelationMembers(Relation r) {
266            add(trn("{0} Member: ", "{0} Members: ", r.getMembersCount(), r.getMembersCount()));
267            for (RelationMember m : r.getMembers()) {
268                s.append(INDENT).append(INDENT);
269                addHeadline(m.getMember());
270                s.append(tr(" as \"{0}\"", m.getRole()));
271                s.append(NL);
272            }
273        }
274
275        void addWayNodes(Way w) {
276            add(tr("{0} Nodes: ", w.getNodesCount()));
277            for (Node n : w.getNodes()) {
278                s.append(INDENT).append(INDENT);
279                addNameAndId(n);
280                s.append(NL);
281            }
282        }
283
284        void addBbox(OsmPrimitive o) {
285            BBox bbox = o.getBBox();
286            if (bbox != null) {
287                add(tr("Bounding box: "), bbox.toStringCSV(", "));
288                EastNorth bottomRigth = Main.getProjection().latlon2eastNorth(bbox.getBottomRight());
289                EastNorth topLeft = Main.getProjection().latlon2eastNorth(bbox.getTopLeft());
290                add(tr("Bounding box (projected): "),
291                        Double.toString(topLeft.east()), ", ",
292                        Double.toString(bottomRigth.north()), ", ",
293                        Double.toString(bottomRigth.east()), ", ",
294                        Double.toString(topLeft.north()));
295                add(tr("Center of bounding box: "), bbox.getCenter().toStringCSV(", "));
296            }
297        }
298
299        void addCoordinates(Node n) {
300            if (n.getCoor() != null) {
301                add(tr("Coordinates: "),
302                        Double.toString(n.getCoor().lat()), ", ",
303                        Double.toString(n.getCoor().lon()));
304                add(tr("Coordinates (projected): "),
305                        Double.toString(n.getEastNorth().east()), ", ",
306                        Double.toString(n.getEastNorth().north()));
307            }
308        }
309
310        void addReferrers(StringBuilder s, OsmPrimitive o) {
311            List<OsmPrimitive> refs = o.getReferrers();
312            if (!refs.isEmpty()) {
313                add(tr("Part of: "));
314                for (OsmPrimitive p : refs) {
315                    s.append(INDENT).append(INDENT);
316                    addHeadline(p);
317                    s.append(NL);
318                }
319            }
320        }
321
322        void addConflicts(OsmPrimitive o) {
323            Conflict<?> c = layer.getConflicts().getConflictForMy(o);
324            if (c != null) {
325                add(tr("In conflict with: "));
326                addNameAndId(c.getTheir());
327            }
328        }
329
330        @Override
331        public String toString() {
332            return s.toString();
333        }
334    }
335
336    protected String buildMapPaintText() {
337        final Collection<OsmPrimitive> sel = Main.main.getCurrentDataSet().getAllSelected();
338        ElemStyles elemstyles = MapPaintStyles.getStyles();
339        NavigatableComponent nc = Main.map.mapView;
340        double scale = nc.getDist100Pixel();
341
342        final StringBuilder txtMappaint = new StringBuilder();
343        MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
344        try {
345            for (OsmPrimitive osm : sel) {
346                txtMappaint.append(tr("Styles Cache for \"{0}\":", osm.getDisplayName(DefaultNameFormatter.getInstance())));
347
348                MultiCascade mc = new MultiCascade();
349
350                for (StyleSource s : elemstyles.getStyleSources()) {
351                    if (s.active) {
352                        txtMappaint.append(tr("\n\n> applying {0} style \"{1}\"\n", getSort(s), s.getDisplayString()));
353                        s.apply(mc, osm, scale, false);
354                        txtMappaint.append(tr("\nRange:{0}", mc.range));
355                        for (Entry<String, Cascade> e : mc.getLayers()) {
356                            txtMappaint.append("\n ").append(e.getKey()).append(": \n").append(e.getValue());
357                        }
358                    } else {
359                        txtMappaint.append(tr("\n\n> skipping \"{0}\" (not active)", s.getDisplayString()));
360                    }
361                }
362                txtMappaint.append(tr("\n\nList of generated Styles:\n"));
363                StyleElementList sl = elemstyles.get(osm, scale, nc);
364                for (StyleElement s : sl) {
365                    txtMappaint.append(" * ").append(s).append('\n');
366                }
367                txtMappaint.append("\n\n");
368            }
369        } finally {
370            MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().unlock();
371        }
372        if (sel.size() == 2) {
373            List<OsmPrimitive> selList = new ArrayList<>(sel);
374            StyleCache sc1 = selList.get(0).mappaintStyle;
375            StyleCache sc2 = selList.get(1).mappaintStyle;
376            if (sc1 == sc2) {
377                txtMappaint.append(tr("The 2 selected objects have identical style caches."));
378            }
379            if (!sc1.equals(sc2)) {
380                txtMappaint.append(tr("The 2 selected objects have different style caches."));
381            }
382            if (sc1.equals(sc2) && sc1 != sc2) {
383                txtMappaint.append(tr("Warning: The 2 selected objects have equal, but not identical style caches."));
384            }
385        }
386        return txtMappaint.toString();
387    }
388
389    /*  Future Ideas:
390        Calculate the most recent edit date from o.getTimestamp().
391        Sort by the count for presentation, so the most active editors are on top.
392        Count only tagged nodes (so empty way nodes don't inflate counts).
393    */
394    protected String buildListOfEditorsText() {
395        final StringBuilder s = new StringBuilder();
396        final Map<String, Integer> editCountByUser = new TreeMap<>(Collator.getInstance(Locale.getDefault()));
397
398        // Count who edited each selected object
399        for (OsmPrimitive o : primitives) {
400            if (o.getUser() != null) {
401                String username = o.getUser().getName();
402                Integer oldCount = editCountByUser.get(username);
403                if (oldCount == null) {
404                    editCountByUser.put(username, 1);
405                } else {
406                    editCountByUser.put(username, oldCount + 1);
407                }
408            }
409        }
410
411        // Print the count in sorted order
412        s.append(trn("{0} user last edited the selection:", "{0} users last edited the selection:",
413                editCountByUser.size(), editCountByUser.size()));
414        s.append("\n\n");
415        for (Map.Entry<String, Integer> entry : editCountByUser.entrySet()) {
416            final String username = entry.getKey();
417            final Integer editCount = entry.getValue();
418            s.append(String.format("%6d  %s%n", editCount, username));
419        }
420        return s.toString();
421    }
422
423    private static String getSort(StyleSource s) {
424        if (s instanceof MapCSSStyleSource) {
425            return tr("mapcss");
426        } else {
427            return tr("unknown");
428        }
429    }
430}