Skip to content

Commit

Permalink
Add geojson parsing support
Browse files Browse the repository at this point in the history
  • Loading branch information
lognaturel committed Jan 31, 2022
1 parent 2b4dd89 commit 6d31598
Show file tree
Hide file tree
Showing 10 changed files with 284 additions and 17 deletions.
10 changes: 7 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ sourceCompatibility = '1.8'

dependencies {
// Be sure to update dependencies in pom.xml to match
implementation 'joda-time:joda-time:2.10.10'
implementation 'org.slf4j:slf4j-api:1.7.31'
implementation 'joda-time:joda-time:2.10.13'
implementation 'org.slf4j:slf4j-api:1.7.33'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.1'

// Upgrade to version higher than 1.4 when Collect minSDK >= 26
implementation 'org.apache.commons:commons-csv:1.4'

compileOnly 'net.sf.kxml:kxml2:2.3.0'
testImplementation 'ch.qos.logback:logback-classic:1.2.3'

testImplementation 'ch.qos.logback:logback-classic:1.2.10'
testImplementation 'junit:junit:4.13.2'
testImplementation 'net.sf.kxml:kxml2:2.3.0'
testImplementation 'org.hamcrest:hamcrest-all:1.3'
Expand Down
Binary file modified javarosa-android-api-level-checker.zip
Binary file not shown.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
<version>2.10.10</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import org.javarosa.core.model.instance.geojson.GeoJsonExternalInstance;
import org.javarosa.core.reference.InvalidReferenceException;
import org.javarosa.core.reference.ReferenceManager;
import org.javarosa.core.util.externalizable.DeserializationException;
Expand Down Expand Up @@ -68,10 +69,12 @@ public static ExternalDataInstance build(String instanceSrc, String instanceId)
return new ExternalDataInstance(root, instanceId, instanceSrc);
}

private static TreeElement parseExternalInstance(String instanceSrc, String instanceId) throws IOException, InvalidReferenceException, InvalidStructureException, XmlPullParserException, UnfullfilledRequirementsException {
private static TreeElement parseExternalInstance(String instanceSrc, String instanceId)
throws IOException, InvalidReferenceException, InvalidStructureException, XmlPullParserException, UnfullfilledRequirementsException {
String path = getPath(instanceSrc);
return instanceSrc.contains("file-csv") ?
CsvExternalInstance.parse(instanceId, path) : XmlExternalInstance.parse(instanceId, path);
return instanceSrc.contains("file-csv") ? CsvExternalInstance.parse(instanceId, path)
: instanceSrc.endsWith("geojson") ? GeoJsonExternalInstance.parse(instanceId, path)
: XmlExternalInstance.parse(instanceId, path);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.javarosa.core.model.instance.geojson;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.javarosa.core.model.instance.TreeElement;

public class GeoJsonExternalInstance {
public static TreeElement parse(String instanceId, String path) throws IOException {
return parse(instanceId, new FileInputStream(path));
}

protected static TreeElement parse(String instanceId, InputStream geojsonStream) throws IOException {
final TreeElement root = new TreeElement("root", 0);
root.setInstanceName(instanceId);

ObjectMapper objectMapper = new ObjectMapper();
try (JsonParser jsonParser = objectMapper.getFactory().createParser(geojsonStream)) {
validatePreamble(jsonParser);

int multiplicity = 0;
while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
GeojsonFeature feature = objectMapper.readValue(jsonParser, GeojsonFeature.class);

root.addChild(feature.toTreeElement(multiplicity));
multiplicity++;
}
}

return root;
}

private static void validatePreamble(JsonParser jsonParser) throws IOException {
if (jsonParser.nextToken() != JsonToken.START_OBJECT) {
throw new IOException("GeoJSON file must contain a top-level Object");
}

jsonParser.nextToken();
String propertyName = jsonParser.getCurrentName();
jsonParser.nextToken();
if (!(propertyName.equals("type") && jsonParser.getValueAsString().equals("FeatureCollection"))) {
throw new IOException("GeoJSON file must contain a top-level FeatureCollection");
}

jsonParser.nextToken();
propertyName = jsonParser.getCurrentName();

if (!(propertyName.equals("features") && jsonParser.nextToken() == JsonToken.START_ARRAY)) {
throw new IOException("GeoJSON FeatureCollection must contain an array of features");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.javarosa.core.model.instance.geojson;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import java.util.Map;
import org.javarosa.core.model.data.UncastData;
import org.javarosa.core.model.instance.TreeElement;

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class GeojsonFeature {
private String type;
private GeojsonGeometry geometry;
private Map<String, String> properties;

public TreeElement toTreeElement(int multiplicity) {
TreeElement item = new TreeElement("item", multiplicity);

TreeElement geoField = new TreeElement("geometry", 0);
geoField.setValue(new UncastData(geometry.getOdkCoordinates()));
item.addChild(geoField);

for (String property : properties.keySet()) {
TreeElement field = new TreeElement(property, 0);
field.setValue(new UncastData(properties.get(property)));
item.addChild(field);
}

return item;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2022 ODK
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.javarosa.core.model.instance.geojson;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import java.util.ArrayList;

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class GeojsonGeometry {
private String type;
private ArrayList<String> coordinates;

public String getType() {
return type;
}

public String getOdkCoordinates() {
if (!(type.equals("Point") && coordinates.size() == 2)) {
throw new UnsupportedOperationException("Only Points are currently supported");
}

return coordinates.get(0) + " " + coordinates.get(1) + " 0 0";
}
}
13 changes: 6 additions & 7 deletions src/main/java/org/javarosa/core/util/GeoUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,17 @@

package org.javarosa.core.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

import static java.lang.Math.PI;
import static java.lang.Math.abs;
import static java.lang.Math.acos;
import static java.lang.Math.cos;
import static java.lang.Math.sin;
import static java.lang.Math.toRadians;
import static java.lang.Math.PI;

import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Author: Meletis Margaritis
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package org.javarosa.core.model.instance.geojson;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.fail;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.javarosa.core.model.instance.TreeElement;
import org.junit.Test;

public class GeoJsonExternalInstanceTest {
@Test
public void parse_throwsException_ifTopLevelElementNotFeatureCollection() {
try {
GeoJsonExternalInstance.parse("id", new ByteArrayInputStream(GEOMETRY_COLLECTION_GEOJSON.getBytes(StandardCharsets.UTF_8)));
fail("Exception expected");
} catch (IOException e) {
// expected
}
}

@Test
public void parse_parsesMultipleFeatures() throws IOException {
TreeElement featureCollection = GeoJsonExternalInstance.parse("id", new ByteArrayInputStream(FEATURE_COLLECTION_GEOJSON.getBytes(StandardCharsets.UTF_8)));
assertThat(featureCollection.getNumChildren(), is(2));
}

@Test
public void parse_addsGeometryAsChild() throws IOException {
TreeElement featureCollection = GeoJsonExternalInstance.parse("id", new ByteArrayInputStream(FEATURE_COLLECTION_GEOJSON.getBytes(StandardCharsets.UTF_8)));
assertThat(featureCollection.getChildAt(0).getChild("geometry", 0).getValue().getValue(), is("102 0.5 0 0"));
assertThat(featureCollection.getChildAt(1).getChild("geometry", 0).getValue().getValue(), is("104 0.5 0 0"));
}

@Test
public void parse_addsAllOtherPropertiesAsChildren() throws IOException {
TreeElement featureCollection = GeoJsonExternalInstance.parse("id", new ByteArrayInputStream(FEATURE_COLLECTION_GEOJSON.getBytes(StandardCharsets.UTF_8)));
assertThat(featureCollection.getChildAt(0).getNumChildren(), is(4));
assertThat(featureCollection.getChildAt(0).getChild("name", 0).getValue().getValue(), is("My cool point"));

assertThat(featureCollection.getChildAt(1).getNumChildren(), is(5));
assertThat(featureCollection.getChildAt(1).getChild("special-property", 0).getValue().getValue(), is("special value"));
}

@Test
public void getOdkCoordinates_convertsPointGeoJsonGeometry() throws IOException {
String point = "{ \"type\": \"Point\", \"coordinates\": [ 102, 0.5 ] }";
ObjectMapper objectMapper = new ObjectMapper();
GeojsonGeometry geometry = objectMapper.readValue(point, GeojsonGeometry.class);
assertThat(geometry.getOdkCoordinates(), is("102 0.5 0 0"));
}

@Test
public void getOdkCoordinates_throwsException_ifGeometryNotPoint() throws IOException {
String notPoint = "{\n" +
" \"type\": \"LineString\",\n" +
" \"coordinates\": [102.0, 0.0]\n" +
" }";
ObjectMapper objectMapper = new ObjectMapper();
GeojsonGeometry geometry = objectMapper.readValue(notPoint, GeojsonGeometry.class);

try {
geometry.getOdkCoordinates();
fail("Exception expected");
} catch (UnsupportedOperationException e) {
// expected
}
}

private static final String GEOMETRY_COLLECTION_GEOJSON = "{\n" +
" \"geometries\": [\n" +
" {\n" +
" \"coordinates\": [\n" +
" [\n" +
" 10.0,\n" +
" 11.2\n" +
" ],\n" +
" [\n" +
" 10.5,\n" +
" 11.9\n" +
" ]\n" +
" ],\n" +
" \"type\": \"Linestring\"\n" +
" },\n" +
" {\n" +
" \"coordinates\": [\n" +
" 10.0,\n" +
" 20.0\n" +
" ],\n" +
" \"type\": \"Point\"\n" +
" }\n" +
" ],\n" +
" \"type\": \"GeometryCollection\"\n" +
"}";

private static final String FEATURE_COLLECTION_GEOJSON = "{\n" +
" \"type\": \"FeatureCollection\",\n" +
" \"features\": [\n" +
" {\n" +
" \"type\": \"Feature\",\n" +
" \"geometry\": {\n" +
" \"type\": \"Point\",\n" +
" \"coordinates\": [\n" +
" 102,\n" +
" 0.5\n" +
" ]\n" +
" },\n" +
" \"properties\": {\n" +
" \"id\": \"fs87b\",\n" +
" \"name\": \"My cool point\",\n" +
" \"foo\": \"bar\"\n" +
" }\n" +
" },\n" +
" {\n" +
" \"type\": \"Feature\",\n" +
" \"geometry\": {\n" +
" \"type\": \"Point\",\n" +
" \"coordinates\": [\n" +
" 104,\n" +
" 0.5\n" +
" ]\n" +
" },\n" +
" \"properties\": {\n" +
" \"id\": \"67abie\",\n" +
" \"name\": \"Your cool point\",\n" +
" \"foo\": \"quux\",\n" +
" \"special-property\": \"special value\"\n" +
" }\n" +
" }\n" +
" ]\n" +
"}";
}
7 changes: 3 additions & 4 deletions src/test/java/org/javarosa/core/util/GeoUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,15 @@

package org.javarosa.core.util;

import static org.javarosa.core.util.GeoUtils.EARTH_EQUATORIAL_CIRCUMFERENCE_METERS;
import static org.junit.Assert.assertEquals;

import java.util.ArrayList;
import java.util.List;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.javarosa.core.util.GeoUtils.EARTH_EQUATORIAL_CIRCUMFERENCE_METERS;
import static org.junit.Assert.assertEquals;

/**
* Tests features of GeoUtils. The tests whose names start with areaAndDistanceAreCorrectForPath
* use data from www.mapdevelopers.com/area_finder.php. They test that the enclosed area,
Expand Down

0 comments on commit 6d31598

Please sign in to comment.