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