Skip to content

Commit

Permalink
Merge branch 'csv_2_geojson'
Browse files Browse the repository at this point in the history
  • Loading branch information
simonpoole committed Jan 11, 2025
2 parents a3d7b19 + 06dfbf1 commit 0e484ad
Show file tree
Hide file tree
Showing 13 changed files with 244 additions and 12 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ dependencies {
implementation 'com.github.zedlabs:ElementHistoryDialog:1.1.1'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.github.kix2902:CompassView:master-SNAPSHOT'
implementation 'com.opencsv:opencsv:5.9'

implementation "ch.poole:PoParser:0.8.0"
implementation "ch.poole:OpeningHoursParser:0.28.2"
Expand Down
2 changes: 1 addition & 1 deletion documentation/docs/help/en/Main map display.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ The layer dialog supports the following actions on the layer entries:
* __Down__ move the layer down.
* __+__ button:
* for disabled layers that can only be displayed once it will show a corresponding "Enable ..." entry that will turn the layer on.
* __Add GeoJSON layer__ Loads a GeoJSON layer from a file in to a new GeoJSON layer.
* __Add GeoJSON layer__ Loads a GeoJSON layer from a file in to a new GeoJSON layer, this will load CVS files with suitable (WGS84) longitude and latitude columns and write a converted file to the Vespucci directory.
* __Add background imagery layer__ Adds a tile based imagery layer from the internal configuration, which can be from ELI or JOSM, or a custom imagery layer.
* __Add overlay imagery layer__ As above but assumes that the layer is partially transparent.
* __Enable photo layer__ Enables the photo layer this will display clickable icons for photos that will start an internal or external viewer. Which photos can be displayed depends strongly on your Android version and settings [Advanced preferences](Advanced%20preferences.md).
Expand Down
26 changes: 26 additions & 0 deletions src/androidTest/java/de/blau/android/layer/LayerDialogTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;

import com.mapbox.geojson.Point;
import com.orhanobut.mockwebserverplus.MockWebServerPlus;

import android.app.Instrumentation;
Expand Down Expand Up @@ -319,6 +320,31 @@ public void geoJsonLayer() {
assertTrue(TestUtils.clickText(device, false, main.getString(R.string.discard), true, false));
assertNull(map.getGeojsonLayer());
}

/**
* Load cvs
*/
@Test
public void geoJsonLayerFromCVS() {
final String cvsFile = "cvs-geojson.csv";
try {
JavaResources.copyFileFromResources(main, cvsFile, null, "/");
} catch (IOException e) {
fail(e.getMessage());
}
assertTrue(TestUtils.clickResource(device, true, device.getCurrentPackageName() + ":id/layers", true));
assertTrue(TestUtils.clickButton(device, device.getCurrentPackageName() + ":id/add", true));
assertTrue(TestUtils.clickText(device, false, main.getString(R.string.menu_layers_load_geojson), true, false));
TestUtils.selectFile(device, main, null, cvsFile, true);
assertTrue(TestUtils.clickText(device, false, main.getString(R.string.okay), true, false));
assertTrue(TestUtils.clickText(device, false, main.getString(R.string.Done), true, false));
TestUtils.unlock(device);
TestUtils.clickAtCoordinates(device, map, 8.06783179982675, 47.399875769847, true);

assertTrue(TestUtils.findText(device, false, main.getString(R.string.feature_information), 5000));
assertTrue(TestUtils.findText(device, false, "Beulen Werke AG"));
assertTrue(TestUtils.clickText(device, false, main.getString(R.string.done), true, false));
}

/**
* Add two geojson layers, hide the 1st one then discard it
Expand Down
1 change: 1 addition & 0 deletions src/main/java/de/blau/android/contract/FileExtensions.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public final class FileExtensions {
public static final String TEMP = "temp";
public static final String PMTILES = "pmtiles";
public static final String XML = "xml";
public static final String CSV = "csv";

/**
* Private default constructor
Expand Down
14 changes: 9 additions & 5 deletions src/main/java/de/blau/android/contract/MimeTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ public final class MimeTypes {
public static final String JPEG = "image/jpeg";
public static final String PNG = "image/png";
public static final String HEIC = "image/heic";
public static final String GPX = "application/gpx+xml";
public static final String GEOJSON = "application/geo+json";
public static final String TEXTPLAIN = "text/plain";
public static final String TEXTXML = "text/xml";
public static final String ZIP = "application/zip";

public static final String GPX = "application/gpx+xml";
public static final String GEOJSON = "application/geo+json";

public static final String TEXTPLAIN = "text/plain";
public static final String TEXTXML = "text/xml";
public static final String TEXTCSV = "text/comma-separated-values";

public static final String ZIP = "application/zip";

// types and subtypes
public static final String IMAGE_TYPE = "image";
Expand Down
51 changes: 50 additions & 1 deletion src/main/java/de/blau/android/dialogs/Layers.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,33 @@

import static de.blau.android.contract.Constants.LOG_TAG_LEN;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;

import org.mozilla.javascript.RhinoException;

import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.mapbox.geojson.Feature;
import com.mapbox.geojson.FeatureCollection;
import com.mapbox.geojson.Point;
import com.opencsv.CSVReader;
import com.opencsv.exceptions.CsvException;

import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.database.sqlite.SQLiteDatabase;
Expand Down Expand Up @@ -68,6 +78,8 @@
import de.blau.android.Map;
import de.blau.android.Mode;
import de.blau.android.R;
import de.blau.android.contract.FileExtensions;
import de.blau.android.contract.MimeTypes;
import de.blau.android.contract.Paths;
import de.blau.android.exception.OsmIllegalOperationException;
import de.blau.android.gpx.Track;
Expand Down Expand Up @@ -110,6 +122,7 @@
import de.blau.android.util.Density;
import de.blau.android.util.ExecutorTask;
import de.blau.android.util.FileUtil;
import de.blau.android.util.GeoJson;
import de.blau.android.util.ReadFile;
import de.blau.android.util.SaveFile;
import de.blau.android.util.SavingHelper;
Expand Down Expand Up @@ -453,10 +466,20 @@ public void read(FragmentActivity activity, List<Uri> fileUris) {
*/
private void addStyleableLayerFromUri(@NonNull final FragmentActivity activity, @NonNull final Preferences prefs, @NonNull final Map map,
@NonNull LayerType type, @NonNull Uri fileUri, boolean showDialog) {
final String uriString = fileUri.toString();
String uriString = fileUri.toString();
de.blau.android.layer.StyleableLayer layer = (de.blau.android.layer.StyleableLayer) map.getLayer(type, uriString);
if (layer == null) {
Log.d(DEBUG_TAG, "addStyleableLayerFromUri " + uriString);
final ContentResolver contentResolver = activity.getContentResolver();
String mimeType = contentResolver.getType(fileUri);
if (MimeTypes.TEXTCSV.equals(mimeType)) {
try {
uriString = convertCSV(activity, fileUri).toString();
} catch (IOException | CsvException | SecurityException | IllegalArgumentException e) {
ScreenMessage.toastTopError(activity, activity.getString(R.string.toast_error_converting, e.getLocalizedMessage()));
return;
}
}
de.blau.android.layer.Util.addLayer(activity, type, uriString);
map.setUpLayers(activity);
layer = (de.blau.android.layer.StyleableLayer) map.getLayer(type, uriString);
Expand All @@ -474,6 +497,32 @@ private void addStyleableLayerFromUri(@NonNull final FragmentActivity activity,
}
}

/**
* Convert an CSV file to geojson and write it to our directory, returning an URi
*
* @param context an Android Context
* @param fileUri the original Uri
* @return an Uri a new Uri for the converted file
* @throws IOException if reading the CSV fails
* @throws CsvException if the CSV can't be parsed
*/
@NonNull
private Uri convertCSV(@NonNull final Context context, @NonNull Uri fileUri) throws IOException, CsvException {
final ContentResolver contentResolver = context.getContentResolver();
try (InputStream is = contentResolver.openInputStream(fileUri)) {
FeatureCollection featureCollection = GeoJson.fromCSV(is);
String fileName = FileUtil.fileNameFromUri(fileUri).replaceAll("\\." + FileExtensions.CSV + "$", "\\." + FileExtensions.GEOJSON);
if (!fileName.contains(FileExtensions.GEOJSON)) {
fileName = fileName + "." + FileExtensions.GEOJSON;
}
File output = FileUtil.openFileForWriting(context, fileName);
try (PrintWriter p = new PrintWriter(new FileWriter(output))) {
p.write(featureCollection.toJson());
}
return Uri.parse(FileUtil.FILE_SCHEME_PREFIX + output.getAbsolutePath());
}
}

/**
* Add a Layer from a mapbox-gl Style
*
Expand Down
18 changes: 17 additions & 1 deletion src/main/java/de/blau/android/util/FileUtil.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package de.blau.android.util;

import static de.blau.android.contract.Constants.LOG_TAG_LEN;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
Expand Down Expand Up @@ -32,7 +34,9 @@
import de.blau.android.dialogs.Tip;

public final class FileUtil {
private static final String DEBUG_TAG = FileUtil.class.getSimpleName().substring(0, Math.min(23, FileUtil.class.getSimpleName().length()));

private static final int TAG_LEN = Math.min(LOG_TAG_LEN, FileUtil.class.getSimpleName().length());
private static final String DEBUG_TAG = FileUtil.class.getSimpleName().substring(0, TAG_LEN);

private static final char PATH_DELIMITER_CHAR = '/';
public static final String FILE_SCHEME_PREFIX = Schemes.FILE + ":";
Expand Down Expand Up @@ -465,4 +469,16 @@ protected void onPostExecute(Boolean result) {
}
}.execute();
}

/**
* Get the filename from an uri (assuming it has one)
*
* @param fileUri the Uri
* @return the filename
*/
@NonNull
public static String fileNameFromUri(@NonNull Uri fileUri) {
String[] temp = fileUri.getLastPathSegment().split("" + PATH_DELIMITER_CHAR);
return temp[temp.length - 1];
}
}
74 changes: 73 additions & 1 deletion src/main/java/de/blau/android/util/GeoJson.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package de.blau.android.util;

import static de.blau.android.contract.Constants.LOG_TAG_LEN;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import com.google.gson.GsonBuilder;
import com.mapbox.geojson.CoordinateContainer;
import com.mapbox.geojson.Feature;
import com.mapbox.geojson.FeatureCollection;
import com.mapbox.geojson.Geometry;
import com.mapbox.geojson.GeometryAdapterFactory;
import com.mapbox.geojson.GeometryCollection;
Expand All @@ -14,6 +21,8 @@
import com.mapbox.geojson.Polygon;
import com.mapbox.geojson.gson.BoundingBoxTypeAdapter;
import com.mapbox.geojson.gson.GeoJsonAdapterFactory;
import com.opencsv.CSVReader;
import com.opencsv.exceptions.CsvException;

import android.graphics.Canvas;
import android.graphics.Paint;
Expand All @@ -32,7 +41,11 @@
*/
public final class GeoJson {

private static final String DEBUG_TAG = GeoJson.class.getSimpleName().substring(0, Math.min(23, GeoJson.class.getSimpleName().length()));
private static final int TAG_LEN = Math.min(LOG_TAG_LEN, GeoJson.class.getSimpleName().length());
private static final String DEBUG_TAG = GeoJson.class.getSimpleName().substring(0, TAG_LEN);

private static final String LON = "lon";
private static final String LAT = "lat";

/**
* Private constructor to stop instantiation
Expand Down Expand Up @@ -239,4 +252,63 @@ public static Geometry geometryFromJson(@NonNull String json) {
gson.registerTypeAdapter(BoundingBox.class, new BoundingBoxTypeAdapter());
return gson.create().fromJson(json, Geometry.class);
}

/**
* Convert a CSV format input stream to a GeoJson FeatureCollection
*
* @param is the input stream in CSV format
* @return a FeatureCollection holding Points created from the CSV
* @throws IOException if reading the CSV fails
* @throws CsvException if the CSV can't be parsed
* @throws IllegalArgumentException if no header for the coordinates can be found, or coordinates cannot be parsed
*
*/
@NonNull
public static FeatureCollection fromCSV(@NonNull InputStream is) throws IOException, CsvException {
List<Feature> features = new ArrayList<>();
try (CSVReader csvReader = new CSVReader(new InputStreamReader(is))) {
List<String[]> csv = csvReader.readAll();
String[] header = csv.get(0);
int latIndex = -1;
int lonIndex = -1;
for (int i = 0; i < header.length; i++) {
if (header[i].toLowerCase(Locale.US).startsWith(LAT)) {
latIndex = i;
}
if (header[i].toLowerCase(Locale.US).startsWith(LON)) {
lonIndex = i;
}
}
if (latIndex < 0 || lonIndex < 0) {
throw new IllegalArgumentException("Unable to find coordinates in CSV header");
}
for (int i = 1; i < csv.size(); i++) {
String[] values = csv.get(i);
double lon = Double.parseDouble(values[lonIndex]);
double lat = Double.parseDouble(values[latIndex]);
checkCoordinates(lon, lat);
Feature feature = Feature.fromGeometry(Point.fromLngLat(lon, lat));
for (int j = 0; j < header.length; j++) {
if (j == latIndex || j == lonIndex) {
continue;
}
feature.addStringProperty(header[j], values[j]);
}
features.add(feature);
}
}
return FeatureCollection.fromFeatures(features);
}

/**
* Cehck that the coordinates are in WGS84 value ranges
*
* @param lon the longitude
* @param lat the latitude
*/
private static void checkCoordinates(double lon, double lat) {
if (lat < -GeoMath.MAX_LAT || lat > GeoMath.MAX_LAT || lon < -GeoMath.MAX_LON || lon > GeoMath.MAX_LON) {
throw new IllegalArgumentException("Coordinates out of WGS84 range. Lat: " + lat + " Lon: " + lon);
}
}
}
4 changes: 1 addition & 3 deletions src/main/java/de/blau/android/util/GeoUriData.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ public class GeoUriData implements Serializable {

private static final String ZOOM_PARAMETER = "z";

private static final int MAX_LAT = 90;

private double lat = -Double.MAX_VALUE;
private double lon = -Double.MAX_VALUE;
private int zoom = -1;
Expand Down Expand Up @@ -175,6 +173,6 @@ public static GeoUriData parse(@NonNull String schemeSpecificPart) {
* @return true if the object contains both a latitude and a longitude
*/
boolean isValid() {
return lat >= -MAX_LAT && lat <= MAX_LAT && lon >= -GeoMath.MAX_LON && lon <= GeoMath.MAX_LON;
return lat >= -GeoMath.MAX_LAT && lat <= GeoMath.MAX_LAT && lon >= -GeoMath.MAX_LON && lon <= GeoMath.MAX_LON;
}
}
1 change: 1 addition & 0 deletions src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,7 @@
<string name="toast_file_already_exists">File %1$s already exists</string>
<string name="toast_file_error_for">File error for %1$s</string>
<string name="toast_error_reading">Error reading %1$s</string>
<string name="toast_error_converting">Error converting %1$s</string>
<string name="toast_read_successfully">File read successfully</string>
<string name="toast_error_writing">Error writing %1$s</string>
<string name="toast_successfully_written">%1$s successfully written</string>
Expand Down
Loading

0 comments on commit 0e484ad

Please sign in to comment.