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