001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import java.io.StringWriter;
005import java.math.BigDecimal;
006import java.math.RoundingMode;
007import java.util.HashMap;
008import java.util.Iterator;
009import java.util.List;
010import java.util.Map;
011import java.util.Map.Entry;
012import java.util.stream.Stream;
013
014import javax.json.Json;
015import javax.json.JsonArrayBuilder;
016import javax.json.JsonObject;
017import javax.json.JsonObjectBuilder;
018import javax.json.JsonValue;
019import javax.json.JsonWriter;
020import javax.json.stream.JsonGenerator;
021
022import org.openstreetmap.josm.data.Bounds;
023import org.openstreetmap.josm.data.coor.EastNorth;
024import org.openstreetmap.josm.data.coor.LatLon;
025import org.openstreetmap.josm.data.osm.DataSet;
026import org.openstreetmap.josm.data.osm.MultipolygonBuilder;
027import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon;
028import org.openstreetmap.josm.data.osm.Node;
029import org.openstreetmap.josm.data.osm.OsmPrimitive;
030import org.openstreetmap.josm.data.osm.Relation;
031import org.openstreetmap.josm.data.osm.Way;
032import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
033import org.openstreetmap.josm.data.projection.Projection;
034import org.openstreetmap.josm.data.projection.Projections;
035import org.openstreetmap.josm.gui.mappaint.ElemStyles;
036import org.openstreetmap.josm.tools.Logging;
037import org.openstreetmap.josm.tools.Pair;
038
039/**
040 * Writes OSM data as a GeoJSON string, using JSR 353: Java API for JSON Processing (JSON-P).
041 * <p>
042 * See <a href="https://tools.ietf.org/html/rfc7946">RFC7946: The GeoJSON Format</a>
043 */
044public class GeoJSONWriter {
045
046    private final DataSet data;
047    private final Projection projection;
048    private static final boolean SKIP_EMPTY_NODES = true;
049
050    /**
051     * Constructs a new {@code GeoJSONWriter}.
052     * @param ds The OSM data set to save
053     * @since 12806
054     */
055    public GeoJSONWriter(DataSet ds) {
056        this.data = ds;
057        this.projection = Projections.getProjectionByCode("EPSG:4326"); // WGS 84
058    }
059
060    /**
061     * Writes OSM data as a GeoJSON string (prettified).
062     * @return The GeoJSON data
063     */
064    public String write() {
065        return write(true);
066    }
067
068    /**
069     * Writes OSM data as a GeoJSON string (prettified or not).
070     * @param pretty {@code true} to have pretty output, {@code false} otherwise
071     * @return The GeoJSON data
072     * @since 6756
073     */
074    public String write(boolean pretty) {
075        StringWriter stringWriter = new StringWriter();
076        Map<String, Object> config = new HashMap<>(1);
077        config.put(JsonGenerator.PRETTY_PRINTING, pretty);
078        try (JsonWriter writer = Json.createWriterFactory(config).createWriter(stringWriter)) {
079            JsonObjectBuilder object = Json.createObjectBuilder()
080                    .add("type", "FeatureCollection")
081                    .add("generator", "JOSM");
082            appendLayerBounds(data, object);
083            appendLayerFeatures(data, object);
084            writer.writeObject(object.build());
085            return stringWriter.toString();
086        }
087    }
088
089    private class GeometryPrimitiveVisitor implements OsmPrimitiveVisitor {
090
091        private final JsonObjectBuilder geomObj;
092
093        GeometryPrimitiveVisitor(JsonObjectBuilder geomObj) {
094            this.geomObj = geomObj;
095        }
096
097        @Override
098        public void visit(Node n) {
099            geomObj.add("type", "Point");
100            LatLon ll = n.getCoor();
101            if (ll != null) {
102                geomObj.add("coordinates", getCoorArray(null, n.getCoor()));
103            }
104        }
105
106        @Override
107        public void visit(Way w) {
108            if (w != null) {
109                final JsonArrayBuilder array = getCoorsArray(w.getNodes());
110                if (w.isClosed() && ElemStyles.hasAreaElemStyle(w, false)) {
111                    final JsonArrayBuilder container = Json.createArrayBuilder().add(array);
112                    geomObj.add("type", "Polygon");
113                    geomObj.add("coordinates", container);
114                } else {
115                    geomObj.add("type", "LineString");
116                    geomObj.add("coordinates", array);
117                }
118            }
119        }
120
121        @Override
122        public void visit(Relation r) {
123            if (r == null || !r.isMultipolygon() || r.hasIncompleteMembers()) {
124                return;
125            }
126            try {
127                final Pair<List<JoinedPolygon>, List<JoinedPolygon>> mp = MultipolygonBuilder.joinWays(r);
128                final JsonArrayBuilder polygon = Json.createArrayBuilder();
129                Stream.concat(mp.a.stream(), mp.b.stream())
130                        .map(p -> getCoorsArray(p.getNodes())
131                                // since first node is not duplicated as last node
132                                .add(getCoorArray(null, p.getNodes().get(0).getCoor())))
133                        .forEach(polygon::add);
134                geomObj.add("type", "MultiPolygon");
135                final JsonArrayBuilder multiPolygon = Json.createArrayBuilder().add(polygon);
136                geomObj.add("coordinates", multiPolygon);
137            } catch (MultipolygonBuilder.JoinedPolygonCreationException ex) {
138                Logging.warn("GeoJSON: Failed to export multipolygon {0}", r.getUniqueId());
139                Logging.warn(ex);
140            }
141        }
142    }
143
144    private JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, LatLon c) {
145        return getCoorArray(builder, projection.latlon2eastNorth(c));
146    }
147
148    private static JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, EastNorth c) {
149        return (builder != null ? builder : Json.createArrayBuilder())
150                .add(BigDecimal.valueOf(c.getX()).setScale(11, RoundingMode.HALF_UP))
151                .add(BigDecimal.valueOf(c.getY()).setScale(11, RoundingMode.HALF_UP));
152    }
153
154    private JsonArrayBuilder getCoorsArray(Iterable<Node> nodes) {
155        final JsonArrayBuilder builder = Json.createArrayBuilder();
156        for (Node n : nodes) {
157            LatLon ll = n.getCoor();
158            if (ll != null) {
159                builder.add(getCoorArray(null, ll));
160            }
161        }
162        return builder;
163    }
164
165    protected void appendPrimitive(OsmPrimitive p, JsonArrayBuilder array) {
166        if (p.isIncomplete()) {
167            return;
168        } else if (SKIP_EMPTY_NODES && p instanceof Node && p.getKeys().isEmpty()) {
169            return;
170        }
171
172        // Properties
173        final JsonObjectBuilder propObj = Json.createObjectBuilder();
174        for (Entry<String, String> t : p.getKeys().entrySet()) {
175            propObj.add(t.getKey(), t.getValue());
176        }
177        final JsonObject prop = propObj.build();
178
179        // Geometry
180        final JsonObjectBuilder geomObj = Json.createObjectBuilder();
181        p.accept(new GeometryPrimitiveVisitor(geomObj));
182        final JsonObject geom = geomObj.build();
183
184        // Build primitive JSON object
185        array.add(Json.createObjectBuilder()
186                .add("type", "Feature")
187                .add("properties", prop.isEmpty() ? JsonValue.NULL : prop)
188                .add("geometry", geom.isEmpty() ? JsonValue.NULL : geom));
189    }
190
191    protected void appendLayerBounds(DataSet ds, JsonObjectBuilder object) {
192        if (ds != null) {
193            Iterator<Bounds> it = ds.getDataSourceBounds().iterator();
194            if (it.hasNext()) {
195                Bounds b = new Bounds(it.next());
196                while (it.hasNext()) {
197                    b.extend(it.next());
198                }
199                appendBounds(b, object);
200            }
201        }
202    }
203
204    protected void appendBounds(Bounds b, JsonObjectBuilder object) {
205        if (b != null) {
206            JsonArrayBuilder builder = Json.createArrayBuilder();
207            getCoorArray(builder, b.getMin());
208            getCoorArray(builder, b.getMax());
209            object.add("bbox", builder);
210        }
211    }
212
213    protected void appendLayerFeatures(DataSet ds, JsonObjectBuilder object) {
214        JsonArrayBuilder array = Json.createArrayBuilder();
215        if (ds != null) {
216            ds.allNonDeletedPrimitives().forEach(p -> appendPrimitive(p, array));
217        }
218        object.add("features", array);
219    }
220}