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