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.preferences.BooleanProperty;
034import org.openstreetmap.josm.data.projection.Projection;
035import org.openstreetmap.josm.data.projection.Projections;
036import org.openstreetmap.josm.gui.mappaint.ElemStyles;
037import org.openstreetmap.josm.tools.Logging;
038import org.openstreetmap.josm.tools.Pair;
039
040/**
041 * Writes OSM data as a GeoJSON string, using JSR 353: Java API for JSON Processing (JSON-P).
042 * <p>
043 * See <a href="https://tools.ietf.org/html/rfc7946">RFC7946: The GeoJSON Format</a>
044 */
045public class GeoJSONWriter {
046
047    private final DataSet data;
048    private final Projection projection;
049    private static final BooleanProperty SKIP_EMPTY_NODES = new BooleanProperty("geojson.export.skip-empty-nodes", true);
050
051    /**
052     * Constructs a new {@code GeoJSONWriter}.
053     * @param ds The OSM data set to save
054     * @since 12806
055     */
056    public GeoJSONWriter(DataSet ds) {
057        this.data = ds;
058        this.projection = Projections.getProjectionByCode("EPSG:4326"); // WGS 84
059    }
060
061    /**
062     * Writes OSM data as a GeoJSON string (prettified).
063     * @return The GeoJSON data
064     */
065    public String write() {
066        return write(true);
067    }
068
069    /**
070     * Writes OSM data as a GeoJSON string (prettified or not).
071     * @param pretty {@code true} to have pretty output, {@code false} otherwise
072     * @return The GeoJSON data
073     * @since 6756
074     */
075    public String write(boolean pretty) {
076        StringWriter stringWriter = new StringWriter();
077        Map<String, Object> config = new HashMap<>(1);
078        config.put(JsonGenerator.PRETTY_PRINTING, pretty);
079        try (JsonWriter writer = Json.createWriterFactory(config).createWriter(stringWriter)) {
080            JsonObjectBuilder object = Json.createObjectBuilder()
081                    .add("type", "FeatureCollection")
082                    .add("generator", "JOSM");
083            appendLayerBounds(data, object);
084            appendLayerFeatures(data, object);
085            writer.writeObject(object.build());
086            return stringWriter.toString();
087        }
088    }
089
090    private class GeometryPrimitiveVisitor implements OsmPrimitiveVisitor {
091
092        private final JsonObjectBuilder geomObj;
093
094        GeometryPrimitiveVisitor(JsonObjectBuilder geomObj) {
095            this.geomObj = geomObj;
096        }
097
098        @Override
099        public void visit(Node n) {
100            geomObj.add("type", "Point");
101            LatLon ll = n.getCoor();
102            if (ll != null) {
103                geomObj.add("coordinates", getCoorArray(null, n.getCoor()));
104            }
105        }
106
107        @Override
108        public void visit(Way w) {
109            if (w != null) {
110                final JsonArrayBuilder array = getCoorsArray(w.getNodes());
111                if (w.isClosed() && ElemStyles.hasAreaElemStyle(w, false)) {
112                    final JsonArrayBuilder container = Json.createArrayBuilder().add(array);
113                    geomObj.add("type", "Polygon");
114                    geomObj.add("coordinates", container);
115                } else {
116                    geomObj.add("type", "LineString");
117                    geomObj.add("coordinates", array);
118                }
119            }
120        }
121
122        @Override
123        public void visit(Relation r) {
124            if (r == null || !r.isMultipolygon() || r.hasIncompleteMembers()) {
125                return;
126            }
127            try {
128                final Pair<List<JoinedPolygon>, List<JoinedPolygon>> mp = MultipolygonBuilder.joinWays(r);
129                final JsonArrayBuilder polygon = Json.createArrayBuilder();
130                Stream.concat(mp.a.stream(), mp.b.stream())
131                        .map(p -> getCoorsArray(p.getNodes())
132                                // since first node is not duplicated as last node
133                                .add(getCoorArray(null, p.getNodes().get(0).getCoor())))
134                        .forEach(polygon::add);
135                geomObj.add("type", "MultiPolygon");
136                final JsonArrayBuilder multiPolygon = Json.createArrayBuilder().add(polygon);
137                geomObj.add("coordinates", multiPolygon);
138            } catch (MultipolygonBuilder.JoinedPolygonCreationException ex) {
139                Logging.warn("GeoJSON: Failed to export multipolygon {0}", r.getUniqueId());
140                Logging.warn(ex);
141            }
142        }
143    }
144
145    private JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, LatLon c) {
146        return getCoorArray(builder, projection.latlon2eastNorth(c));
147    }
148
149    private static JsonArrayBuilder getCoorArray(JsonArrayBuilder builder, EastNorth c) {
150        return (builder != null ? builder : Json.createArrayBuilder())
151                .add(BigDecimal.valueOf(c.getX()).setScale(11, RoundingMode.HALF_UP))
152                .add(BigDecimal.valueOf(c.getY()).setScale(11, RoundingMode.HALF_UP));
153    }
154
155    private JsonArrayBuilder getCoorsArray(Iterable<Node> nodes) {
156        final JsonArrayBuilder builder = Json.createArrayBuilder();
157        for (Node n : nodes) {
158            LatLon ll = n.getCoor();
159            if (ll != null) {
160                builder.add(getCoorArray(null, ll));
161            }
162        }
163        return builder;
164    }
165
166    protected void appendPrimitive(OsmPrimitive p, JsonArrayBuilder array) {
167        if (p.isIncomplete() ||
168            (SKIP_EMPTY_NODES.get() && 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}