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