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