Andreas Bruns

Softwareentwicklung für Oldenburg und Bremen

GeoJSON erzeugen mit Java

Bei der Webentwicklung hat sich für den Austausch von Daten zwischen Server und Client das JSON-Format bewährt. Es ist gegenüber XML kompakter, lässt sich trotzdem einfach lesen und wird von JavaScript direkt in verwendbare Objekte umgewandelt. Eine spezielle JSON-Ausprägung für geografische Daten bildet das GeoJSON-Format. Dieses Format wird auch von OpenLayers unterstützt, sodass man keinen umfangreichen JavaScript-Umwandlungscode schreiben muss. Der folgende GeoJSON-Code enthält die Daten mit Geo-Koordinaten für drei Plätze in meiner Heimat:

{ "type": "FeatureCollection", "features": [
{ "type": "Feature", "id": 1, "properties": { "name": "", "image": "playground.png"} , "geometry": { "type": "Point", "coordinates": [8.5864, 52.8988] } },
{ "type": "Feature", "id": 2, "properties": { "name": "Balkan-Restaurant", "image": "restaurant.png"} , "geometry": { "type": "Point", "coordinates": [8.5992, 52.9106] } },
{ "type": "Feature", "id": 3, "properties": { "name": "carpe diem", "image": "hairdresser.png"} , "geometry": { "type": "Point", "coordinates": [8.5873, 52.9079] } }
] }

Wie wird jetzt dieser GeoJSON-Code serverseitig generiert? Da GeoJSON auch normales JSON ist, können wir es genauso generieren, wie wir JSON generieren würden. Manche Sprachen unterstützen die Transformation zwischen Objekten und JSON direkt:

  • JavaScript: JSON.stringify($object) und JSON.parse($string)
  • PHP: json_encode($object) und json_decode($string)

In Java müssen wir selber Code dafür schreiben oder eine entsprechende Bibliothek nutzen. Zum Glück gibt es allerhand Java-Bibliotheken für JSON. Mit den Bibliotheken org.json, json-simple und Jackson habe ich ausprobiert, wie leicht das Erzeugen von JSON mit solchen Bibliotheken von der Hand geht. Das ist nur eine kleine Auswahl von den über 20 auf json.org aufgelisteten Bibliotheken.

Die Verwendung einer Bibliothek ist auf jeden Fall anzuraten, denn das Erzeugen von JSON per String-Konkatenation ist einfach unschön (egal wie sehr man den Java-Quellcode formatiert):

private static final String LINE_SEPERATOR = "n";

public String asGeoJson(List<PoiData> pois) throws IOException {
   StringBuilder geoJsonString = new StringBuilder();
   geoJsonString.append("{ "type": "FeatureCollection", "features": [" + LINE_SEPERATOR);
   for (int i = 0; i < pois.size(); i++) {
      PoiData poi = pois.get(i);
      if (i != 0) {
         geoJsonString.append("," + LINE_SEPERATOR);
      }
      String poiGeoJson =
         "{ "type": "Feature"," +
            " "id": " + poi.getId() + "," + //
            " "properties": {" + //
               " "name": "" + poi.getName() + ""," + //
               " "image": "" + poi.getType().getImageFile() + ""}," + //
            " "geometry": {" + //
               " "type": "Point"," + //
               " "coordinates": [" + poi.getLongitude() + ", " + poi.getLatitude() + //
         "] } }";
      geoJsonString.append(poiGeoJson);
   }
   geoJsonString.append(LINE_SEPERATOR + "] }");
   return geoJsonString.toString();
}

Mit den drei Bibliotheken habe ich jeweils einen eigenen JsonService implementiert, der das folgende Interface für JSON-Serialisierung und JSON-Deserialisierung implementieren musste:

public interface JsonService {
   public String asGeoJson(List<PoiData> pois) throws Exception;
   public List<PoiData> asPois(String geoJson) throws Exception;
}

Anschließend musste dann dieser Test auch entsprechend „grün“ werden:

public class OrgJsonServiceTest {

   private static final List<PoiData> POIS_LIST = Arrays.asList(new PoiData[] {
      new PoiData(1l, "", PoiTypeEnum.PLAYGROUND, 8.5864, 52.8988),
      new PoiData(2l, "Balkan-Restaurant", PoiTypeEnum.RESTAURANT, 8.5992, 52.9106),
      new PoiData(3l, "carpe diem", PoiTypeEnum.HAIRDRESSER, 8.5873, 52.9079)
   });

   private JsonService jsonService;

   @Before
   public void setup() {
      jsonService = new OrgJsonService();
   }

   @Test
   public void testSerializeDeserialize() throws Exception {
      String asGeoJson = jsonService.asGeoJson(POIS_LIST);
      List<PoiData> pois = jsonService.asPois(asGeoJson);
      assertEquals(3, pois.size());

      assertEquals(1l, pois.get(0).getId());
      assertEquals("", pois.get(0).getName());
      assertEquals(PoiTypeEnum.PLAYGROUND, pois.get(0).getType());
      assertEquals(8.5864, pois.get(0).getLongitude(), 0.0001);
      assertEquals(52.8988, pois.get(0).getLatitude(), 0.0001);

      assertEquals(2l, pois.get(1).getId());
      assertEquals("Balkan-Restaurant", pois.get(1).getName());
      assertEquals(PoiTypeEnum.RESTAURANT, pois.get(1).getType());
      assertEquals(8.5992, pois.get(1).getLongitude(), 0.0001);
      assertEquals(52.9106, pois.get(1).getLatitude(), 0.0001);

      assertEquals(3l, pois.get(2).getId());
      assertEquals("carpe diem", pois.get(2).getName());
      assertEquals(PoiTypeEnum.HAIRDRESSER, pois.get(2).getType());
      assertEquals(8.5873, pois.get(2).getLongitude(), 0.0001);
      assertEquals(52.9079, pois.get(2).getLatitude(), 0.0001);
   }
}

org.json – das Original

Als Erstes habe ich natürlich das Original org.json ausprobiert, das man direkt bei json.org herunterladen oder mit folgendem Maven-Eintrag einbinden kann:

<dependencies>
   <dependency>
      <groupId>org.json</groupId>
      <artifactId>json</artifactId>
      <version>20090211</version>
   </dependency>
</dependencies>

Der entsprechende Service kann dann so implementiert werden:

public class OrgJsonService implements JsonService {

   @Override
   public String asGeoJson(List<PoiData> pois) throws Exception {
      JSONArray featuresJson = new JSONArray();
      for (PoiData poi : pois) {
         JSONArray coordinatesJson = new JSONArray();
         coordinatesJson.put(poi.getLongitude());
         coordinatesJson.put(poi.getLatitude());

         JSONObject geometryJson = new JSONObject();
         geometryJson.put("type", "Point");
         geometryJson.put("coordinates", coordinatesJson);

         JSONObject propertiesJson = new JSONObject();
         propertiesJson.put("name", poi.getName());
         propertiesJson.put("image", poi.getType().getImageFile());

         JSONObject featureJson = new JSONObject();
         featureJson.put("type", "Feature");
         featureJson.put("id", poi.getId());
         featureJson.put("properties", propertiesJson);
         featureJson.put("geometry", geometryJson);
         featuresJson.put(featureJson);
      }
      JSONObject featureCollectionJson = new JSONObject();
      featureCollectionJson.put("type", "FeatureCollection");
      featureCollectionJson.put("features", featuresJson);
      return featureCollectionJson.toString();
   }

   @Override
   public List<PoiData> asPois(String geoJson) throws Exception {
      List<PoiData> pois = new ArrayList<PoiData>();
      JSONObject jsonRoot = new JSONObject(geoJson);
      JSONArray features = jsonRoot.getJSONArray("features");
      for (int i = 0; i < features.length(); i++) {
         JSONObject feature = (JSONObject) features.get(i);
         long id = feature.getLong("id");

         JSONObject properties = (JSONObject) feature.get("properties");
         String name = properties.getString("name");
         PoiTypeEnum type = PoiTypeEnum.ofImage(properties.getString("image"));

         JSONObject geometry = (JSONObject) feature.get("geometry");
         JSONArray coordinates = (JSONArray) geometry.get("coordinates");
         double longitude = coordinates.getDouble(0);
         double latitude = coordinates.getDouble(1);

         PoiData poiData = new PoiData(id, name, type, longitude, latitude);
         pois.add(poiData);
      }
      return pois;
   }
}

Das sieht doch sehr einfach und intuitiv aus. Es werden sowohl beim Serialisieren als auch beim Deserialisieren die Klassen JSONObject und JSONArray genutzt, die das Verhalten von Maps beziehungsweise Listen nachbilden.

Die 46 KB kleine Bibliothek json-20090211.jar steht auch unter einer schönen Lizenz:

The license includes this restriction: „The software shall be used for good, not evil.“ If your conscience cannot live with that, then choose a different package.

Nach dem JSON-Standard ist die Reihenfolge der Attribute eines Elements nicht relevant. Konsequenterweise kann man mit der Bibliothek org.json auch nicht die Reihenfolge der Attribute festlegen. Dafür werden noch Funktionen für die Verarbeitung von CSV, XML, HTTP-Header und HTTP-Cookies angeboten. Diese Bibliothek steht übrigens auch bei der Entwicklung für Android direkt zur Verfügung.

JSON-simple – klein aber fein

Als Nächstes habe ich dann JSON-simple ausprobiert, das ebenfalls im Standard-Repository von Maven verfügbar ist:

<dependencies>
   <dependency>
      <groupId>com.googlecode.json-simple</groupId>
      <artifactId>json-simple</artifactId>
      <version>1.1.1</version>
   </dependency>
</dependencies>
public class JsonSimpleService implements JsonService {

   @Override
   public String asGeoJson(List<PoiData> pois) throws IOException {
      List<Map<String, Object>> featuresList = new ArrayList<Map<String, Object>>();
      for (PoiData poi : pois) {
         List<Double> coordinatesList = new ArrayList<Double>();
         coordinatesList.add(poi.getLongitude());
         coordinatesList.add(poi.getLatitude());

         Map<String, Object> geometryMap = new LinkedHashMap<String, Object>();
         geometryMap.put("type", "Point");
         geometryMap.put("coordinates", coordinatesList);

         Map<String, Object> propertiesMap = new LinkedHashMap<String, Object>();
         propertiesMap.put("name", poi.getName());
         propertiesMap.put("image", poi.getType().getImageFile());

         Map<String, Object> featureMap = new LinkedHashMap<String, Object>();
         featureMap.put("type", "Feature");
         featureMap.put("id", poi.getId());
         featureMap.put("properties", propertiesMap);
         featureMap.put("geometry", geometryMap);
         featuresList.add(featureMap);
      }
      Map<String, Object> featureCollectionMap = new LinkedHashMap<String, Object>();
      featureCollectionMap.put("type", "FeatureCollection");
      featureCollectionMap.put("features", featuresList);
      return JSONValue.toJSONString(featureCollectionMap);
   }

   @Override
   public List<PoiData> asPois(String geoJson) throws IOException {
      List<PoiData> pois = new ArrayList<PoiData>();

      JSONObject jsonRoot=(JSONObject)JSONValue.parse(geoJson);
      JSONArray features = (JSONArray) jsonRoot.get("features");
      for (int i = 0; i < features.size(); i++) {
         JSONObject feature = (JSONObject) features.get(i);
         long id = (Long) feature.get("id");

         JSONObject properties = (JSONObject) feature.get("properties");
         String name = (String) properties.get("name");
         PoiTypeEnum type = PoiTypeEnum.ofImage((String) properties.get("image"));

         JSONObject geometry = (JSONObject) feature.get("geometry");
         JSONArray coordinates = (JSONArray) geometry.get("coordinates");
         double longitude = (Double) coordinates.get(0);
         double latitude = (Double) coordinates.get(1);

         PoiData poiData = new PoiData(id, name, type, longitude, latitude);
         pois.add(poiData);
      }
      return pois;
   }
}

Die unter der Apache License 2.0 stehende Bibliothek json-simple-1.1.1.jar ist mit 24KB nur halb so klein wie das Original. Der Code ist genauso einfach wie der vorherige Code. Die Bibliothek kennt ebenfalls die Klassen JSONObject und JSONArray, nutzt sie jedoch nur für die Deserialisierung. Bei der Serialisierung werden normale Maps und Listen verwendet. Durch die Verwendung einer LinkedHashMap (statt HashMap) können wir bei der Serialisierung die Reihenfolge der Attribute sogar selber bestimmen. JSON-simple bietet neben dieser einfachen Art der Umwandlung außerdem eine Unterstützung für eine SAX-artige Stream-Verarbeitung.

Jackson – ein Allrounder

Von Jackson wurde gerade ganz frisch (Ende März 2012) die Verion 2.0 veröffentlicht, was dann auch gleich mit einem Umzug von jackson.codehaus.org nach Github verbunden worden ist. Es steht unter der Apache License 2.0 und der GNU Lesser GPL zur Verfügung. Aus dem Standard-Repository von Maven können wir es uns auch schon herunterladen lassen:

<dependencies>
   <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.0.0</version>
   </dependency>
</dependencies>
public class JacksonJsonService implements JsonService {

   @Override
   public String asGeoJson(List<PoiData> pois) throws IOException {
      List<Map<String, Object>> featuresList = new ArrayList<Map<String, Object>>();
      for (PoiData poi : pois) {
         List<Double> coordinatesList = new ArrayList<Double>();
         coordinatesList.add(poi.getLongitude());
         coordinatesList.add(poi.getLatitude());

         Map<String, Object> geometryMap = new LinkedHashMap<String, Object>();
         geometryMap.put("type", "Point");
         geometryMap.put("coordinates", coordinatesList);

         Map<String, Object> propertiesMap = new LinkedHashMap<String, Object>();
         propertiesMap.put("name", poi.getName());
         propertiesMap.put("image", poi.getType().getImageFile());

         Map<String, Object> featureMap = new LinkedHashMap<String, Object>();
         featureMap.put("type", "Feature");
         featureMap.put("id", poi.getId());
         featureMap.put("properties", propertiesMap);
         featureMap.put("geometry", geometryMap);
         featuresList.add(featureMap);
      }

      Map<String, Object> featureCollectionMap = new LinkedHashMap<String, Object>();
      featureCollectionMap.put("type", "FeatureCollection");
      featureCollectionMap.put("features", featuresList);
      return new ObjectMapper().writeValueAsString(featureCollectionMap);
   }

   @Override
   @SuppressWarnings("unchecked")
   public List<PoiData> asPois(String geoJson) throws IOException {
      List<PoiData> pois = new ArrayList<PoiData>();

      Map<String,Object> dataRoot = new ObjectMapper().readValue(geoJson, new TypeReference<Map<String,Object>>() {});
      List<Object> features = (List<Object>) dataRoot.get("features");
      for (int i = 0; i < features.size(); i++) {
         Map<String,Object> feature =  (Map<String, Object>) features.get(i);
         long id = ((Integer) feature.get("id")).longValue();

         Map<String,Object> properties = (Map<String, Object>) feature.get("properties");
         String name = (String) properties.get("name");
         PoiTypeEnum type = PoiTypeEnum.ofImage((String) properties.get("image"));

         Map<String,Object> geometry = (Map<String, Object>) feature.get("geometry");
         List<Object> coordinates = (List<Object>) geometry.get("coordinates");
         double longitude = (Double) coordinates.get(0);
         double latitude = (Double) coordinates.get(1);

         PoiData poiData = new PoiData(id, name, type, longitude, latitude);
         pois.add(poiData);
      }
      return pois;
   }
}

Jackson ist sehr umfangreich und mit unserem einfachen Beispiel unterfordern wir es schon eher. Drei Möglichkeiten werden uns angeboten:

  • per Streaming-API mit Events-Verarbeitung, ähnlich der StAX-API
  • DOM-artiger Aufbau eines Baumes im Speicher
  • Databinding nach zwei Arten:
    – simple: alles händisch – wie wir es hier machen
    – full: mit Annotationen am Datenmodell – wie bei JAXB

Dafür schlägt Jackson auch mit insgesamt 1MB für jackson-databind (800KB), dessen Abhängigkeit jackson-core (200KB) und der hier nicht benötigten Abhängigkeit jackson-annotation (35KB) gut beim Speicherbedarf zu. Bei der Verwendung von Jackson nach dem einfachen Databinding kommen für die Serialisierung und Deserialisierung nur normale Maps und Listen zum Einsatz, sodass der Code ähnlich einfach wie bei den anderen Beispielen aussieht. Mit den Standard-Compilereinstellungen erhalten wir bei der Methode zum Deserialisieren dann auch Warnungen ‚Type safety: Unchecked cast‚, die wir mit @SuppressWarnings("unchecked") unterdrücken können.

Fazit

Der Einsatz einer Bibliothek zur Erzeugung von GeoJSON lohnt sich auf jeden Fall. Der Code-Unterschied zwischen den drei Beispielen ist eher gering. Falls man nicht sowieso schon org.json zur Verfügung hat, wie bei der Android-Entwicklung, würde ich mit JSON-simple anfangen. Bei der Entwicklung hilft es für die Übersichtlichkeit auch, wenn die Attribute in einer festgelegten Reihenfolge generiert werden können.

Wenn man Erfahrungen mit JSON-simple gesammelt hat und/oder die Schwerpunkte für die Auswahl einer Bibliothek klar sind, lohnt es sich, die zahlreichen anderen JSON-Bibliotheken zu prüfen. Oder man verlässt sich einfach auf die Erfahrungen anderer Entwickler, wobei man die Aktualität der Artikel beachten sollte:

  • Stackoverflow – eine bessere JSON-Bibliothek: hier
  • Blog – Vergleich von fünf JSON-Bibliotheken: hier

Und natürlich gibt es auch Performancevergleiche, wie hier und hier. Bei dem zweiten Vergleich schneidet JSON-simple jedoch nicht so gut ab.

Anhang: Beispielanwendung mit OpenLayers und GeoJSON

Wenn wir jetzt unser GeoJSON haben, können wir es beispielsweise für eine Karte mit OpenLayers direkt benutzen. Bei der folgenden HTML-Seite mit JavaScript-Code muss man unter der angegebenen URL des zweiten Layers (‚POIs‘) beispielsweise die oben angezeigten GeoJSON-Daten liefern. Die passenden Bilder müssen natürlich ebenfalls ausgeliefert werden.

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Meine Orte</title>

<script type="text/javascript" src="http://dev.openlayers.org/releases/OpenLayers-2.11/OpenLayers.js"></script>

<script type="text/javascript">
  PROJECTION_4326 = new OpenLayers.Projection("EPSG:4326");
  PROJECTION_MERC = new OpenLayers.Projection("EPSG:900913");

  function init() {
      map = new OpenLayers.Map("map", {
      controls : [ new OpenLayers.Control.PanZoomBar(),
        new OpenLayers.Control.Navigation(),
        new OpenLayers.Control.LayerSwitcher(),
        new OpenLayers.Control.MousePosition(),
        new OpenLayers.Control.Attribution(),
        new OpenLayers.Control.OverviewMap() ],
      maxExtent : new OpenLayers.Bounds(-20037508.34, -20037508.34, 20037508.34, 20037508.34),
      numZoomLevels : 18,
      maxResolution : 156543,
      units : 'm',
      projection : PROJECTION_MERC,
      displayProjection : PROJECTION_4326
    });

    // Layer: OSM
    var osmLayer = new OpenLayers.Layer.OSM("OpenStreetMap");
    map.addLayer(osmLayer);

    var center = new OpenLayers.LonLat(8.59, 52.9);
    var centerAsMerc = center.transform(PROJECTION_4326, PROJECTION_MERC);
    map.setCenter(centerAsMerc, 14);

    // Layer: POIs
    var poiStyle = {externalGraphic: '${image}', graphicHeight: 37, graphicWidth: 32, graphicYOffset: -34};
    var poisLayer = new OpenLayers.Layer.Vector("POIs", {
        projection: PROJECTION_4326,
        protocol: new OpenLayers.Protocol.HTTP({
                url: "pois",
                format: new OpenLayers.Format.GeoJSON()
        }),
        strategies: [new OpenLayers.Strategy.BBOX()],
		styleMap: new OpenLayers.StyleMap(poiStyle)
    })
    map.addLayer(poisLayer);
  }
</script>

<style type="text/css">
html,body,#map {
  width: 100%;
  height: 100%;
  margin: 0;
}
</style>

</head>
<body onload="init()">
  <div id="map"></div>
</body>
</html>

Das komplette Maven-basierte Beispiel mit Servlet, Jetty, GeoJSON-Generierung und OpenLayers-Webanwendung ist unter Github verfügbar. Bei installiertem Maven und laufendem Jetty-Server erhält man mit http://localhost:8080/ einige meiner Lieblingsorte. Der Jetty-Server wird dank des Jetty-Maven-Plugins so gestartet:

mvn clean install jetty:run

 

Meine Lieblingsorte aus den GeoJSON-Daten

Ein Live-Beispiel gibt es hier zu sehen und unter der URL http://cartopol.com/example/json-converting/pois erhalten wir unser generiertes GeoJSON.

Kommentare sind geschlossen.