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