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