// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.dialogs;
import static org.openstreetmap.josm.tools.I18n.tr;
import static org.openstreetmap.josm.tools.I18n.trn;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagLayout;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.SingleSelectionModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.conflict.Conflict;
import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.osm.BBox;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.DefaultNameFormatter;
import org.openstreetmap.josm.gui.ExtendedDialog;
import org.openstreetmap.josm.gui.NavigatableComponent;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.gui.mappaint.Cascade;
import org.openstreetmap.josm.gui.mappaint.ElemStyle;
import org.openstreetmap.josm.gui.mappaint.ElemStyles;
import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
import org.openstreetmap.josm.gui.mappaint.MultiCascade;
import org.openstreetmap.josm.gui.mappaint.StyleCache;
import org.openstreetmap.josm.gui.mappaint.StyleCache.StyleList;
import org.openstreetmap.josm.gui.mappaint.StyleSource;
import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
import org.openstreetmap.josm.gui.mappaint.xml.XmlStyleSource;
import org.openstreetmap.josm.gui.widgets.JosmTextArea;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.Geometry;
import org.openstreetmap.josm.tools.WindowGeometry;
import org.openstreetmap.josm.tools.date.DateUtils;
/**
* Panel to inspect one or more OsmPrimitives.
*
* Gives an unfiltered view of the object's internal state.
* Might be useful for power users to give more detailed bug reports and
* to better understand the JOSM data representation.
*/
public class InspectPrimitiveDialog extends ExtendedDialog {
protected List<OsmPrimitive> primitives;
protected OsmDataLayer layer;
private JosmTextArea txtMappaint;
boolean mappaintTabLoaded;
public InspectPrimitiveDialog(Collection<OsmPrimitive> primitives, OsmDataLayer layer) {
super(Main.parent, tr("Advanced object info"), new String[] {tr("Close")});
this.primitives = new ArrayList<>(primitives);
this.layer = layer;
setRememberWindowGeometry(getClass().getName() + ".geometry",
WindowGeometry.centerInWindow(Main.parent, new Dimension(750, 550)));
setButtonIcons(new String[]{"ok.png"});
final JTabbedPane tabs = new JTabbedPane();
JPanel pData = buildDataPanel();
tabs.addTab(tr("data"), pData);
final JPanel pMapPaint = new JPanel();
tabs.addTab(tr("map style"), pMapPaint);
tabs.getModel().addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
if (!mappaintTabLoaded && ((SingleSelectionModel) e.getSource()).getSelectedIndex() == 1) {
mappaintTabLoaded = true;
buildMapPaintPanel(pMapPaint);
createMapPaintText();
}
}
});
setContent(tabs, false);
}
protected JPanel buildDataPanel() {
JPanel p = new JPanel(new GridBagLayout());
JosmTextArea txtData = new JosmTextArea();
txtData.setFont(new Font("Monospaced", txtData.getFont().getStyle(), txtData.getFont().getSize()));
txtData.setEditable(false);
txtData.setText(buildDataText());
txtData.setSelectionStart(0);
txtData.setSelectionEnd(0);
JScrollPane scroll = new JScrollPane(txtData);
p.add(scroll, GBC.std().fill());
return p;
}
protected String buildDataText() {
DataText dt = new DataText();
Collections.sort(primitives, new OsmPrimitiveComparator());
for (OsmPrimitive o : primitives) {
dt.addPrimitive(o);
}
return dt.toString();
}
class DataText {
static final String INDENT = " ";
static final String NL = "\n";
private StringBuilder s = new StringBuilder();
private DataText add(String title, String... values) {
s.append(INDENT).append(title);
for (String v : values) {
s.append(v);
}
s.append(NL);
return this;
}
private String getNameAndId(String name, long id) {
if (name != null) {
return name + tr(" ({0})", /* sic to avoid thousand seperators */ Long.toString(id));
} else {
return Long.toString(id);
}
}
public void addPrimitive(OsmPrimitive o) {
addHeadline(o);
if (!(o.getDataSet() != null && o.getDataSet().getPrimitiveById(o) != null)) {
s.append(NL).append(INDENT).append(tr("not in data set")).append(NL);
return;
}
if (o.isIncomplete()) {
s.append(NL).append(INDENT).append(tr("incomplete")).append(NL);
return;
}
s.append(NL);
addState(o);
addCommon(o);
addAttributes(o);
addSpecial(o);
addReferrers(s, o);
addConflicts(o);
s.append(NL);
}
void addHeadline(OsmPrimitive o) {
addType(o);
addNameAndId(o);
}
void addType(OsmPrimitive o) {
if (o instanceof Node) {
s.append(tr("Node: "));
} else if (o instanceof Way) {
s.append(tr("Way: "));
} else if (o instanceof Relation) {
s.append(tr("Relation: "));
}
}
void addNameAndId(OsmPrimitive o) {
String name = o.get("name");
if (name == null) {
s.append(o.getUniqueId());
} else {
s.append(getNameAndId(name, o.getUniqueId()));
}
}
void addState(OsmPrimitive o) {
StringBuilder sb = new StringBuilder(INDENT);
/* selected state is left out: not interesting as it is always selected */
if (o.isDeleted()) {
sb.append(tr("deleted")).append(INDENT);
}
if (!o.isVisible()) {
sb.append(tr("deleted-on-server")).append(INDENT);
}
if (o.isModified()) {
sb.append(tr("modified")).append(INDENT);
}
if (o.isDisabledAndHidden()) {
sb.append(tr("filtered/hidden")).append(INDENT);
}
if (o.isDisabled()) {
sb.append(tr("filtered/disabled")).append(INDENT);
}
if (o.hasDirectionKeys()) {
if (o.reversedDirection()) {
sb.append(tr("has direction keys (reversed)")).append(INDENT);
} else {
sb.append(tr("has direction keys")).append(INDENT);
}
}
String state = sb.toString().trim();
if (!state.isEmpty()) {
add(tr("State: "), sb.toString().trim());
}
}
void addCommon(OsmPrimitive o) {
add(tr("Data Set: "), Integer.toHexString(o.getDataSet().hashCode()));
add(tr("Edited at: "), o.isTimestampEmpty() ? tr("<new object>")
: DateUtils.fromDate(o.getTimestamp()));
add(tr("Edited by: "), o.getUser() == null ? tr("<new object>")
: getNameAndId(o.getUser().getName(), o.getUser().getId()));
add(tr("Version: "), Integer.toString(o.getVersion()));
add(tr("In changeset: "), Integer.toString(o.getChangesetId()));
}
void addAttributes(OsmPrimitive o) {
if (o.hasKeys()) {
add(tr("Tags: "));
for (String key : o.keySet()) {
s.append(INDENT).append(INDENT);
s.append(String.format("\"%s\"=\"%s\"%n", key, o.get(key)));
}
}
}
void addSpecial(OsmPrimitive o) {
if (o instanceof Node) {
addCoordinates((Node) o);
} else if (o instanceof Way) {
addBbox(o);
add(tr("Centroid: "), Main.getProjection().eastNorth2latlon(
Geometry.getCentroid(((Way) o).getNodes())).toStringCSV(", "));
addWayNodes((Way) o);
} else if (o instanceof Relation) {
addBbox(o);
addRelationMembers((Relation) o);
}
}
void addRelationMembers(Relation r) {
add(trn("{0} Member: ", "{0} Members: ", r.getMembersCount(), r.getMembersCount()));
for (RelationMember m : r.getMembers()) {
s.append(INDENT).append(INDENT);
addHeadline(m.getMember());
s.append(tr(" as \"{0}\"", m.getRole()));
s.append(NL);
}
}
void addWayNodes(Way w) {
add(tr("{0} Nodes: ", w.getNodesCount()));
for (Node n : w.getNodes()) {
s.append(INDENT).append(INDENT);
addNameAndId(n);
s.append(NL);
}
}
void addBbox(OsmPrimitive o) {
BBox bbox = o.getBBox();
if (bbox != null) {
add(tr("Bounding box: "), bbox.toStringCSV(", "));
EastNorth bottomRigth = Main.getProjection().latlon2eastNorth(bbox.getBottomRight());
EastNorth topLeft = Main.getProjection().latlon2eastNorth(bbox.getTopLeft());
add(tr("Bounding box (projected): "),
Double.toString(topLeft.east()), ", ",
Double.toString(bottomRigth.north()), ", ",
Double.toString(bottomRigth.east()), ", ",
Double.toString(topLeft.north()));
add(tr("Center of bounding box: "), bbox.getCenter().toStringCSV(", "));
}
}
void addCoordinates(Node n) {
if (n.getCoor() != null) {
add(tr("Coordinates: "),
Double.toString(n.getCoor().lat()), ", ",
Double.toString(n.getCoor().lon()));
add(tr("Coordinates (projected): "),
Double.toString(n.getEastNorth().east()), ", ",
Double.toString(n.getEastNorth().north()));
}
}
void addReferrers(StringBuilder s, OsmPrimitive o) {
List<OsmPrimitive> refs = o.getReferrers();
if (!refs.isEmpty()) {
add(tr("Part of: "));
for (OsmPrimitive p : refs) {
s.append(INDENT).append(INDENT);
addHeadline(p);
s.append(NL);
}
}
}
void addConflicts(OsmPrimitive o) {
Conflict<?> c = layer.getConflicts().getConflictForMy(o);
if (c != null) {
add(tr("In conflict with: "));
addNameAndId(c.getTheir());
}
}
@Override
public String toString() {
return s.toString();
}
}
protected void buildMapPaintPanel(JPanel p) {
p.setLayout(new GridBagLayout());
txtMappaint = new JosmTextArea();
txtMappaint.setFont(new Font("Monospaced", txtMappaint.getFont().getStyle(), txtMappaint.getFont().getSize()));
txtMappaint.setEditable(false);
p.add(new JScrollPane(txtMappaint), GBC.std().fill());
}
protected void createMapPaintText() {
final Collection<OsmPrimitive> sel = Main.main.getCurrentDataSet().getAllSelected();
ElemStyles elemstyles = MapPaintStyles.getStyles();
NavigatableComponent nc = Main.map.mapView;
double scale = nc.getDist100Pixel();
for (OsmPrimitive osm : sel) {
txtMappaint.append(tr("Styles Cache for \"{0}\":", osm.getDisplayName(DefaultNameFormatter.getInstance())));
MultiCascade mc = new MultiCascade();
for (StyleSource s : elemstyles.getStyleSources()) {
if (s.active) {
txtMappaint.append(tr("\n\n> applying {0} style \"{1}\"\n", getSort(s), s.getDisplayString()));
s.apply(mc, osm, scale, null, false);
txtMappaint.append(tr("\nRange:{0}", mc.range));
for (Entry<String, Cascade> e : mc.getLayers()) {
txtMappaint.append("\n " + e.getKey() + ": \n" + e.getValue());
}
} else {
txtMappaint.append(tr("\n\n> skipping \"{0}\" (not active)", s.getDisplayString()));
}
}
txtMappaint.append(tr("\n\nList of generated Styles:\n"));
StyleList sl = elemstyles.get(osm, scale, nc);
for (ElemStyle s : sl) {
txtMappaint.append(" * " + s + "\n");
}
txtMappaint.append("\n\n");
}
if (sel.size() == 2) {
List<OsmPrimitive> selList = new ArrayList<>(sel);
StyleCache sc1 = selList.get(0).mappaintStyle;
StyleCache sc2 = selList.get(1).mappaintStyle;
if (sc1 == sc2) {
txtMappaint.append(tr("The 2 selected objects have identical style caches."));
}
if (!sc1.equals(sc2)) {
txtMappaint.append(tr("The 2 selected objects have different style caches."));
}
if (sc1.equals(sc2) && sc1 != sc2) {
txtMappaint.append(tr("Warning: The 2 selected objects have equal, but not identical style caches."));
}
}
}
private String getSort(StyleSource s) {
if (s instanceof XmlStyleSource) {
return tr("xml");
} else if (s instanceof MapCSSStyleSource) {
return tr("mapcss");
} else {
return tr("unknown");
}
}
}