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.util.Arrays;
008import java.util.List;
009import java.util.stream.Collectors;
010import java.util.stream.Stream;
011
012import org.openstreetmap.josm.data.conflict.Conflict;
013import org.openstreetmap.josm.data.coor.ILatLon;
014import org.openstreetmap.josm.data.coor.LatLon;
015import org.openstreetmap.josm.data.coor.conversion.AbstractCoordinateFormat;
016import org.openstreetmap.josm.data.coor.conversion.DecimalDegreesCoordinateFormat;
017import org.openstreetmap.josm.data.coor.conversion.ProjectedCoordinateFormat;
018import org.openstreetmap.josm.data.osm.BBox;
019import org.openstreetmap.josm.data.osm.DataSet;
020import org.openstreetmap.josm.data.osm.INode;
021import org.openstreetmap.josm.data.osm.IPrimitive;
022import org.openstreetmap.josm.data.osm.IRelation;
023import org.openstreetmap.josm.data.osm.IRelationMember;
024import org.openstreetmap.josm.data.osm.IWay;
025import org.openstreetmap.josm.data.osm.OsmData;
026import org.openstreetmap.josm.data.osm.OsmPrimitive;
027import org.openstreetmap.josm.data.projection.ProjectionRegistry;
028import org.openstreetmap.josm.data.projection.proj.TransverseMercator;
029import org.openstreetmap.josm.data.projection.proj.TransverseMercator.Hemisphere;
030import org.openstreetmap.josm.tools.Geometry;
031import org.openstreetmap.josm.tools.Pair;
032import org.openstreetmap.josm.tools.date.DateUtils;
033
034/**
035 * Textual representation of primitive contents, used in {@code InspectPrimitiveDialog}.
036 * @since 10198
037 */
038public class InspectPrimitiveDataText {
039    private static final String INDENT = "  ";
040    private static final char NL = '\n';
041
042    private final StringBuilder s = new StringBuilder();
043    private final OsmData<?, ?, ?, ?> ds;
044
045    InspectPrimitiveDataText(OsmData<?, ?, ?, ?> ds) {
046        this.ds = ds;
047    }
048
049    private InspectPrimitiveDataText add(String title, String... values) {
050        s.append(INDENT).append(title);
051        for (String v : values) {
052            s.append(v);
053        }
054        s.append(NL);
055        return this;
056    }
057
058    private static String getNameAndId(String name, long id) {
059        if (name != null) {
060            return name + tr(" ({0})", /* sic to avoid thousand separators */ Long.toString(id));
061        } else {
062            return Long.toString(id);
063        }
064    }
065
066    /**
067     * Adds a new OSM primitive.
068     * @param o primitive to add
069     */
070    public void addPrimitive(IPrimitive o) {
071
072        addHeadline(o);
073
074        if (!(o.getDataSet() != null && o.getDataSet().getPrimitiveById(o) != null)) {
075            s.append(NL).append(INDENT).append(tr("not in data set")).append(NL);
076            return;
077        }
078        if (o.isIncomplete()) {
079            s.append(NL).append(INDENT).append(tr("incomplete")).append(NL);
080            return;
081        }
082        s.append(NL);
083
084        addState(o);
085        addCommon(o);
086        addAttributes(o);
087        addSpecial(o);
088        addReferrers(s, o);
089        if (o instanceof OsmPrimitive) {
090            addConflicts((OsmPrimitive) o);
091        }
092        s.append(NL);
093    }
094
095    void addHeadline(IPrimitive o) {
096        addType(o);
097        addNameAndId(o);
098    }
099
100    void addType(IPrimitive o) {
101        if (o instanceof INode) {
102            s.append(tr("Node: "));
103        } else if (o instanceof IWay) {
104            s.append(tr("Way: "));
105        } else if (o instanceof IRelation) {
106            s.append(tr("Relation: "));
107        }
108    }
109
110    void addNameAndId(IPrimitive o) {
111        String name = o.get("name");
112        if (name == null) {
113            s.append(o.getUniqueId());
114        } else {
115            s.append(getNameAndId(name, o.getUniqueId()));
116        }
117    }
118
119    void addState(IPrimitive o) {
120        StringBuilder sb = new StringBuilder(INDENT);
121        /* selected state is left out: not interesting as it is always selected */
122        if (o.isDeleted()) {
123            sb.append(tr("deleted")).append(INDENT);
124        }
125        if (!o.isVisible()) {
126            sb.append(tr("deleted-on-server")).append(INDENT);
127        }
128        if (o.isModified()) {
129            sb.append(tr("modified")).append(INDENT);
130        }
131        if (o.isDisabledAndHidden()) {
132            sb.append(tr("filtered/hidden")).append(INDENT);
133        }
134        if (o.isDisabled()) {
135            sb.append(tr("filtered/disabled")).append(INDENT);
136        }
137        if (o.hasDirectionKeys()) {
138            if (o.reversedDirection()) {
139                sb.append(tr("has direction keys (reversed)")).append(INDENT);
140            } else {
141                sb.append(tr("has direction keys")).append(INDENT);
142            }
143        }
144        String state = sb.toString().trim();
145        if (!state.isEmpty()) {
146            add(tr("State: "), sb.toString().trim());
147        }
148    }
149
150    void addCommon(IPrimitive o) {
151        add(tr("Data Set: "), Integer.toHexString(o.getDataSet().hashCode()));
152        add(tr("Edited at: "), o.isTimestampEmpty() ? tr("<new object>")
153                : DateUtils.fromTimestamp(o.getRawTimestamp()));
154        add(tr("Edited by: "), o.getUser() == null ? tr("<new object>")
155                : getNameAndId(o.getUser().getName(), o.getUser().getId()));
156        add(tr("Version:"), " ", Integer.toString(o.getVersion()));
157        add(tr("In changeset: "), Integer.toString(o.getChangesetId()));
158    }
159
160    void addAttributes(IPrimitive o) {
161        if (o.hasKeys()) {
162            add(tr("Tags: "));
163            for (String key : o.keySet()) {
164                s.append(INDENT).append(INDENT);
165                s.append(String.format("\"%s\"=\"%s\"%n", key, o.get(key)));
166            }
167        }
168    }
169
170    void addSpecial(IPrimitive o) {
171        if (o instanceof INode) {
172            addCoordinates((INode) o);
173        } else if (o instanceof IWay) {
174            addBbox(o);
175            add(tr("Centroid: "), toStringCSV(false,
176                    ProjectionRegistry.getProjection().eastNorth2latlon(Geometry.getCentroid(((IWay<?>) o).getNodes()))));
177            addWayNodes((IWay<?>) o);
178        } else if (o instanceof IRelation) {
179            addBbox(o);
180            addRelationMembers((IRelation<?>) o);
181        }
182    }
183
184    void addRelationMembers(IRelation<?> r) {
185        add(trn("{0} Member: ", "{0} Members: ", r.getMembersCount(), r.getMembersCount()));
186        for (IRelationMember<?> m : r.getMembers()) {
187            s.append(INDENT).append(INDENT);
188            addHeadline(m.getMember());
189            s.append(tr(" as \"{0}\"", m.getRole()));
190            s.append(NL);
191        }
192    }
193
194    void addWayNodes(IWay<?> w) {
195        add(tr("{0} Nodes: ", w.getNodesCount()));
196        for (INode n : w.getNodes()) {
197            s.append(INDENT).append(INDENT);
198            addNameAndId(n);
199            s.append(NL);
200        }
201    }
202
203    void addBbox(IPrimitive o) {
204        BBox bbox = o.getBBox();
205        if (bbox != null) {
206            final LatLon bottomRight = bbox.getBottomRight();
207            final LatLon topLeft = bbox.getTopLeft();
208            add(tr("Bounding box: "), toStringCSV(false, bottomRight, topLeft));
209            add(tr("Bounding box (projected): "), toStringCSV(true, bottomRight, topLeft));
210            add(tr("Center of bounding box: "), toStringCSV(false, bbox.getCenter()));
211        }
212    }
213
214    void addCoordinates(INode n) {
215        if (n.isLatLonKnown()) {
216            add(tr("Coordinates:"), " ", toStringCSV(false, n));
217            add(tr("Coordinates (projected): "), toStringCSV(true, n));
218            Pair<Integer, Hemisphere> utmZone = TransverseMercator.locateUtmZone(n.getCoor());
219            String utmLabel = tr("UTM Zone");
220            add(utmLabel, utmLabel.endsWith(":") ? " " : ": ", Integer.toString(utmZone.a), utmZone.b.name().substring(0, 1));
221        }
222    }
223
224    void addReferrers(StringBuilder s, IPrimitive o) {
225        List<? extends IPrimitive> refs = o.getReferrers();
226        if (!refs.isEmpty()) {
227            add(tr("Part of: "));
228            for (IPrimitive p : refs) {
229                s.append(INDENT).append(INDENT);
230                addHeadline(p);
231                s.append(NL);
232            }
233        }
234    }
235
236    void addConflicts(OsmPrimitive o) {
237        Conflict<?> c = ((DataSet) ds).getConflicts().getConflictForMy(o);
238        if (c != null) {
239            add(tr("In conflict with: "));
240            addNameAndId(c.getTheir());
241        }
242    }
243
244    /**
245     * Returns the coordinates in human-readable format.
246     * @param projected whether to use projected coordinates
247     * @param coordinates the coordinates to format
248     * @return String in the format {@code "1.23456, 2.34567"}
249     */
250    private static String toStringCSV(boolean projected, ILatLon... coordinates) {
251        final AbstractCoordinateFormat format = projected
252                ? ProjectedCoordinateFormat.INSTANCE
253                : DecimalDegreesCoordinateFormat.INSTANCE;
254        return Arrays.stream(coordinates)
255                .flatMap(ll -> Stream.of(format.latToString(ll), format.lonToString(ll)))
256                .collect(Collectors.joining(", "));
257    }
258
259    @Override
260    public String toString() {
261        return s.toString();
262    }
263}