diff --git a/CHANGELOG.md b/CHANGELOG.md index b1d098210..ae8779c27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Mapbox welcomes participation and contributions from everyone. ### main - Added `DirectionsRefreshResponse#fromJson(Reader)`, a static factory method that deserializes a `DirectionsRefreshResponse` from a `java.io.Reader`. +- Added `FlattenListOfPoints` to hold a list of points in a more memory-efficient way. +- Deprecate `LineString#coordinates()` and `Point#coordinates()`. It's encouraged to use the new `flattenCoordinates()` methods. + ### v7.9.0 - November 20, 2025 diff --git a/services-geojson/src/main/java/com/mapbox/geojson/BaseCoordinatesTypeAdapter.java b/services-geojson/src/main/java/com/mapbox/geojson/BaseCoordinatesTypeAdapter.java index 752c43efb..38c165f2e 100644 --- a/services-geojson/src/main/java/com/mapbox/geojson/BaseCoordinatesTypeAdapter.java +++ b/services-geojson/src/main/java/com/mapbox/geojson/BaseCoordinatesTypeAdapter.java @@ -1,22 +1,20 @@ package com.mapbox.geojson; import androidx.annotation.Keep; +import androidx.annotation.NonNull; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; -import com.mapbox.geojson.exception.GeoJsonException; import com.mapbox.geojson.shifter.CoordinateShifterManager; import com.mapbox.geojson.utils.GeoJsonUtils; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; /** - * Base class for converting {@code T} instance of coordinates to JSON and - * JSON to instance of {@code T}. + * Base class for converting {@code T} instance of coordinates to JSON and + * JSON to instance of {@code T}. * * @param Type of coordinates * @since 4.6.0 @@ -25,25 +23,19 @@ abstract class BaseCoordinatesTypeAdapter extends TypeAdapter { - protected void writePoint(JsonWriter out, Point point) throws IOException { + protected void writePoint(JsonWriter out, Point point) throws IOException { if (point == null) { return; } - writePointList(out, point.coordinates()); + writePointList(out, point.flattenCoordinates()); } protected Point readPoint(JsonReader in) throws IOException { - - List coordinates = readPointList(in); - if (coordinates != null && coordinates.size() > 1) { - return new Point("Point",null, coordinates); - } - - throw new GeoJsonException(" Point coordinates should be non-null double array"); + return new Point("Point", null, readPointList(in)); } - protected void writePointList(JsonWriter out, List value) throws IOException { + protected void writePointList(JsonWriter out, double[] value) throws IOException { if (value == null) { return; @@ -52,38 +44,52 @@ protected void writePointList(JsonWriter out, List value) throws IOExcep out.beginArray(); // Unshift coordinates - List unshiftedCoordinates = - CoordinateShifterManager.getCoordinateShifter().unshiftPoint(value); + double[] unshiftedCoordinates = CoordinateShifterManager.getCoordinateShifter() + .unshiftPointArray(value); - out.value(GeoJsonUtils.trim(unshiftedCoordinates.get(0))); - out.value(GeoJsonUtils.trim(unshiftedCoordinates.get(1))); + out.value(GeoJsonUtils.trim(unshiftedCoordinates[0])); + out.value(GeoJsonUtils.trim(unshiftedCoordinates[1])); // Includes altitude - if (value.size() > 2) { - out.value(unshiftedCoordinates.get(2)); + if (value.length > 2) { + out.value(unshiftedCoordinates[2]); } out.endArray(); } - protected List readPointList(JsonReader in) throws IOException { - + @NonNull + protected double[] readPointList(JsonReader in) throws IOException { if (in.peek() == JsonToken.NULL) { throw new NullPointerException(); } - List coordinates = new ArrayList(3); + double lon; + double lat; + double altitude; in.beginArray(); - while (in.hasNext()) { - coordinates.add(in.nextDouble()); + if (in.hasNext()) { + lon = in.nextDouble(); + } else { + throw new IndexOutOfBoundsException("Point coordinates should contain at least two values"); } - in.endArray(); - - if (coordinates.size() > 2) { - return CoordinateShifterManager.getCoordinateShifter() - .shiftLonLatAlt(coordinates.get(0), coordinates.get(1), coordinates.get(2)); + if (in.hasNext()) { + lat = in.nextDouble(); + } else { + throw new IndexOutOfBoundsException("Point coordinates should contain at least two values"); + } + if (in.hasNext()) { + altitude = in.nextDouble(); + // Consume any extra value but don't store it + while (in.hasNext()) { + in.skipValue(); + } + in.endArray(); + return CoordinateShifterManager.getCoordinateShifter().shift(lon, lat, altitude); + } else { + in.endArray(); + return CoordinateShifterManager.getCoordinateShifter().shift(lon, lat); } - return CoordinateShifterManager.getCoordinateShifter() - .shiftLonLat(coordinates.get(0), coordinates.get(1)); + } } diff --git a/services-geojson/src/main/java/com/mapbox/geojson/BaseGeometryTypeAdapter.java b/services-geojson/src/main/java/com/mapbox/geojson/BaseGeometryTypeAdapter.java index 47ed7c913..9c435869c 100644 --- a/services-geojson/src/main/java/com/mapbox/geojson/BaseGeometryTypeAdapter.java +++ b/services-geojson/src/main/java/com/mapbox/geojson/BaseGeometryTypeAdapter.java @@ -18,29 +18,68 @@ * * @param Geometry * @param Type of coordinates + * @param The type of coordinates adapter * @since 4.6.0 */ @Keep -abstract class BaseGeometryTypeAdapter extends TypeAdapter { +abstract class BaseGeometryTypeAdapter extends TypeAdapter { private volatile TypeAdapter stringAdapter; private volatile TypeAdapter boundingBoxAdapter; - private volatile TypeAdapter coordinatesAdapter; + private final BaseCoordinatesTypeAdapter coordinatesAdapter; private final Gson gson; - BaseGeometryTypeAdapter(Gson gson, TypeAdapter coordinatesAdapter) { + BaseGeometryTypeAdapter(Gson gson, BaseCoordinatesTypeAdapter coordinatesAdapter) { + if (coordinatesAdapter == null) { + throw new GeoJsonException("Coordinates type adapter is null"); + } this.gson = gson; this.coordinatesAdapter = coordinatesAdapter; this.boundingBoxAdapter = new BoundingBoxTypeAdapter(); } - public void writeCoordinateContainer(JsonWriter jsonWriter, CoordinateContainer object) + public void writeFlattenedCoordinateContainer( + JsonWriter jsonWriter, + FlattenedCoordinateContainer object + ) throws IOException { + if (object == null) { + jsonWriter.nullValue(); + return; + } + writeCommon(jsonWriter, object); + jsonWriter.name("coordinates"); + coordinatesAdapter.write(jsonWriter, object.flattenCoordinates()); + jsonWriter.endObject(); + } + + public void writeCoordinateContainer(JsonWriter jsonWriter, CoordinateContainer object) throws IOException { if (object == null) { jsonWriter.nullValue(); return; } + + writeCommon(jsonWriter, object); + + jsonWriter.name("coordinates"); + if (object.coordinates() == null) { + jsonWriter.nullValue(); + } else { + coordinatesAdapter.write(jsonWriter, object.coordinates()); + } + + jsonWriter.endObject(); + } + + /** + * Write the common part of the coordinate container: "type" and "bbox". + */ + private void writeCommon( + JsonWriter jsonWriter, + @SuppressWarnings("rawtypes") + CoordinateContainer object + ) throws IOException { jsonWriter.beginObject(); jsonWriter.name("type"); if (object.type() == null) { @@ -64,17 +103,6 @@ public void writeCoordinateContainer(JsonWriter jsonWriter, CoordinateContainer< } boundingBoxAdapter.write(jsonWriter, object.bbox()); } - jsonWriter.name("coordinates"); - if (object.coordinates() == null) { - jsonWriter.nullValue(); - } else { - TypeAdapter coordinatesAdapter = this.coordinatesAdapter; - if (coordinatesAdapter == null) { - throw new GeoJsonException("Coordinates type adapter is null"); - } - coordinatesAdapter.write(jsonWriter, object.coordinates()); - } - jsonWriter.endObject(); } public CoordinateContainer readCoordinateContainer(JsonReader jsonReader) throws IOException { @@ -86,7 +114,7 @@ public CoordinateContainer readCoordinateContainer(JsonReader jsonReader) thr jsonReader.beginObject(); String type = null; BoundingBox bbox = null; - T coordinates = null; + A coordinates = null; while (jsonReader.hasNext()) { String name = jsonReader.nextName(); @@ -114,7 +142,7 @@ public CoordinateContainer readCoordinateContainer(JsonReader jsonReader) thr break; case "coordinates": - TypeAdapter coordinatesAdapter = this.coordinatesAdapter; + TypeAdapter coordinatesAdapter = this.coordinatesAdapter; if (coordinatesAdapter == null) { throw new GeoJsonException("Coordinates type adapter is null"); } @@ -133,5 +161,5 @@ public CoordinateContainer readCoordinateContainer(JsonReader jsonReader) thr abstract CoordinateContainer createCoordinateContainer(String type, BoundingBox bbox, - T coordinates); + A coordinates); } diff --git a/services-geojson/src/main/java/com/mapbox/geojson/FlattenListOfPoints.java b/services-geojson/src/main/java/com/mapbox/geojson/FlattenListOfPoints.java new file mode 100644 index 000000000..7442ecc1f --- /dev/null +++ b/services-geojson/src/main/java/com/mapbox/geojson/FlattenListOfPoints.java @@ -0,0 +1,219 @@ +package com.mapbox.geojson; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * A class that contains the required data to store a list of {@link Point}s as a flat structure. + */ +@Keep +public class FlattenListOfPoints implements Serializable { + /** + * A one-dimensional array to store the flattened coordinates: [lng1, lat1, lng2, lat2, ...]. + *

+ * Note: we use one-dimensional array for performance reasons related to JNI access ( + * Android JNI Tips + * - Primitive arrays) + */ + @NonNull + private final double[] flattenLngLatPoints; + /** + * An array to store the altitudes of each coordinate or {@link Double#NaN} if the coordinate + * does not have altitude. + */ + @Nullable + private final double[] altitudes; + /** + * An array to store the {@link BoundingBox} of each coordinate or null if the coordinate does + * not have bounding box. + * In practice is very unlikely that the points have bounding box when inside a list of points. + */ + @Nullable + private BoundingBox[] boundingBoxes; + + /** + * + * @param flattenLngLatPoints A one-dimensional array coordinates: [lng1, lat1, lng2, lat2, ...]. + * It is stored as is, no copy or shifting is done. + * @param altitudes An array of altitudes of each coordinate or {@link Double#NaN} if the + * coordinate does not have altitude. It is stored as is, no copy or shifting is + * done. + */ + public FlattenListOfPoints(@NonNull double[] flattenLngLatPoints, @Nullable double[] altitudes) { + this.flattenLngLatPoints = flattenLngLatPoints; + this.altitudes = altitudes; + } + + FlattenListOfPoints(@NonNull List points) { + if (points.isEmpty()) { + this.flattenLngLatPoints = new double[0]; + this.altitudes = null; + this.boundingBoxes = null; + return; + } + double[] flattenLngLatCoordinates = new double[points.size() * 2]; + double[] altitudes = null; + for (int i = 0; i < points.size(); i++) { + Point point = points.get(i); + flattenLngLatCoordinates[i * 2] = point.longitude(); + flattenLngLatCoordinates[(i * 2) + 1] = point.latitude(); + + // It is quite common to not have altitude in Point. Therefore only if we have points + // with altitude then we create an array to store those. + if (point.hasAltitude()) { + // If one point has altitude we create an array of double to store the altitudes. + if (altitudes == null) { + altitudes = new double[points.size()]; + // Fill in any previous altitude as NaN + for (int j = 0; j < i; j++) { + altitudes[j] = Double.NaN; + } + } + altitudes[i] = point.altitude(); + } else if (altitudes != null) { + // If we are storing altitudes but this point doesn't have it then set it to NaN + altitudes[i] = Double.NaN; + } + + // Similarly to altitudes, if one point has bounding box we create an array to store those. + if (point.bbox() != null) { + if (boundingBoxes == null) { + boundingBoxes = new BoundingBox[points.size()]; + } + boundingBoxes[i] = point.bbox(); + } + } + this.flattenLngLatPoints = flattenLngLatCoordinates; + this.altitudes = altitudes; + } + + /** + * @return a flatten array of all the coordinates (longitude, latitude): + * [lng1, lat1, lng2, lat2, ...]. + */ + @NonNull + public double[] getFlattenLngLatArray() { + return flattenLngLatPoints; + } + + /** + * @return an array of all the altitudes (or null if no altitudes are present at all). If a + * coordinate does not contain altitude it's represented as {@link Double#NaN} + */ + @Nullable + public double[] getAltitudes() { + return altitudes; + } + + /** + * Returns the total number of points stored in this flattened structure. + * + * @return the total number of points. + */ + public int size() { + return flattenLngLatPoints.length / 2; + } + + /** + * Creates a list of {@link Point}s and returns it. + *

+ * If possible consider using {@link #getFlattenLngLatArray()} and {@link #getAltitudes()} + * instead. + * + * @return a list of {@link Point}s + */ + @NonNull + public List points() { + if (flattenLngLatPoints.length == 0) { + return new ArrayList<>(); + } + ArrayList points = new ArrayList<>(flattenLngLatPoints.length / 2); + for (int i = 0; i < flattenLngLatPoints.length / 2; i++) { + double[] coordinates; + if (altitudes != null && !Double.isNaN(altitudes[i])) { + coordinates = new double[]{ + flattenLngLatPoints[i * 2], + flattenLngLatPoints[(i * 2) + 1], + altitudes[i] + }; + } else { + coordinates = new double[]{flattenLngLatPoints[i * 2], flattenLngLatPoints[(i * 2) + 1]}; + } + BoundingBox pointBbox = null; + if (boundingBoxes != null) { + pointBbox = boundingBoxes[i]; + } + // We create the Point directly instead of static factory method to avoid double coordinate + // shifting. + Point point = new Point(Point.TYPE, pointBbox, coordinates); + points.add(point); + } + return points; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof FlattenListOfPoints)) { + return false; + } + FlattenListOfPoints that = (FlattenListOfPoints) o; + return Objects.deepEquals(flattenLngLatPoints, that.flattenLngLatPoints) + && Objects.deepEquals(altitudes, that.altitudes) + && Objects.deepEquals(boundingBoxes, that.boundingBoxes); + } + + @Override + public int hashCode() { + return Objects.hash( + Arrays.hashCode(flattenLngLatPoints), + Arrays.hashCode(altitudes), + Arrays.hashCode(boundingBoxes) + ); + } + + @Override + public String toString() { + int totalPoints = flattenLngLatPoints.length / 2; + + int iMax = totalPoints - 1; + if (iMax == -1) { + return "[]"; + } + + StringBuilder b = new StringBuilder(); + b.append("["); + + for (int i = 0;; i++) { + b.append("Point{type=Point, bbox="); + if (boundingBoxes != null) { + BoundingBox boundingBox = boundingBoxes[i]; + b.append(boundingBox); + } else { + b.append("null"); + } + b.append(", coordinates=["); + b.append(flattenLngLatPoints[i * 2]); + b.append(", "); + b.append(flattenLngLatPoints[i * 2 + 1]); + if (altitudes != null && !Double.isNaN(altitudes[i])) { + b.append(", "); + b.append(altitudes[i]); + } + b.append("]}"); + if (i == iMax) { + b.append("]"); + break; + } + b.append(", "); + } + + return b.toString(); + } +} diff --git a/services-geojson/src/main/java/com/mapbox/geojson/FlattenListOfPointsTypeAdapter.java b/services-geojson/src/main/java/com/mapbox/geojson/FlattenListOfPointsTypeAdapter.java new file mode 100644 index 000000000..4c0c3a700 --- /dev/null +++ b/services-geojson/src/main/java/com/mapbox/geojson/FlattenListOfPointsTypeAdapter.java @@ -0,0 +1,142 @@ +package com.mapbox.geojson; + +import androidx.annotation.Keep; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.mapbox.geojson.exception.GeoJsonException; + +import java.io.IOException; + +/** + * Type Adapter to serialize/deserialize {@link FlattenListOfPoints} into/from two dimensional + * JSON array. + */ +@Keep +class FlattenListOfPointsTypeAdapter extends BaseCoordinatesTypeAdapter { + + private static final int INITIAL_CAPACITY = 100; + + @Override + public void write(JsonWriter out, FlattenListOfPoints flattenListOfPoints) throws IOException { + if (flattenListOfPoints == null) { + out.nullValue(); + return; + } + + out.beginArray(); + int size = flattenListOfPoints.size(); + if (size == 0) { + out.endArray(); + return; + } + + double[] flattenLngLatCoordinates = flattenListOfPoints.getFlattenLngLatArray(); + double[] altitudes = flattenListOfPoints.getAltitudes(); + + for (int i = 0; i < size; i++) { + double[] value; + if (altitudes != null && !Double.isNaN(altitudes[i])) { + value = new double[]{ + flattenLngLatCoordinates[i * 2], + flattenLngLatCoordinates[(i * 2) + 1], + altitudes[i] + }; + } else { + value = new double[]{ + flattenLngLatCoordinates[i * 2], + flattenLngLatCoordinates[(i * 2) + 1] + }; + } + + writePointList(out, value); + } + + out.endArray(); + } + + @Override + public FlattenListOfPoints read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + throw new NullPointerException(); + } + + if (in.peek() == JsonToken.BEGIN_ARRAY) { + in.beginArray(); + double[] flattenLngLats = new double[INITIAL_CAPACITY * 2]; + double[] altitudes = null; + int currentIdx = 0; + + while (in.peek() == JsonToken.BEGIN_ARRAY) { + in.beginArray(); + // Read longitude + if (in.hasNext()) { + flattenLngLats[currentIdx * 2] = in.nextDouble(); + } else { + throw new IndexOutOfBoundsException( + "Point coordinates should contain at least two values" + ); + } + + // Read latitude + if (in.hasNext()) { + flattenLngLats[currentIdx * 2 + 1] = in.nextDouble(); + } else { + throw new IndexOutOfBoundsException( + "Point coordinates should contain at least two values" + ); + } + + // Finally altitude if present + if (in.hasNext()) { + if (altitudes == null) { + altitudes = new double[flattenLngLats.length / 2]; + // Fill in any previous altitude as NaN + for (int j = 0; j < currentIdx; j++) { + altitudes[j] = Double.NaN; + } + } + altitudes[currentIdx] = in.nextDouble(); + // Consume any extra value but don't store it + while (in.hasNext()) { + in.skipValue(); + } + in.endArray(); + } else { + in.endArray(); + if (altitudes != null) { + // If we are storing altitudes but this point doesn't have it then set it to NaN + altitudes[currentIdx] = Double.NaN; + } + } + currentIdx++; + // If we run out of space we grow the the arrays + if (currentIdx * 2 >= flattenLngLats.length) { + double[] newFlattenLngLats = new double[flattenLngLats.length * 2]; + System.arraycopy(flattenLngLats, 0, newFlattenLngLats, 0, flattenLngLats.length); + flattenLngLats = newFlattenLngLats; + if (altitudes != null) { + double[] newAltitudes = new double[altitudes.length * 2]; + System.arraycopy(altitudes, 0, newAltitudes, 0, altitudes.length); + altitudes = newAltitudes; + } + } + } + in.endArray(); + + int totalPoints = currentIdx; + double[] trimmedFlattenLngLats = new double[totalPoints * 2]; + System.arraycopy(flattenLngLats, 0, trimmedFlattenLngLats, 0, totalPoints * 2); + double[] trimmedAltitudes = null; + if (altitudes != null) { + trimmedAltitudes = new double[totalPoints]; + System.arraycopy(altitudes, 0, trimmedAltitudes, 0, totalPoints); + } + + return new FlattenListOfPoints(trimmedFlattenLngLats, trimmedAltitudes); + } + + throw new GeoJsonException("coordinates should be non-null array of array of double"); + } +} diff --git a/services-geojson/src/main/java/com/mapbox/geojson/FlattenedCoordinateContainer.java b/services-geojson/src/main/java/com/mapbox/geojson/FlattenedCoordinateContainer.java new file mode 100644 index 000000000..398c81b0f --- /dev/null +++ b/services-geojson/src/main/java/com/mapbox/geojson/FlattenedCoordinateContainer.java @@ -0,0 +1,28 @@ +package com.mapbox.geojson; + +import androidx.annotation.Keep; + +/** + * Interface for GeoJSON geometry types that support both standard and flattened coordinate + * representations. The flattened representation stores coordinates in primitive arrays for + * improved performance, particularly for JNI access on Android. + *

+ * This interface extends {@link CoordinateContainer} to provide access to coordinates in their + * standard form (typically as {@link Point} objects) while also offering a flattened form + * optimized for low-level operations. + * + * @param the standard coordinate container type (e.g., {@code List}) + * @param

the flattened coordinate representation type (e.g., {@link FlattenListOfPoints}) + */ +@Keep +interface FlattenedCoordinateContainer extends CoordinateContainer { + /** + * Returns the flattened coordinate representation of this geometry. + *

+ * The flattened form stores coordinates in primitive arrays, which provides better performance + * for operations that require direct array access, particularly in JNI contexts. + * + * @return the flattened coordinate representation. + */ + P flattenCoordinates(); +} diff --git a/services-geojson/src/main/java/com/mapbox/geojson/LineString.java b/services-geojson/src/main/java/com/mapbox/geojson/LineString.java index e57edb101..a033a4f36 100644 --- a/services-geojson/src/main/java/com/mapbox/geojson/LineString.java +++ b/services-geojson/src/main/java/com/mapbox/geojson/LineString.java @@ -9,11 +9,14 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import com.mapbox.geojson.gson.GeoJsonAdapterFactory; +import com.mapbox.geojson.shifter.CoordinateShifter; +import com.mapbox.geojson.shifter.CoordinateShifterManager; import com.mapbox.geojson.utils.PolylineUtils; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * A linestring represents two or more geographic points that share a relationship and is one of the @@ -49,7 +52,8 @@ * @since 1.0.0 */ @Keep -public final class LineString implements CoordinateContainer> { +public final class LineString implements + FlattenedCoordinateContainer, FlattenListOfPoints> { private static final String TYPE = "LineString"; @@ -57,7 +61,8 @@ public final class LineString implements CoordinateContainer> { private final BoundingBox bbox; - private final List coordinates; + @NonNull + private final FlattenListOfPoints flattenListOfPoints; /** * Create a new instance of this class by passing in a formatted valid JSON String. If you are @@ -127,38 +132,41 @@ public static LineString fromLngLats(@NonNull List points, @Nullable Boun return new LineString(TYPE, bbox, points); } + static LineString fromLngLats(double[][] coordinates) { + ArrayList converted = new ArrayList<>(coordinates.length); + for (int i = 0; i < coordinates.length; i++) { + converted.add(Point.fromLngLat(coordinates[i])); + } + return LineString.fromLngLats(converted); + } + /** - * Create a new instance of this class by defining a {@link MultiPoint} object and passing. The - * multipoint object should comply with the GeoJson specifications described in the documentation. + * Create a new instance by providing a flatten array of [lng1, lat1, lng2, lat2, ...]. + * The flatten array object should comply with the GeoJson specifications described in the + * documentation. * - * @param multiPoint which will make up the LineString geometry - * @param bbox optionally include a bbox definition as a double array + * @param flattenLngLatArray which will make up the LineString geometry. WARNING: The points will + * be shifted according to the current + * {@link CoordinateShifterManager#getCoordinateShifter()} in place! + * @param bbox optionally include a bbox definition * @return a new instance of this class defined by the values passed inside this static factory * method - * @since 3.0.0 */ - public static LineString fromLngLats(@NonNull MultiPoint multiPoint, @Nullable BoundingBox bbox) { - return new LineString(TYPE, bbox, multiPoint.coordinates()); - } - - LineString(String type, @Nullable BoundingBox bbox, List coordinates) { - if (type == null) { - throw new NullPointerException("Null type"); + public static LineString fromFlattenArrayOfPoints( + double[] flattenLngLatArray, + @Nullable BoundingBox bbox + ) { + CoordinateShifter coordinateShifter = CoordinateShifterManager.getCoordinateShifter(); + // Iterate all the points and shift them + for (int i = 0; i < flattenLngLatArray.length / 2; i++) { + double lon = flattenLngLatArray[i * 2]; + double lat = flattenLngLatArray[(i * 2) + 1]; + double[] shifted = coordinateShifter.shift(lon, lat); + flattenLngLatArray[i * 2] = shifted[0]; + flattenLngLatArray[(i * 2) + 1] = shifted[1]; } - this.type = type; - this.bbox = bbox; - if (coordinates == null) { - throw new NullPointerException("Null coordinates"); - } - this.coordinates = coordinates; - } - static LineString fromLngLats(double[][] coordinates) { - ArrayList converted = new ArrayList<>(coordinates.length); - for (int i = 0; i < coordinates.length; i++) { - converted.add(Point.fromLngLat(coordinates[i])); - } - return LineString.fromLngLats(converted); + return new LineString(TYPE, bbox, new FlattenListOfPoints(flattenLngLatArray, null)); } /** @@ -176,7 +184,24 @@ static LineString fromLngLats(double[][] coordinates) { * @since 1.0.0 */ public static LineString fromPolyline(@NonNull String polyline, int precision) { - return LineString.fromLngLats(PolylineUtils.decode(polyline, precision), null); + double[] points = PolylineUtils.decodeToFlattenListOfPoints(polyline, precision); + return LineString.fromFlattenArrayOfPoints(points, null); + } + + LineString(String type, @Nullable BoundingBox bbox, List coordinates) { + this(type, bbox, new FlattenListOfPoints(coordinates)); + } + + LineString(String type, @Nullable BoundingBox bbox, FlattenListOfPoints flattenListOfPoints) { + if (type == null) { + throw new NullPointerException("Null type"); + } + if (flattenListOfPoints == null) { + throw new NullPointerException("Null coordinates"); + } + this.flattenListOfPoints = flattenListOfPoints; + this.type = type; + this.bbox = bbox; } /** @@ -211,14 +236,17 @@ public BoundingBox bbox() { /** * Provides the list of {@link Point}s that make up the LineString geometry. + *

* * @return a list of points * @since 3.0.0 + * @deprecated Please consider using {@link #flattenCoordinates()} instead for better performance. */ @NonNull @Override - public List coordinates() { - return coordinates; + @Deprecated + public List coordinates() { + return flattenListOfPoints.points(); } /** @@ -245,7 +273,7 @@ public String toJson() { * @since 1.0.0 */ public String toPolyline(int precision) { - return PolylineUtils.encode(coordinates(), precision); + return PolylineUtils.encode(flattenListOfPoints.getFlattenLngLatArray(), precision); } /** @@ -264,34 +292,29 @@ public String toString() { return "LineString{" + "type=" + type + ", " + "bbox=" + bbox + ", " - + "coordinates=" + coordinates + + "coordinates=" + flattenListOfPoints + "}"; } @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (obj instanceof LineString) { - LineString that = (LineString) obj; - return (this.type.equals(that.type())) - && ((this.bbox == null) ? (that.bbox() == null) : this.bbox.equals(that.bbox())) - && (this.coordinates.equals(that.coordinates())); + public boolean equals(Object o) { + if (!(o instanceof LineString)) { + return false; } - return false; + LineString that = (LineString) o; + return Objects.equals(type, that.type) + && Objects.equals(bbox, that.bbox) + && Objects.equals(flattenListOfPoints, that.flattenListOfPoints); } @Override public int hashCode() { - int hashCode = 1; - hashCode *= 1000003; - hashCode ^= type.hashCode(); - hashCode *= 1000003; - hashCode ^= (bbox == null) ? 0 : bbox.hashCode(); - hashCode *= 1000003; - hashCode ^= coordinates.hashCode(); - return hashCode; + return Objects.hash(type, bbox, flattenListOfPoints); + } + + @Override + public FlattenListOfPoints flattenCoordinates() { + return flattenListOfPoints; } /** @@ -299,15 +322,16 @@ public int hashCode() { * * @since 4.6.0 */ - static final class GsonTypeAdapter extends BaseGeometryTypeAdapter> { + static final class GsonTypeAdapter extends + BaseGeometryTypeAdapter, FlattenListOfPoints> { GsonTypeAdapter(Gson gson) { - super(gson, new ListOfPointCoordinatesTypeAdapter()); + super(gson, new FlattenListOfPointsTypeAdapter()); } @Override public void write(JsonWriter jsonWriter, LineString object) throws IOException { - writeCoordinateContainer(jsonWriter, object); + writeFlattenedCoordinateContainer(jsonWriter, object); } @Override @@ -316,10 +340,11 @@ public LineString read(JsonReader jsonReader) throws IOException { } @Override - CoordinateContainer> createCoordinateContainer(String type, - BoundingBox bbox, - List coordinates) { - return new LineString(type == null ? "LineString" : type, bbox, coordinates); + CoordinateContainer> createCoordinateContainer( + String type, + BoundingBox bbox, + FlattenListOfPoints flattenListOfPoints) { + return new LineString(type == null ? "LineString" : type, bbox, flattenListOfPoints); } } } diff --git a/services-geojson/src/main/java/com/mapbox/geojson/ListOfDoublesCoordinatesTypeAdapter.java b/services-geojson/src/main/java/com/mapbox/geojson/ListOfDoublesCoordinatesTypeAdapter.java index 4be4e86f7..97b9dacb7 100644 --- a/services-geojson/src/main/java/com/mapbox/geojson/ListOfDoublesCoordinatesTypeAdapter.java +++ b/services-geojson/src/main/java/com/mapbox/geojson/ListOfDoublesCoordinatesTypeAdapter.java @@ -6,23 +6,22 @@ import com.google.gson.stream.JsonWriter; import java.io.IOException; -import java.util.List; /** - * Type Adapter to serialize/deserialize Poinr into/from for double array. + * Type Adapter to serialize/deserialize Point into/from for double array. * * @since 4.6.0 */ @Keep -class ListOfDoublesCoordinatesTypeAdapter extends BaseCoordinatesTypeAdapter> { +class ListOfDoublesCoordinatesTypeAdapter extends BaseCoordinatesTypeAdapter { @Override - public void write(JsonWriter out, List value) throws IOException { + public void write(JsonWriter out, double[] value) throws IOException { writePointList(out, value); } @Override - public List read(JsonReader in) throws IOException { + public double[] read(JsonReader in) throws IOException { return readPointList(in); } } diff --git a/services-geojson/src/main/java/com/mapbox/geojson/ListOfPointCoordinatesTypeAdapter.java b/services-geojson/src/main/java/com/mapbox/geojson/ListOfPointCoordinatesTypeAdapter.java deleted file mode 100644 index 215aec083..000000000 --- a/services-geojson/src/main/java/com/mapbox/geojson/ListOfPointCoordinatesTypeAdapter.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.mapbox.geojson; - -import androidx.annotation.Keep; - -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import com.mapbox.geojson.exception.GeoJsonException; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * Type Adapter to serialize/deserialize List<Point> into/from two dimentional double array. - * - * @since 4.6.0 - */ -@Keep -class ListOfPointCoordinatesTypeAdapter extends BaseCoordinatesTypeAdapter> { - - @Override - public void write(JsonWriter out, List points) throws IOException { - - if (points == null) { - out.nullValue(); - return; - } - - out.beginArray(); - - for (Point point : points) { - writePoint(out, point); - } - - out.endArray(); - } - - @Override - public List read(JsonReader in) throws IOException { - - if (in.peek() == JsonToken.NULL) { - throw new NullPointerException(); - } - - if (in.peek() == JsonToken.BEGIN_ARRAY) { - List points = new ArrayList<>(); - in.beginArray(); - - while (in.peek() == JsonToken.BEGIN_ARRAY) { - points.add(readPoint(in)); - } - in.endArray(); - - return points; - } - - throw new GeoJsonException("coordinates should be non-null array of array of double"); - } -} diff --git a/services-geojson/src/main/java/com/mapbox/geojson/MultiLineString.java b/services-geojson/src/main/java/com/mapbox/geojson/MultiLineString.java index 659b5ffa0..92096eb17 100644 --- a/services-geojson/src/main/java/com/mapbox/geojson/MultiLineString.java +++ b/services-geojson/src/main/java/com/mapbox/geojson/MultiLineString.java @@ -326,7 +326,7 @@ public int hashCode() { * @since 4.6.0 */ static final class GsonTypeAdapter - extends BaseGeometryTypeAdapter>> { + extends BaseGeometryTypeAdapter>, List>> { GsonTypeAdapter(Gson gson) { super(gson, new ListOfListOfPointCoordinatesTypeAdapter()); diff --git a/services-geojson/src/main/java/com/mapbox/geojson/MultiPoint.java b/services-geojson/src/main/java/com/mapbox/geojson/MultiPoint.java index 4e3757890..8eb69a104 100644 --- a/services-geojson/src/main/java/com/mapbox/geojson/MultiPoint.java +++ b/services-geojson/src/main/java/com/mapbox/geojson/MultiPoint.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * A MultiPoint represents two or more geographic points that share a relationship and is one of the @@ -35,7 +36,8 @@ * @since 1.0.0 */ @Keep -public final class MultiPoint implements CoordinateContainer> { +public final class MultiPoint implements + FlattenedCoordinateContainer, FlattenListOfPoints> { private static final String TYPE = "MultiPoint"; @@ -43,7 +45,8 @@ public final class MultiPoint implements CoordinateContainer> { private final BoundingBox bbox; - private final List coordinates; + @NonNull + private final FlattenListOfPoints flattenListOfPoints; /** * Create a new instance of this class by passing in a formatted valid JSON String. If you are @@ -103,15 +106,19 @@ static MultiPoint fromLngLats(@NonNull double[][] coordinates) { } MultiPoint(String type, @Nullable BoundingBox bbox, List coordinates) { + this(type, bbox, new FlattenListOfPoints(coordinates)); + } + + MultiPoint(String type, @Nullable BoundingBox bbox, FlattenListOfPoints flattenListOfPoints) { if (type == null) { throw new NullPointerException("Null type"); } this.type = type; this.bbox = bbox; - if (coordinates == null) { + if (flattenListOfPoints == null) { throw new NullPointerException("Null coordinates"); } - this.coordinates = coordinates; + this.flattenListOfPoints = flattenListOfPoints; } /** @@ -153,7 +160,7 @@ public BoundingBox bbox() { @NonNull @Override public List coordinates() { - return coordinates; + return flattenListOfPoints.points(); } /** @@ -186,34 +193,29 @@ public String toString() { return "MultiPoint{" + "type=" + type + ", " + "bbox=" + bbox + ", " - + "coordinates=" + coordinates + + "coordinates=" + flattenListOfPoints + "}"; } @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (obj instanceof MultiPoint) { - MultiPoint that = (MultiPoint) obj; - return (this.type.equals(that.type())) - && ((this.bbox == null) ? (that.bbox() == null) : this.bbox.equals(that.bbox())) - && (this.coordinates.equals(that.coordinates())); + public boolean equals(Object o) { + if (!(o instanceof MultiPoint)) { + return false; } - return false; + MultiPoint that = (MultiPoint) o; + return Objects.equals(type, that.type) + && Objects.equals(bbox, that.bbox) + && Objects.equals(flattenListOfPoints, that.flattenListOfPoints); } @Override public int hashCode() { - int hashCode = 1; - hashCode *= 1000003; - hashCode ^= type.hashCode(); - hashCode *= 1000003; - hashCode ^= (bbox == null) ? 0 : bbox.hashCode(); - hashCode *= 1000003; - hashCode ^= coordinates.hashCode(); - return hashCode; + return Objects.hash(type, bbox, flattenListOfPoints); + } + + @Override + public FlattenListOfPoints flattenCoordinates() { + return flattenListOfPoints; } /** @@ -221,15 +223,16 @@ public int hashCode() { * * @since 4.6.0 */ - static final class GsonTypeAdapter extends BaseGeometryTypeAdapter> { + static final class GsonTypeAdapter extends + BaseGeometryTypeAdapter, FlattenListOfPoints> { GsonTypeAdapter(Gson gson) { - super(gson, new ListOfPointCoordinatesTypeAdapter()); + super(gson, new FlattenListOfPointsTypeAdapter()); } @Override public void write(JsonWriter jsonWriter, MultiPoint object) throws IOException { - writeCoordinateContainer(jsonWriter, object); + writeFlattenedCoordinateContainer(jsonWriter, object); } @Override @@ -238,10 +241,11 @@ public MultiPoint read(JsonReader jsonReader) throws IOException { } @Override - CoordinateContainer> createCoordinateContainer(String type, - BoundingBox bbox, - List coordinates) { - return new MultiPoint(type == null ? "MultiPoint" : type, bbox, coordinates); + CoordinateContainer> createCoordinateContainer( + String type, + BoundingBox bbox, + FlattenListOfPoints flattenListOfPoints) { + return new MultiPoint(type == null ? "MultiPoint" : type, bbox, flattenListOfPoints); } } } diff --git a/services-geojson/src/main/java/com/mapbox/geojson/MultiPolygon.java b/services-geojson/src/main/java/com/mapbox/geojson/MultiPolygon.java index 325479d9c..0688a7be1 100644 --- a/services-geojson/src/main/java/com/mapbox/geojson/MultiPolygon.java +++ b/services-geojson/src/main/java/com/mapbox/geojson/MultiPolygon.java @@ -344,8 +344,8 @@ public int hashCode() { * * @since 4.6.0 */ - static final class GsonTypeAdapter - extends BaseGeometryTypeAdapter>>> { + static final class GsonTypeAdapter extends + BaseGeometryTypeAdapter>>, List>>> { GsonTypeAdapter(Gson gson) { super(gson, new ListofListofListOfPointCoordinatesTypeAdapter()); diff --git a/services-geojson/src/main/java/com/mapbox/geojson/Point.java b/services-geojson/src/main/java/com/mapbox/geojson/Point.java index e75b1b0eb..60e463898 100644 --- a/services-geojson/src/main/java/com/mapbox/geojson/Point.java +++ b/services-geojson/src/main/java/com/mapbox/geojson/Point.java @@ -12,7 +12,10 @@ import com.mapbox.geojson.shifter.CoordinateShifterManager; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Objects; /** * A point represents a single geographic position and is one of the seven Geometries found in the @@ -46,9 +49,9 @@ * @since 1.0.0 */ @Keep -public final class Point implements CoordinateContainer> { +public final class Point implements FlattenedCoordinateContainer, double[]> { - private static final String TYPE = "Point"; + static final String TYPE = "Point"; @NonNull private final String type; @@ -57,7 +60,7 @@ public final class Point implements CoordinateContainer> { private final BoundingBox bbox; @NonNull - private final List coordinates; + private final double[] coordinates; /** * Create a new instance of this class by passing in a formatted valid JSON String. If you are @@ -91,8 +94,8 @@ public static Point fromJson(@NonNull String json) { */ public static Point fromLngLat(double longitude, double latitude) { - List coordinates = - CoordinateShifterManager.getCoordinateShifter().shiftLonLat(longitude, latitude); + double[] coordinates = + CoordinateShifterManager.getCoordinateShifter().shift(longitude, latitude); return new Point(TYPE, null, coordinates); } @@ -113,8 +116,8 @@ public static Point fromLngLat(double longitude, double latitude) { public static Point fromLngLat(double longitude, double latitude, @Nullable BoundingBox bbox) { - List coordinates = - CoordinateShifterManager.getCoordinateShifter().shiftLonLat(longitude, latitude); + double[] coordinates = + CoordinateShifterManager.getCoordinateShifter().shift(longitude, latitude); return new Point(TYPE, bbox, coordinates); } @@ -135,8 +138,8 @@ public static Point fromLngLat(double longitude, double latitude, */ public static Point fromLngLat(double longitude, double latitude, double altitude) { - List coordinates = - CoordinateShifterManager.getCoordinateShifter().shiftLonLatAlt(longitude, latitude, altitude); + double[] coordinates = + CoordinateShifterManager.getCoordinateShifter().shift(longitude, latitude, altitude); return new Point(TYPE, null, coordinates); } @@ -160,28 +163,24 @@ public static Point fromLngLat(double longitude, double latitude, double altitud public static Point fromLngLat(double longitude, double latitude, double altitude, @Nullable BoundingBox bbox) { - List coordinates = - CoordinateShifterManager.getCoordinateShifter().shiftLonLatAlt(longitude, latitude, altitude); + double[] coordinates = + CoordinateShifterManager.getCoordinateShifter().shift(longitude, latitude, altitude); return new Point(TYPE, bbox, coordinates); } static Point fromLngLat(@NonNull double[] coords) { if (coords.length == 2) { return Point.fromLngLat(coords[0], coords[1]); - } else if (coords.length > 2) { return Point.fromLngLat(coords[0], coords[1], coords[2]); } return null; } - Point(String type, @Nullable BoundingBox bbox, List coordinates) { - if (type == null) { - throw new NullPointerException("Null type"); - } + Point(@NonNull String type, @Nullable BoundingBox bbox, @NonNull double[] coordinates) { this.type = type; this.bbox = bbox; - if (coordinates == null || coordinates.size() == 0) { + if (coordinates.length == 0) { throw new NullPointerException("Null coordinates"); } this.coordinates = coordinates; @@ -197,7 +196,7 @@ static Point fromLngLat(@NonNull double[] coords) { * @since 3.0.0 */ public double longitude() { - return coordinates().get(0); + return coordinates[0]; } /** @@ -210,7 +209,7 @@ public double longitude() { * @since 3.0.0 */ public double latitude() { - return coordinates().get(1); + return coordinates[1]; } /** @@ -223,10 +222,10 @@ public double latitude() { * @since 3.0.0 */ public double altitude() { - if (coordinates().size() < 3) { + if (coordinates.length < 3) { return Double.NaN; } - return coordinates().get(2); + return coordinates[2]; } /** @@ -272,16 +271,35 @@ public BoundingBox bbox() { } /** - * Provide a single double array containing the longitude, latitude, and optionally an + * Provide a list of Doubles containing the longitude, latitude, and optionally an * altitude/elevation. {@link #longitude()}, {@link #latitude()}, and {@link #altitude()} are all - * avaliable which make getting specific coordinates more direct. + * available which make getting specific coordinates more direct. * * @return a double array which holds this points coordinates * @since 3.0.0 + * @deprecated Please use {@link #flattenCoordinates()} instead. */ @NonNull @Override + @Deprecated public List coordinates() { + ArrayList list = new ArrayList<>(coordinates.length); + for (double coordinate : coordinates) { + list.add(coordinate); + } + return list; + } + + /** + * Provide a single double array containing the longitude, latitude, and optionally an + * altitude. {@link #longitude()}, {@link #latitude()}, and {@link #altitude()} are all + * available which make getting specific coordinates more direct. + * + * @return a double array which holds this points coordinates + * @since 3.0.0 + */ + @Override + public double[] flattenCoordinates() { return coordinates; } @@ -312,25 +330,32 @@ public static TypeAdapter typeAdapter(Gson gson) { @Override public String toString() { + String coordinatesStr; + if (coordinates.length > 2) { + coordinatesStr = "[" + + this.coordinates[0] + ", " + + this.coordinates[1] + ", " + + this.coordinates[2] + + "]"; + } else { + coordinatesStr = "[" + this.coordinates[0] + ", " + this.coordinates[1] + "]"; + } return "Point{" + "type=" + type + ", " + "bbox=" + bbox + ", " - + "coordinates=" + coordinates + + "coordinates=" + coordinatesStr + "}"; } @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (obj instanceof Point) { - Point that = (Point) obj; - return (this.type.equals(that.type())) - && ((this.bbox == null) ? (that.bbox() == null) : this.bbox.equals(that.bbox())) - && (this.coordinates.equals(that.coordinates())); + public boolean equals(Object o) { + if (!(o instanceof Point)) { + return false; } - return false; + Point point = (Point) o; + return Objects.equals(type, point.type) + && Objects.equals(bbox, point.bbox) + && Objects.deepEquals(coordinates, point.coordinates); } @Override @@ -341,7 +366,7 @@ public int hashCode() { hashCode *= 1000003; hashCode ^= (bbox == null) ? 0 : bbox.hashCode(); hashCode *= 1000003; - hashCode ^= coordinates.hashCode(); + hashCode ^= Arrays.hashCode(coordinates); return hashCode; } @@ -350,7 +375,8 @@ public int hashCode() { * * @since 4.6.0 */ - static final class GsonTypeAdapter extends BaseGeometryTypeAdapter> { + static final class GsonTypeAdapter extends + BaseGeometryTypeAdapter, double[]> { GsonTypeAdapter(Gson gson) { super(gson, new ListOfDoublesCoordinatesTypeAdapter()); @@ -359,7 +385,7 @@ static final class GsonTypeAdapter extends BaseGeometryTypeAdapter> createCoordinateContainer(String type, BoundingBox bbox, - List coordinates) { + double[] coordinates) { return new Point(type == null ? "Point" : type, bbox, coordinates); } } diff --git a/services-geojson/src/main/java/com/mapbox/geojson/Polygon.java b/services-geojson/src/main/java/com/mapbox/geojson/Polygon.java index c40d5791c..e8970c064 100644 --- a/services-geojson/src/main/java/com/mapbox/geojson/Polygon.java +++ b/services-geojson/src/main/java/com/mapbox/geojson/Polygon.java @@ -432,7 +432,8 @@ public int hashCode() { * * @since 4.6.0 */ - static final class GsonTypeAdapter extends BaseGeometryTypeAdapter>> { + static final class GsonTypeAdapter extends + BaseGeometryTypeAdapter>, List>> { GsonTypeAdapter(Gson gson) { super(gson, new ListOfListOfPointCoordinatesTypeAdapter()); diff --git a/services-geojson/src/main/java/com/mapbox/geojson/shifter/CoordinateShifter.java b/services-geojson/src/main/java/com/mapbox/geojson/shifter/CoordinateShifter.java index a7cab8894..fc3571d84 100644 --- a/services-geojson/src/main/java/com/mapbox/geojson/shifter/CoordinateShifter.java +++ b/services-geojson/src/main/java/com/mapbox/geojson/shifter/CoordinateShifter.java @@ -54,4 +54,31 @@ public interface CoordinateShifter { * @since 4.2.0 */ List unshiftPoint(List shiftedCoordinates); + + /** + * Shifted coordinate values according to its algorithm. + * + * @param lon unshifted longitude + * @param lat unshifted latitude + * @param altitude unshifted altitude + * @return shifted longitude, shifted latitude, shifted altitude + */ + double[] shift(double lon, double lat, double altitude); + + /** + * Shifted coordinate values according to its algorithm. + * + * @param lon unshifted longitude + * @param lat unshifted latitude + * @return shifted longitude, shifted latitude + */ + double[] shift(double lon, double lat); + + /** + * Unshifted coordinate values according to its algorithm. + * + * @param shiftedCoordinates shifted point + * @return unshifted longitude, shifted latitude, and altitude (if present) + */ + double[] unshiftPointArray(double[] shiftedCoordinates); } diff --git a/services-geojson/src/main/java/com/mapbox/geojson/shifter/CoordinateShifterManager.java b/services-geojson/src/main/java/com/mapbox/geojson/shifter/CoordinateShifterManager.java index ef9ecb186..b5cc12d44 100644 --- a/services-geojson/src/main/java/com/mapbox/geojson/shifter/CoordinateShifterManager.java +++ b/services-geojson/src/main/java/com/mapbox/geojson/shifter/CoordinateShifterManager.java @@ -34,6 +34,25 @@ public List unshiftPoint(Point point) { public List unshiftPoint(List coordinates) { return coordinates; } + + @Override + public double[] shift(double lon, double lat) { + return new double[]{lon, lat}; + } + + @Override + public double[] shift(double lon, double lat, double altitude) { + if (Double.isNaN(altitude)) { + return shift(lon, lat); + } else { + return new double[]{lon, lat, altitude}; + } + } + + @Override + public double[] unshiftPointArray(double[] shiftedCoordinates) { + return shiftedCoordinates; + } }; private static volatile CoordinateShifter coordinateShifter = DEFAULT; diff --git a/services-geojson/src/main/java/com/mapbox/geojson/utils/PolylineUtils.java b/services-geojson/src/main/java/com/mapbox/geojson/utils/PolylineUtils.java index 90a9b3b3a..04e6c4f19 100644 --- a/services-geojson/src/main/java/com/mapbox/geojson/utils/PolylineUtils.java +++ b/services-geojson/src/main/java/com/mapbox/geojson/utils/PolylineUtils.java @@ -1,6 +1,8 @@ package com.mapbox.geojson.utils; import androidx.annotation.NonNull; + +import com.mapbox.geojson.FlattenListOfPoints; import com.mapbox.geojson.Point; import java.util.ArrayList; @@ -79,6 +81,71 @@ public static List decode(@NonNull final String encodedPath, int precisio return path.subList(0, itemsCount); } + /** + * Decodes an encoded path string into a {@link FlattenListOfPoints}. + * + * @param encodedPath a String representing an encoded path string + * @param precision OSRMv4 uses 6, OSRMv5 and Google uses 5 + * @return an array of doubles representing a line geometry with flattened points + * in the form: [lng1, lat1, lng2, lat2, ...] + * @see Part of algorithm came from this source + * @see Part of algorithm came from this source. + */ + @NonNull + public static double[] decodeToFlattenListOfPoints( + @NonNull + final String encodedPath, + int precision + ) { + int len = encodedPath.length(); + + // OSRM uses precision=6, the default Polyline spec divides by 1E5, capping at precision=5 + double factor = Math.pow(10, precision); + + // For speed we preallocate to an upper bound on the final length, then + // truncate the array before returning. + double[] flattenLngLatCoordinates = new double[len]; + int index = 0; + int lat = 0; + int lng = 0; + int itemsCount = 0; + + while (index < len) { + int result = 1; + int shift = 0; + int temp; + do { + temp = encodedPath.charAt(index++) - 63 - 1; + result += temp << shift; + shift += 5; + } + while (temp >= 0x1f); + lat += (result & 1) != 0 ? ~(result >> 1) : (result >> 1); + + result = 1; + shift = 0; + do { + temp = encodedPath.charAt(index++) - 63 - 1; + result += temp << shift; + shift += 5; + } + while (temp >= 0x1f); + lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1); + + flattenLngLatCoordinates[itemsCount * 2] = lng / factor; + flattenLngLatCoordinates[itemsCount * 2 + 1] = lat / factor; + + itemsCount++; + } + + double[] trimmedFlattenLngLatCoordinates = new double[itemsCount * 2]; + System.arraycopy(flattenLngLatCoordinates, 0, + trimmedFlattenLngLatCoordinates, 0, + itemsCount * 2 + ); + return trimmedFlattenLngLatCoordinates; + } + /** * Encodes a sequence of Points into an encoded path string. * @@ -113,6 +180,47 @@ public static String encode(@NonNull final List path, int precision) { return result.toString(); } + /** + * Encodes an array of doubles representing a line geometry with flattened points into an encoded + * path string. + * + * @param flattenLngLatArray an array of doubles representing a line geometry with flattened + * points in the form: [lng1, lat1, lng2, lat2, ...] + * @param precision OSRMv4 uses 6, OSRMv5 and Google uses 5 + * @return a String representing a path string + */ + @NonNull + public static String encode( + @NonNull + double[] flattenLngLatArray, + int precision + ) { + long lastLat = 0; + long lastLng = 0; + + final StringBuilder result = new StringBuilder(); + + // OSRM uses precision=6, the default Polyline spec divides by 1E5, capping at precision=5 + double factor = Math.pow(10, precision); + + for (int i = 0; i < flattenLngLatArray.length / 2; i++) { + double longitude = flattenLngLatArray[i * 2]; + double latitude = flattenLngLatArray[i * 2 + 1]; + long lat = Math.round(latitude * factor); + long lng = Math.round(longitude * factor); + + long varLat = lat - lastLat; + long varLng = lng - lastLng; + + encode(varLat, result); + encode(varLng, result); + + lastLat = lat; + lastLng = lng; + } + return result.toString(); + } + private static void encode(long variable, StringBuilder result) { variable = variable < 0 ? ~(variable << 1) : variable << 1; while (variable >= 0x20) { diff --git a/services-geojson/src/test/java/com/mapbox/geojson/FlattenListOfPointsTest.java b/services-geojson/src/test/java/com/mapbox/geojson/FlattenListOfPointsTest.java new file mode 100644 index 000000000..0ff05da7f --- /dev/null +++ b/services-geojson/src/test/java/com/mapbox/geojson/FlattenListOfPointsTest.java @@ -0,0 +1,456 @@ +package com.mapbox.geojson; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class FlattenListOfPointsTest extends TestUtils { + + @Test + public void constructor_withArrays_storesDataCorrectly() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0}; + double[] altitudes = new double[]{5.0, 6.0}; + + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, altitudes); + + assertNotNull(flatten); + assertArrayEquals(lngLatArray, flatten.getFlattenLngLatArray(), DELTA); + assertArrayEquals(altitudes, flatten.getAltitudes(), DELTA); + } + + @Test + public void constructor_withArrays_nullAltitudes() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0}; + + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, null); + + assertNotNull(flatten); + assertArrayEquals(lngLatArray, flatten.getFlattenLngLatArray(), DELTA); + assertNull(flatten.getAltitudes()); + } + + @Test + public void size_emptyFlatten_returnsZero() { + double[] lngLatArray = new double[]{}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, null); + + assertEquals(0, flatten.size()); + } + + @Test + public void size_singlePoint_returnsOne() { + double[] lngLatArray = new double[]{1.0, 2.0}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, null); + + assertEquals(1, flatten.size()); + } + + @Test + public void size_multiplePoints_returnsCorrectCount() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, null); + + assertEquals(3, flatten.size()); + } + + @Test + public void size_fromListOfPoints_returnsCorrectCount() { + List points = new ArrayList<>(); + points.add(Point.fromLngLat(1.0, 2.0)); + points.add(Point.fromLngLat(3.0, 4.0)); + points.add(Point.fromLngLat(5.0, 6.0)); + points.add(Point.fromLngLat(7.0, 8.0)); + + FlattenListOfPoints flatten = new FlattenListOfPoints(points); + + assertEquals(4, flatten.size()); + } + + @Test + public void constructor_withEmptyList_createsEmptyFlatten() { + List points = new ArrayList<>(); + + FlattenListOfPoints flatten = new FlattenListOfPoints(points); + + assertNotNull(flatten); + assertEquals(0, flatten.getFlattenLngLatArray().length); + assertNull(flatten.getAltitudes()); + } + + @Test + public void constructor_withPointsNoAltitude_storesLngLatOnly() { + List points = new ArrayList<>(); + points.add(Point.fromLngLat(1.0, 2.0)); + points.add(Point.fromLngLat(3.0, 4.0)); + points.add(Point.fromLngLat(5.0, 6.0)); + + FlattenListOfPoints flatten = new FlattenListOfPoints(points); + + assertNotNull(flatten); + double[] expected = new double[]{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + assertArrayEquals(expected, flatten.getFlattenLngLatArray(), DELTA); + assertNull(flatten.getAltitudes()); + } + + @Test + public void constructor_withPointsWithAltitude_storesAllData() { + List points = new ArrayList<>(); + points.add(Point.fromLngLat(1.0, 2.0, 10.0)); + points.add(Point.fromLngLat(3.0, 4.0, 20.0)); + points.add(Point.fromLngLat(5.0, 6.0, 30.0)); + + FlattenListOfPoints flatten = new FlattenListOfPoints(points); + + assertNotNull(flatten); + double[] expectedLngLat = new double[]{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + double[] expectedAlt = new double[]{10.0, 20.0, 30.0}; + assertArrayEquals(expectedLngLat, flatten.getFlattenLngLatArray(), DELTA); + assertArrayEquals(expectedAlt, flatten.getAltitudes(), DELTA); + } + + @Test + public void constructor_withMixedAltitudes_storesNaNForMissingAltitudes() { + List points = new ArrayList<>(); + points.add(Point.fromLngLat(1.0, 2.0)); // no altitude + points.add(Point.fromLngLat(3.0, 4.0, 20.0)); // with altitude + points.add(Point.fromLngLat(5.0, 6.0)); // no altitude + points.add(Point.fromLngLat(7.0, 8.0, 40.0)); // with altitude + + FlattenListOfPoints flatten = new FlattenListOfPoints(points); + + assertNotNull(flatten); + double[] expectedLngLat = new double[]{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}; + assertArrayEquals(expectedLngLat, flatten.getFlattenLngLatArray(), DELTA); + + double[] altitudes = flatten.getAltitudes(); + assertNotNull(altitudes); + assertEquals(4, altitudes.length); + assertTrue(Double.isNaN(altitudes[0])); + assertEquals(20.0, altitudes[1], DELTA); + assertTrue(Double.isNaN(altitudes[2])); + assertEquals(40.0, altitudes[3], DELTA); + } + + @Test + public void constructor_withSinglePoint_worksCorrectly() { + List points = new ArrayList<>(); + points.add(Point.fromLngLat(1.0, 2.0, 10.0)); + + FlattenListOfPoints flatten = new FlattenListOfPoints(points); + + assertNotNull(flatten); + double[] expectedLngLat = new double[]{1.0, 2.0}; + double[] expectedAlt = new double[]{10.0}; + assertArrayEquals(expectedLngLat, flatten.getFlattenLngLatArray(), DELTA); + assertArrayEquals(expectedAlt, flatten.getAltitudes(), DELTA); + } + + @Test + public void points_returnsEmptyListForEmptyFlatten() { + double[] lngLatArray = new double[]{}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, null); + + List points = flatten.points(); + + assertNotNull(points); + assertEquals(0, points.size()); + } + + @Test + public void points_reconstructsPointsWithoutAltitude() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, null); + + List points = flatten.points(); + + assertNotNull(points); + assertEquals(2, points.size()); + assertEquals(1.0, points.get(0).longitude(), DELTA); + assertEquals(2.0, points.get(0).latitude(), DELTA); + assertFalse(points.get(0).hasAltitude()); + assertEquals(3.0, points.get(1).longitude(), DELTA); + assertEquals(4.0, points.get(1).latitude(), DELTA); + assertFalse(points.get(1).hasAltitude()); + } + + @Test + public void points_reconstructsPointsWithAltitude() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0}; + double[] altitudes = new double[]{10.0, 20.0}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, altitudes); + + List points = flatten.points(); + + assertNotNull(points); + assertEquals(2, points.size()); + assertEquals(1.0, points.get(0).longitude(), DELTA); + assertEquals(2.0, points.get(0).latitude(), DELTA); + assertEquals(10.0, points.get(0).altitude(), DELTA); + assertTrue(points.get(0).hasAltitude()); + assertEquals(3.0, points.get(1).longitude(), DELTA); + assertEquals(4.0, points.get(1).latitude(), DELTA); + assertEquals(20.0, points.get(1).altitude(), DELTA); + assertTrue(points.get(1).hasAltitude()); + } + + @Test + public void points_reconstructsPointsWithMixedAltitudes() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + double[] altitudes = new double[]{Double.NaN, 20.0, 30.0}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, altitudes); + + List points = flatten.points(); + + assertNotNull(points); + assertEquals(3, points.size()); + assertFalse(points.get(0).hasAltitude()); + assertTrue(points.get(1).hasAltitude()); + assertEquals(20.0, points.get(1).altitude(), DELTA); + assertTrue(points.get(2).hasAltitude()); + assertEquals(30.0, points.get(2).altitude(), DELTA); + } + + @Test + public void points_roundTrip_preservesData() { + List originalPoints = new ArrayList<>(); + originalPoints.add(Point.fromLngLat(1.0, 2.0, 10.0)); + originalPoints.add(Point.fromLngLat(3.0, 4.0)); + originalPoints.add(Point.fromLngLat(5.0, 6.0, 30.0)); + + FlattenListOfPoints flatten = new FlattenListOfPoints(originalPoints); + List reconstructedPoints = flatten.points(); + + assertEquals(originalPoints.size(), reconstructedPoints.size()); + for (int i = 0; i < originalPoints.size(); i++) { + Point original = originalPoints.get(i); + Point reconstructed = reconstructedPoints.get(i); + assertEquals(original.longitude(), reconstructed.longitude(), DELTA); + assertEquals(original.latitude(), reconstructed.latitude(), DELTA); + assertEquals(original.hasAltitude(), reconstructed.hasAltitude()); + if (original.hasAltitude()) { + assertEquals(original.altitude(), reconstructed.altitude(), DELTA); + } + } + } + + @Test + public void equals_sameFlatten_returnsTrue() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0}; + double[] altitudes = new double[]{10.0, 20.0}; + FlattenListOfPoints flatten1 = new FlattenListOfPoints(lngLatArray, altitudes); + FlattenListOfPoints flatten2 = new FlattenListOfPoints(lngLatArray, altitudes); + + assertTrue(flatten1.equals(flatten2)); + assertTrue(flatten2.equals(flatten1)); + } + + @Test + public void equals_differentLngLat_returnsFalse() { + double[] lngLatArray1 = new double[]{1.0, 2.0, 3.0, 4.0}; + double[] lngLatArray2 = new double[]{1.0, 2.0, 5.0, 6.0}; + double[] altitudes = new double[]{10.0, 20.0}; + FlattenListOfPoints flatten1 = new FlattenListOfPoints(lngLatArray1, altitudes); + FlattenListOfPoints flatten2 = new FlattenListOfPoints(lngLatArray2, altitudes); + + assertFalse(flatten1.equals(flatten2)); + } + + @Test + public void equals_differentAltitudes_returnsFalse() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0}; + double[] altitudes1 = new double[]{10.0, 20.0}; + double[] altitudes2 = new double[]{10.0, 30.0}; + FlattenListOfPoints flatten1 = new FlattenListOfPoints(lngLatArray, altitudes1); + FlattenListOfPoints flatten2 = new FlattenListOfPoints(lngLatArray, altitudes2); + + assertFalse(flatten1.equals(flatten2)); + } + + @Test + public void equals_oneWithAltitudeOneWithout_returnsFalse() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0}; + double[] altitudes = new double[]{10.0, 20.0}; + FlattenListOfPoints flatten1 = new FlattenListOfPoints(lngLatArray, altitudes); + FlattenListOfPoints flatten2 = new FlattenListOfPoints(lngLatArray, null); + + assertFalse(flatten1.equals(flatten2)); + } + + @Test + public void equals_withNull_returnsFalse() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, null); + + assertFalse(flatten.equals(null)); + } + + @Test + public void equals_withDifferentType_returnsFalse() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, null); + + assertFalse(flatten.equals("not a FlattenListOfPoints")); + } + + @Test + public void hashCode_sameData_returnsSameHashCode() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0}; + double[] altitudes = new double[]{10.0, 20.0}; + FlattenListOfPoints flatten1 = new FlattenListOfPoints(lngLatArray, altitudes); + FlattenListOfPoints flatten2 = new FlattenListOfPoints(lngLatArray, altitudes); + + assertEquals(flatten1.hashCode(), flatten2.hashCode()); + } + + @Test + public void hashCode_differentData_returnsDifferentHashCode() { + double[] lngLatArray1 = new double[]{1.0, 2.0, 3.0, 4.0}; + double[] lngLatArray2 = new double[]{1.0, 2.0, 5.0, 6.0}; + double[] altitudes = new double[]{10.0, 20.0}; + FlattenListOfPoints flatten1 = new FlattenListOfPoints(lngLatArray1, altitudes); + FlattenListOfPoints flatten2 = new FlattenListOfPoints(lngLatArray2, altitudes); + + assertNotEquals(flatten1.hashCode(), flatten2.hashCode()); + } + + @Test + public void toString_emptyFlatten_returnsEmptyBrackets() { + double[] lngLatArray = new double[]{}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, null); + + String result = flatten.toString(); + + assertEquals("[]", result); + } + + @Test + public void toString_singlePointWithoutAltitude_returnsCorrectFormat() { + double[] lngLatArray = new double[]{1.0, 2.0}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, null); + + String result = flatten.toString(); + + assertEquals("[Point{type=Point, bbox=null, coordinates=[1.0, 2.0]}]", result); + } + + @Test + public void toString_singlePointWithAltitude_returnsCorrectFormat() { + double[] lngLatArray = new double[]{1.0, 2.0}; + double[] altitudes = new double[]{10.0}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, altitudes); + + String result = flatten.toString(); + + assertEquals("[Point{type=Point, bbox=null, coordinates=[1.0, 2.0, 10.0]}]", result); + } + + @Test + public void toString_multiplePointsWithoutAltitude_returnsCorrectFormat() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, null); + + String result = flatten.toString(); + + String expected = "[Point{type=Point, bbox=null, coordinates=[1.0, 2.0]}, " + + "Point{type=Point, bbox=null, coordinates=[3.0, 4.0]}]"; + assertEquals(expected, result); + } + + @Test + public void toString_multiplePointsWithAltitude_returnsCorrectFormat() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0}; + double[] altitudes = new double[]{10.0, 20.0}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, altitudes); + + String result = flatten.toString(); + + String expected = "[Point{type=Point, bbox=null, coordinates=[1.0, 2.0, 10.0]}, " + + "Point{type=Point, bbox=null, coordinates=[3.0, 4.0, 20.0]}]"; + assertEquals(expected, result); + } + + @Test + public void toString_mixedAltitudes_returnsCorrectFormat() { + double[] lngLatArray = new double[]{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + double[] altitudes = new double[]{10.0, Double.NaN, 30.0}; + FlattenListOfPoints flatten = new FlattenListOfPoints(lngLatArray, altitudes); + + String result = flatten.toString(); + + String expected = "[Point{type=Point, bbox=null, coordinates=[1.0, 2.0, 10.0]}, " + + "Point{type=Point, bbox=null, coordinates=[3.0, 4.0]}, " + + "Point{type=Point, bbox=null, coordinates=[5.0, 6.0, 30.0]}]"; + assertEquals(expected, result); + } + + @Test + public void testSerializable() throws IOException, ClassNotFoundException { + List points = new ArrayList<>(); + points.add(Point.fromLngLat(1.0, 2.0, 10.0)); + points.add(Point.fromLngLat(3.0, 4.0)); + points.add(Point.fromLngLat(5.0, 6.0, 30.0)); + + FlattenListOfPoints flatten = new FlattenListOfPoints(points); + + byte[] bytes = serialize(flatten); + FlattenListOfPoints deserialized = deserialize(bytes, FlattenListOfPoints.class); + + assertEquals(flatten, deserialized); + } + + @Test + public void constructor_withPointsWithBoundingBox_storesBoundingBoxes() { + List points = new ArrayList<>(); + BoundingBox bbox1 = BoundingBox.fromLngLats(0.0, 0.0, 1.0, 1.0); + BoundingBox bbox2 = BoundingBox.fromLngLats(2.0, 2.0, 3.0, 3.0); + points.add(Point.fromLngLat(1.0, 2.0, bbox1)); + points.add(Point.fromLngLat(3.0, 4.0, bbox2)); + + FlattenListOfPoints flatten = new FlattenListOfPoints(points); + List reconstructedPoints = flatten.points(); + + assertNotNull(reconstructedPoints.get(0).bbox()); + assertNotNull(reconstructedPoints.get(1).bbox()); + assertEquals(bbox1, reconstructedPoints.get(0).bbox()); + assertEquals(bbox2, reconstructedPoints.get(1).bbox()); + } + + @Test + public void constructor_withMixedBoundingBoxes_handlesCorrectly() { + List points = new ArrayList<>(); + BoundingBox bbox = BoundingBox.fromLngLats(0.0, 0.0, 1.0, 1.0); + points.add(Point.fromLngLat(1.0, 2.0, bbox)); + points.add(Point.fromLngLat(3.0, 4.0)); // no bbox + + FlattenListOfPoints flatten = new FlattenListOfPoints(points); + List reconstructedPoints = flatten.points(); + + assertNotNull(reconstructedPoints.get(0).bbox()); + assertNull(reconstructedPoints.get(1).bbox()); + assertEquals(bbox, reconstructedPoints.get(0).bbox()); + } + + @Test + public void toString_withBoundingBox_includesBoundingBoxInOutput() { + List points = new ArrayList<>(); + BoundingBox bbox = BoundingBox.fromLngLats(0.0, 0.0, 1.0, 1.0); + points.add(Point.fromLngLat(1.0, 2.0, bbox)); + + FlattenListOfPoints flatten = new FlattenListOfPoints(points); + String result = flatten.toString(); + + assertTrue(result.contains("BoundingBox")); + assertTrue(result.contains("1.0")); + assertTrue(result.contains("2.0")); + } +} diff --git a/services-geojson/src/test/java/com/mapbox/geojson/LineStringTest.java b/services-geojson/src/test/java/com/mapbox/geojson/LineStringTest.java index 0e6a8d3fc..fd4960122 100644 --- a/services-geojson/src/test/java/com/mapbox/geojson/LineStringTest.java +++ b/services-geojson/src/test/java/com/mapbox/geojson/LineStringTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import org.junit.Rule; import org.junit.Test; @@ -43,6 +44,13 @@ public void fromLngLats_generatedFromMultipoint() throws Exception { assertEquals("_gayB_c`|@_wemJ_kbvD", lineString.toPolyline(PRECISION_6)); } + @Test + public void fromFlattenListOfPoints() throws Exception { + double[] flattenLngLatPoints= new double[]{1.0, 2.0, 4.0, 8.0}; + LineString lineString = LineString.fromFlattenArrayOfPoints(flattenLngLatPoints, null); + assertEquals("_gayB_c`|@_wemJ_kbvD", lineString.toPolyline(PRECISION_6)); + } + @Test public void bbox_nullWhenNotSet() throws Exception { List points = new ArrayList<>(); @@ -104,42 +112,197 @@ public void bbox_doesDeserializeWhenPresent() throws Exception { assertEquals(2.0, lineString.bbox().southwest().latitude(), DELTA); assertEquals(3.0, lineString.bbox().northeast().longitude(), DELTA); assertEquals(4.0, lineString.bbox().northeast().latitude(), DELTA); - assertNotNull(lineString.coordinates()); - assertEquals(1, lineString.coordinates().get(0).longitude(), DELTA); - assertEquals(2, lineString.coordinates().get(0).latitude(), DELTA); - assertEquals(2, lineString.coordinates().get(1).longitude(), DELTA); - assertEquals(3, lineString.coordinates().get(1).latitude(), DELTA); - assertEquals(3, lineString.coordinates().get(2).longitude(), DELTA); - assertEquals(4, lineString.coordinates().get(2).latitude(), DELTA); + List coordinates = lineString.coordinates(); + assertNotNull(coordinates); + assertEquals(1, coordinates.get(0).longitude(), DELTA); + assertEquals(2, coordinates.get(0).latitude(), DELTA); + assertEquals(2, coordinates.get(1).longitude(), DELTA); + assertEquals(3, coordinates.get(1).latitude(), DELTA); + assertEquals(3, coordinates.get(2).longitude(), DELTA); + assertEquals(4, coordinates.get(2).latitude(), DELTA); + + double[] coordinatesPrimitive = lineString.flattenCoordinates().getFlattenLngLatArray(); + assertEquals(1, coordinatesPrimitive[0], DELTA); + assertEquals(2, coordinatesPrimitive[1], DELTA); + assertEquals(2, coordinatesPrimitive[2], DELTA); + assertEquals(3, coordinatesPrimitive[3], DELTA); + assertEquals(3, coordinatesPrimitive[4], DELTA); + assertEquals(4, coordinatesPrimitive[5], DELTA); } @Test public void testSerializable() throws Exception { List points = new ArrayList<>(); - points.add(Point.fromLngLat(1.0, 1.0)); + points.add(Point.fromLngLat(1.0, 1.0, 1.0)); points.add(Point.fromLngLat(2.0, 2.0)); points.add(Point.fromLngLat(3.0, 3.0)); BoundingBox bbox = BoundingBox.fromLngLats(1.0, 2.0, 3.0, 4.0); LineString lineString = LineString.fromLngLats(points, bbox); byte[] bytes = serialize(lineString); - assertEquals(lineString, deserialize(bytes, LineString.class)); + LineString deserialize = deserialize(bytes, LineString.class); + assertEquals(lineString, deserialize); } @Test public void fromJson() throws IOException { final String json = "{\"type\": \"LineString\"," + - " \"coordinates\": [[ 100, 0], [101, 1]]} "; + " \"coordinates\": [[ 100, 0, 1000], [101, 1]]} "; + LineString geo = LineString.fromJson(json); + assertEquals("LineString", geo.type()); + List points = geo.coordinates(); + Point firstPoint = points.get(0); + assertEquals(100.0, firstPoint.longitude(), 0.0); + assertEquals(0.0, firstPoint.latitude(), 0.0); + assertTrue(firstPoint.hasAltitude()); + assertEquals(1000.0, firstPoint.altitude(), 0.0); + + Point secondPoint = points.get(1); + assertEquals(101.0, secondPoint.longitude(), 0.0); + assertEquals(1.0, secondPoint.latitude(), 0.0); + assertFalse(secondPoint.hasAltitude()); + + double[] coordinates = geo.flattenCoordinates().getFlattenLngLatArray(); + double[] altitudes = geo.flattenCoordinates().getAltitudes(); + assertEquals(100.0, coordinates[0], 0.0); + assertEquals(0.0, coordinates[1], 0.0); + assertNotNull(altitudes); + assertEquals(1000.0, altitudes[0], 0.0); + assertEquals(101.0, coordinates[2], 0.0); + assertEquals(1.0, coordinates[3], 0.0); + assertEquals(Double.NaN, altitudes[1], 0.0); + } + + @Test + public void fromJsonWithExtraValuesAreIgnored() throws IOException { + final String json = "{\"type\": \"LineString\"," + + " \"coordinates\": [[ 100, 0, 1000, 2, 3], [101, 1]]} "; LineString geo = LineString.fromJson(json); - assertEquals(geo.type(), "LineString"); - assertEquals(geo.coordinates().get(0).longitude(), 100.0, 0.0); - assertEquals(geo.coordinates().get(0).latitude(), 0.0, 0.0); - assertFalse(geo.coordinates().get(0).hasAltitude()); + assertEquals("LineString", geo.type()); + List points = geo.coordinates(); + Point firstPoint = points.get(0); + assertEquals(100.0, firstPoint.longitude(), 0.0); + assertEquals(0.0, firstPoint.latitude(), 0.0); + assertTrue(firstPoint.hasAltitude()); + assertEquals(1000.0, firstPoint.altitude(), 0.0); + + Point secondPoint = points.get(1); + assertEquals(101.0, secondPoint.longitude(), 0.0); + assertEquals(1.0, secondPoint.latitude(), 0.0); + assertFalse(secondPoint.hasAltitude()); + + double[] coordinates = geo.flattenCoordinates().getFlattenLngLatArray(); + assertEquals(4, geo.flattenCoordinates().getFlattenLngLatArray().length); + double[] altitudes = geo.flattenCoordinates().getAltitudes(); + assertEquals(100.0, coordinates[0], 0.0); + assertEquals(0.0, coordinates[1], 0.0); + assertNotNull(altitudes); + assertEquals(1000.0, altitudes[0], 0.0); + + // Second point + assertEquals(101.0, coordinates[2], 0.0); + assertEquals(1.0, coordinates[3], 0.0); + assertEquals(Double.NaN, altitudes[1], 0.0); + } + + /** + * Test to trigger reading a JSON that needs to extend the array capacity while parsing. + * {@link FlattenListOfPointsTypeAdapter#INITIAL_CAPACITY}. + * + * @throws IOException + */ + @SuppressWarnings("JavadocReference") + @Test + public void readJsonWithMoreThan_INITIAL_CAPACITY() throws IOException { + final StringBuilder json = new StringBuilder("{\"type\": \"LineString\"," + + " \"coordinates\": ["); + int totalPoints = 100 * 2; + for (int i = 0; i < totalPoints; i++) { + json.append("[ "); + double lng = 100 + i; + double lat = i; + json.append(lng).append(","); + json.append(lat); + // Only add altitude for half of the points + if (i >= 100) { + double alt = 1000 + i; + json.append(",").append(alt); + } + json.append("], "); + } + // Trim the last `, ` + json.deleteCharAt(json.length() - 2); + json.append("]}"); + + LineString geo = LineString.fromJson(json.toString()); + assertEquals("LineString", geo.type()); + List points = geo.coordinates(); + assertEquals(totalPoints, points.size()); + + // Verify the list of points contents + for (int i = 0; i < totalPoints; i++) { + Point point = points.get(i); + double expectedLng = 100 + i; + double expectedLat = i; + assertEquals(expectedLng, point.longitude(), DELTA); + assertEquals(expectedLat, point.latitude(), DELTA); + + if (i >= 100) { + // Second half should have altitude + assertTrue(point.hasAltitude()); + double expectedAlt = 1000 + i; + assertEquals(expectedAlt, point.altitude(), DELTA); + } else { + // First half should not have altitude + assertFalse(point.hasAltitude()); + } + } + + FlattenListOfPoints flattenListOfPoints = geo.flattenCoordinates(); + assertEquals(totalPoints, flattenListOfPoints.size()); + double[] flattenLngLatArray = flattenListOfPoints.getFlattenLngLatArray(); + assertEquals(totalPoints * 2, flattenLngLatArray.length); + double[] altitudes = flattenListOfPoints.getAltitudes(); + assertNotNull(altitudes); + assertEquals(totalPoints, altitudes.length); + + // Verify the contents of flattenLngLatArray and altitudes + for (int i = 0; i < totalPoints; i++) { + double expectedLng = 100 + i; + double expectedLat = i; + assertEquals(expectedLng, flattenLngLatArray[i * 2], DELTA); + assertEquals(expectedLat, flattenLngLatArray[i * 2 + 1], DELTA); + + if (i >= 100) { + // Second half should have altitude + double expectedAlt = 1000 + i; + assertEquals(expectedAlt, altitudes[i], DELTA); + } else { + // First half should have NaN for altitude + assertTrue(Double.isNaN(altitudes[i])); + } + } } @Test public void toJson() throws IOException { final String json = "{\"type\": \"LineString\"," + - " \"coordinates\": [[ 100, 0], [101, 1]]} "; + " \"coordinates\": [[ 100, 0, 1], [101, 1]]} "; + LineString geo = LineString.fromJson(json); + String geoJsonString = geo.toJson(); + compareJson(geoJsonString, json); + } + + @Test + public void toJsonEmpty() throws IOException { + final String json = "{\"type\": \"LineString\", \"coordinates\": [ ]} "; + LineString geo = LineString.fromLngLats(new ArrayList<>()); + String geoJsonString = geo.toJson(); + compareJson(geoJsonString, json); + } + + @Test + public void fromEmptyJson() throws IOException { + final String json = "{\"type\": \"LineString\", \"coordinates\": [ ]} "; LineString geo = LineString.fromJson(json); String geoJsonString = geo.toJson(); compareJson(geoJsonString, json); diff --git a/services-geojson/src/test/java/com/mapbox/geojson/MultiPointTest.java b/services-geojson/src/test/java/com/mapbox/geojson/MultiPointTest.java index 3e93a76f5..f3c95a60b 100644 --- a/services-geojson/src/test/java/com/mapbox/geojson/MultiPointTest.java +++ b/services-geojson/src/test/java/com/mapbox/geojson/MultiPointTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import org.junit.Rule; import org.junit.Test; @@ -93,15 +94,31 @@ public void testSerializable() throws Exception { @Test public void fromJson() throws IOException { final String json = "{ \"type\": \"MultiPoint\"," + - "\"coordinates\": [ [100, 0], [101, 1] ] } "; + "\"coordinates\": [ [100, 90, 1000], [101, 1] ] } "; MultiPoint geo = MultiPoint.fromJson(json); - assertEquals(geo.type(), "MultiPoint"); - assertEquals(geo.coordinates().get(0).longitude(), 100.0, DELTA); - assertEquals(geo.coordinates().get(0).latitude(), 0.0, DELTA); - assertEquals(geo.coordinates().get(1).longitude(), 101.0, DELTA); - assertEquals(geo.coordinates().get(1).latitude(), 1.0, DELTA); - assertFalse(geo.coordinates().get(0).hasAltitude()); - assertEquals(Double.NaN, geo.coordinates().get(0).altitude(), DELTA); + assertEquals("MultiPoint", geo.type()); + List coordinates = geo.coordinates(); + Point firstPoint = coordinates.get(0); + assertEquals(100.0, firstPoint.longitude(), DELTA); + assertEquals(90.0, firstPoint.latitude(), DELTA); + assertTrue(firstPoint.hasAltitude()); + assertEquals(1000.0, firstPoint.altitude(), DELTA); + + double[] flattenLngLatArray = geo.flattenCoordinates().getFlattenLngLatArray(); + assertEquals(100.0, flattenLngLatArray[0], DELTA); + assertEquals(firstPoint.longitude(), flattenLngLatArray[0], DELTA); + assertEquals(90.0, flattenLngLatArray[1], DELTA); + assertEquals(firstPoint.latitude(), flattenLngLatArray[1], DELTA); + + + Point secondPoint = coordinates.get(1); + assertEquals(101.0, secondPoint.longitude(), DELTA); + assertEquals(1.0, secondPoint.latitude(), DELTA); + assertFalse(secondPoint.hasAltitude()); + assertEquals(Double.NaN, secondPoint.altitude(), DELTA); + + assertEquals(101.0, flattenLngLatArray[2], DELTA); + assertEquals(1.0, flattenLngLatArray[3], DELTA); } @Test @@ -111,6 +128,22 @@ public void toJson() throws IOException { MultiPoint geo = MultiPoint.fromJson(json); compareJson(json, geo.toJson()); } + @Test + public void toJsonEmpty() throws IOException { + final String json = "{ \"type\": \"MultiPoint\"," + + "\"coordinates\": [ ] } "; + MultiPoint geo = MultiPoint.fromLngLats(new ArrayList<>()); + String geoJson = geo.toJson(); + compareJson(json, geoJson); + } + + @Test + public void fromJsonEmpty() throws IOException { + final String json = "{ \"type\": \"MultiPoint\",\"coordinates\": [ ] } "; + MultiPoint geo = MultiPoint.fromJson(json); + String geoJson = geo.toJson(); + compareJson(json, geoJson); + } @Test public void fromJson_coordinatesPresent() throws Exception { diff --git a/services-geojson/src/test/java/com/mapbox/geojson/PointTest.java b/services-geojson/src/test/java/com/mapbox/geojson/PointTest.java index 1b9335fe7..66de019b1 100644 --- a/services-geojson/src/test/java/com/mapbox/geojson/PointTest.java +++ b/services-geojson/src/test/java/com/mapbox/geojson/PointTest.java @@ -25,6 +25,18 @@ public class PointTest extends TestUtils { public void sanity() throws Exception { Point point = Point.fromLngLat(1.0, 2.0); assertNotNull(point); + assertEquals("Point", point.type()); + assertEquals(1.0, point.longitude(), DELTA); + assertEquals(2.0, point.latitude(), DELTA); + assertEquals(Double.NaN, point.altitude(), DELTA); + List coordinates = point.coordinates(); + assertEquals(2, coordinates.size()); + assertEquals(1.0, coordinates.get(0), DELTA); + assertEquals(2.0, coordinates.get(1), DELTA); + double[] doubles = point.flattenCoordinates(); + assertEquals(2, doubles.length); + assertEquals(1.0, doubles[0], DELTA); + assertEquals(2.0, doubles[1], DELTA); } @Test @@ -37,6 +49,19 @@ public void hasAltitude_returnsFalseWhenAltitudeNotPresent() throws Exception { public void hasAltitude_returnsTrueWhenAltitudeIsPresent() throws Exception { Point point = Point.fromLngLat(1.0, 2.0, 5.0); assertTrue(point.hasAltitude()); + assertEquals(1.0, point.longitude(), DELTA); + assertEquals(2.0, point.latitude(), DELTA); + assertEquals(5.0, point.altitude(), DELTA); + List coordinates = point.coordinates(); + assertEquals(3, coordinates.size()); + assertEquals(1.0, coordinates.get(0), DELTA); + assertEquals(2.0, coordinates.get(1), DELTA); + assertEquals(5.0, coordinates.get(2), DELTA); + double[] doubles = point.flattenCoordinates(); + assertEquals(3, doubles.length); + assertEquals(1.0, doubles[0], DELTA); + assertEquals(2.0, doubles[1], DELTA); + assertEquals(5.0, doubles[2], DELTA); } @Test diff --git a/services-geojson/src/test/java/com/mapbox/geojson/shifter/ShifterTest.java b/services-geojson/src/test/java/com/mapbox/geojson/shifter/ShifterTest.java index b2430b15b..3940d8fb7 100644 --- a/services-geojson/src/test/java/com/mapbox/geojson/shifter/ShifterTest.java +++ b/services-geojson/src/test/java/com/mapbox/geojson/shifter/ShifterTest.java @@ -2,6 +2,7 @@ import com.google.gson.JsonParser; import com.mapbox.geojson.BoundingBox; +import com.mapbox.geojson.FlattenListOfPoints; import com.mapbox.geojson.LineString; import com.mapbox.geojson.Point; @@ -16,17 +17,33 @@ public class ShifterTest { + private static final double DELTA = 0.0001; + static class TestCoordinateShifter implements CoordinateShifter { @Override public List shiftLonLat(double lon, double lat) { return Arrays.asList(lon + 3, lat + 5); } + @Override + public double[] shift(double lon, double lat) { + return new double[]{lon + 3, lat + 5}; + } + @Override public List shiftLonLatAlt(double lon, double lat, double altitude) { return Arrays.asList(lon + 3, lat + 5, altitude + 8); } + @Override + public double[] shift(double lon, double lat, double altitude) { + if (Double.isNaN(altitude)) { + return shift(lon, lat); + } else { + return new double[]{lon, lat, altitude}; + } + } + @Override public List unshiftPoint(Point shiftedPoint) { return Arrays.asList(shiftedPoint.longitude() - 3, @@ -44,6 +61,18 @@ public List unshiftPoint(List coordinates) { return Arrays.asList(coordinates.get(0) - 3, coordinates.get(1) - 5); } + + @Override + public double[] unshiftPointArray(double[] coordinates) { + if (coordinates.length > 2) { + return new double[]{coordinates[0] - 3, + coordinates[1] - 5, + coordinates[2] - 8 + }; + } + return new double[]{coordinates[0] - 3, + coordinates[1] - 5}; + } } @Test @@ -62,6 +91,72 @@ public void default_shifter(){ CoordinateShifterManager.setCoordinateShifter(null); assertTrue(CoordinateShifterManager.isUsingDefaultShifter()); + + // Test default shifter behavior - it should return coordinates unchanged + CoordinateShifter defaultShifter = CoordinateShifterManager.getCoordinateShifter(); + assertNotNull(defaultShifter); + + // Test shiftLonLat - should return coordinates unchanged + List shiftedLonLat = defaultShifter.shiftLonLat(10.5, 20.3); + assertNotNull(shiftedLonLat); + assertEquals(2, shiftedLonLat.size()); + assertEquals(10.5, shiftedLonLat.get(0), DELTA); + assertEquals(20.3, shiftedLonLat.get(1), DELTA); + + // Test shiftLonLatAlt with valid altitude - should return all three coordinates + List shiftedWithAlt = defaultShifter.shiftLonLatAlt(10.5, 20.3, 30.7); + assertNotNull(shiftedWithAlt); + assertEquals(3, shiftedWithAlt.size()); + assertEquals(10.5, shiftedWithAlt.get(0), DELTA); + assertEquals(20.3, shiftedWithAlt.get(1), DELTA); + assertEquals(30.7, shiftedWithAlt.get(2), DELTA); + + // Test shiftLonLatAlt with NaN altitude - should return only lon, lat + List shiftedNoAlt = defaultShifter.shiftLonLatAlt(10.5, 20.3, Double.NaN); + assertNotNull(shiftedNoAlt); + assertEquals(2, shiftedNoAlt.size()); + assertEquals(10.5, shiftedNoAlt.get(0), DELTA); + assertEquals(20.3, shiftedNoAlt.get(1), DELTA); + + // Test shift with lon, lat - should return array unchanged + double[] shiftedArray = defaultShifter.shift(15.2, 25.8); + assertNotNull(shiftedArray); + assertEquals(2, shiftedArray.length); + assertEquals(15.2, shiftedArray[0], DELTA); + assertEquals(25.8, shiftedArray[1], DELTA); + + // Test shift with lon, lat, altitude - should return all three + double[] shiftedArrayWithAlt = defaultShifter.shift(15.2, 25.8, 35.4); + assertNotNull(shiftedArrayWithAlt); + assertEquals(3, shiftedArrayWithAlt.length); + assertEquals(15.2, shiftedArrayWithAlt[0], DELTA); + assertEquals(25.8, shiftedArrayWithAlt[1], DELTA); + assertEquals(35.4, shiftedArrayWithAlt[2], DELTA); + + // Test shift with lon, lat, NaN altitude - should return only lon, lat + double[] shiftedArrayNoAlt = defaultShifter.shift(15.2, 25.8, Double.NaN); + assertNotNull(shiftedArrayNoAlt); + assertEquals(2, shiftedArrayNoAlt.length); + assertEquals(15.2, shiftedArrayNoAlt[0], DELTA); + assertEquals(25.8, shiftedArrayNoAlt[1], DELTA); + + // Test unshiftPoint with Point object - should return point coordinates unchanged + Point testPoint = Point.fromLngLat(12.3, 45.6, 78.9); + List unshiftedPoint = defaultShifter.unshiftPoint(testPoint); + assertNotNull(unshiftedPoint); + assertEquals(testPoint.coordinates(), unshiftedPoint); + + // Test unshiftPoint with coordinates list - should return list unchanged + List testCoordinates = Arrays.asList(5.5, 6.6, 7.7); + List unshiftedCoordinates = defaultShifter.unshiftPoint(testCoordinates); + assertNotNull(unshiftedCoordinates); + assertEquals(testCoordinates, unshiftedCoordinates); + + // Test unshiftPointArray - should return array unchanged + double[] testArray = new double[]{8.8, 9.9, 10.1}; + double[] unshiftedArray = defaultShifter.unshiftPointArray(testArray); + assertNotNull(unshiftedArray); + assertArrayEquals(testArray, unshiftedArray, DELTA); } @Test @@ -120,6 +215,36 @@ public void bbox_basic_shift() throws Exception { CoordinateShifterManager.setCoordinateShifter(null); } + @Test + public void bbox_basic_shift_primitive() throws Exception { + + Point southwest = Point.fromLngLat(2.0, 2.0); + Point northeast = Point.fromLngLat(4.0, 4.0); + + CoordinateShifter shifter = new TestCoordinateShifter(); + + // Manually shifted + double[] shifted = shifter.shift(southwest.longitude(), southwest.latitude()); + Point southwestManualShifted = Point.fromLngLat(shifted[0], shifted[1]); + shifted = shifter.shift(northeast.longitude(), northeast.latitude()); + Point northeastManualShifted = Point.fromLngLat(shifted[0], shifted[1]); + + CoordinateShifterManager.setCoordinateShifter(shifter); + + BoundingBox boundingBoxFromDouble = BoundingBox.fromLngLats(2.0, 2.0, 4.0, 4.0); + + BoundingBox boundingBoxFromPoints = + BoundingBox.fromPoints(Point.fromLngLat(2.0, 2.0), + Point.fromLngLat(4.0, 4.0)); + + + assertEquals(boundingBoxFromDouble, boundingBoxFromPoints); + assertEquals(southwestManualShifted, boundingBoxFromPoints.southwest()); + assertEquals(northeastManualShifted, boundingBoxFromPoints.northeast()); + + CoordinateShifterManager.setCoordinateShifter(null); + } + @Test public void point_toJson() throws Exception { @@ -166,6 +291,13 @@ public void linestring_basic_shift_with_bbox() { + "\"type\":\"LineString\",\"bbox\":[1.0,2.0,3.0,4.0]}", jsonString); + double[] flattenLngLatPoints= new double[]{1.0, 1.0, 2.0, 2.0, 3.0, 3.0}; + LineString lineString2 = LineString.fromFlattenArrayOfPoints(flattenLngLatPoints, bbox); + String jsonString2 = lineString2.toJson(); + compareJson("{\"coordinates\":[[1,1],[2,2],[3,3]]," + + "\"type\":\"LineString\",\"bbox\":[1.0,2.0,3.0,4.0]}", + jsonString2); + CoordinateShifterManager.setCoordinateShifter(null); } diff --git a/services-geojson/src/test/java/com/mapbox/geojson/utils/PolylineUtilsTest.java b/services-geojson/src/test/java/com/mapbox/geojson/utils/PolylineUtilsTest.java index bd0a026b5..47f8f88fb 100644 --- a/services-geojson/src/test/java/com/mapbox/geojson/utils/PolylineUtilsTest.java +++ b/services-geojson/src/test/java/com/mapbox/geojson/utils/PolylineUtilsTest.java @@ -1,12 +1,14 @@ package com.mapbox.geojson.utils; import static com.mapbox.geojson.utils.PolylineUtils.decode; +import static com.mapbox.geojson.utils.PolylineUtils.decodeToFlattenListOfPoints; import static com.mapbox.geojson.utils.PolylineUtils.encode; import static com.mapbox.geojson.utils.PolylineUtils.simplify; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import com.mapbox.geojson.FlattenListOfPoints; import com.mapbox.geojson.LineString; import com.mapbox.geojson.Point; import com.mapbox.geojson.TestUtils; @@ -114,12 +116,17 @@ public void decode_neverReturnsNullButRatherAnEmptyList() throws Exception { List path = decode("", PRECISION_5); assertNotNull(path); assertEquals(0, path.size()); + double[] pathArray = decodeToFlattenListOfPoints("", PRECISION_5); + assertNotNull(pathArray); + assertEquals(0, pathArray.length); } @Test public void encode_neverReturnsNull() throws Exception { String encodedString = encode(new ArrayList(), PRECISION_6); assertNotNull(encodedString); + String encodedArrayString = encode(new double[]{}, PRECISION_6); + assertNotNull(encodedArrayString); } @Test diff --git a/services-turf/src/test/java/com/mapbox/turf/TurfMiscTest.java b/services-turf/src/test/java/com/mapbox/turf/TurfMiscTest.java index f2d4f7770..266bc3a54 100644 --- a/services-turf/src/test/java/com/mapbox/turf/TurfMiscTest.java +++ b/services-turf/src/test/java/com/mapbox/turf/TurfMiscTest.java @@ -15,6 +15,7 @@ import static junit.framework.TestCase.assertNotNull; import static org.hamcrest.CoreMatchers.startsWith; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; @@ -453,10 +454,14 @@ public void testLineSliceAlongLine1() throws IOException, TurfException { Point end_point = TurfMeasurement.along(lineStringLine1, stop, TurfConstants.UNIT_MILES); LineString sliced = TurfMisc.lineSliceAlong(line1, start, stop, TurfConstants.UNIT_MILES); - assertEquals(sliced.coordinates().get(0).coordinates(), - start_point.coordinates()); - assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).coordinates(), + List slicedCoordinates = sliced.coordinates(); + assertEquals(slicedCoordinates.get(0).coordinates(), start_point.coordinates()); + assertArrayEquals(slicedCoordinates.get(0).flattenCoordinates(), + start_point.flattenCoordinates(), DELTA); + assertEquals(slicedCoordinates.get(slicedCoordinates.size() - 1).coordinates(), end_point.coordinates()); + assertArrayEquals(slicedCoordinates.get(slicedCoordinates.size() - 1).flattenCoordinates(), + end_point.flattenCoordinates(), DELTA); } @Test @@ -471,10 +476,14 @@ public void testLineSliceAlongOvershootLine1() throws IOException, TurfException Point end_point = TurfMeasurement.along(lineStringLine1, stop, TurfConstants.UNIT_MILES); LineString sliced = TurfMisc.lineSliceAlong(line1, start, stop, TurfConstants.UNIT_MILES); - assertEquals(sliced.coordinates().get(0).coordinates(), - start_point.coordinates()); - assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).coordinates(), + List slicedCoordinates = sliced.coordinates(); + assertEquals(slicedCoordinates.get(0).coordinates(), start_point.coordinates()); + assertArrayEquals(slicedCoordinates.get(0).flattenCoordinates(), + start_point.flattenCoordinates(), DELTA); + assertEquals(slicedCoordinates.get(slicedCoordinates.size() - 1).coordinates(), end_point.coordinates()); + assertArrayEquals(slicedCoordinates.get(slicedCoordinates.size() - 1).flattenCoordinates(), + end_point.flattenCoordinates(), DELTA); } @Test @@ -490,10 +499,14 @@ public void testLineSliceAlongRoute1() throws IOException, TurfException { LineString sliced = TurfMisc.lineSliceAlong(route1, start, stop, TurfConstants.UNIT_MILES); - assertEquals(sliced.coordinates().get(0).coordinates(), - start_point.coordinates()); - assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).coordinates(), + List slicedCoordinates = sliced.coordinates(); + assertEquals(slicedCoordinates.get(0).coordinates(), start_point.coordinates()); + assertArrayEquals(slicedCoordinates.get(0).flattenCoordinates(), + start_point.flattenCoordinates(), DELTA); + assertEquals(slicedCoordinates.get(slicedCoordinates.size() - 1).coordinates(), end_point.coordinates()); + assertArrayEquals(slicedCoordinates.get(slicedCoordinates.size() - 1).flattenCoordinates(), + end_point.flattenCoordinates(), DELTA); } @Test @@ -508,10 +521,13 @@ public void testLineSliceAlongRoute2() throws IOException, TurfException { Point end_point = TurfMeasurement.along(lineStringRoute2, stop, TurfConstants.UNIT_MILES); LineString sliced = TurfMisc.lineSliceAlong(route2, start, stop, TurfConstants.UNIT_MILES); - assertEquals(sliced.coordinates().get(0).coordinates(), - start_point.coordinates()); + assertEquals(sliced.coordinates().get(0).coordinates(), start_point.coordinates()); + assertArrayEquals(sliced.coordinates().get(0).flattenCoordinates(), + start_point.flattenCoordinates(), DELTA); assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).coordinates(), end_point.coordinates()); + assertArrayEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).flattenCoordinates(), + end_point.flattenCoordinates(), DELTA); } @Test @@ -536,10 +552,15 @@ public void testLineAlongStopLongerThanLength() throws IOException, TurfExceptio Point start_point = TurfMeasurement.along(lineStringLine1, start, TurfConstants.UNIT_MILES); List lineCoordinates = lineStringLine1.coordinates(); LineString sliced = TurfMisc.lineSliceAlong(line1, start, stop, TurfConstants.UNIT_MILES); - assertEquals(sliced.coordinates().get(0).coordinates(), - start_point.coordinates()); - assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).coordinates(), + List slicedCoordinates = sliced.coordinates(); + assertEquals(slicedCoordinates.get(0).coordinates(), start_point.coordinates()); + assertArrayEquals(slicedCoordinates.get(0).flattenCoordinates(), + start_point.flattenCoordinates(), DELTA); + + assertEquals(slicedCoordinates.get(slicedCoordinates.size() - 1).coordinates(), lineCoordinates.get(lineCoordinates.size() - 1).coordinates()); + assertArrayEquals(slicedCoordinates.get(slicedCoordinates.size() - 1).flattenCoordinates(), + lineCoordinates.get(lineCoordinates.size() - 1).flattenCoordinates(), DELTA); } @Test @@ -557,9 +578,13 @@ public void testShortLine() throws IOException, TurfException { Point end_point = TurfMeasurement.along(lineStringLine1, stop, TurfConstants.UNIT_MILES); LineString sliced = TurfMisc.lineSliceAlong(lineStringLine1, start, stop, TurfConstants.UNIT_MILES); - assertEquals(sliced.coordinates().get(0).coordinates(), - start_point.coordinates()); - assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).coordinates(), + List slicedCoordinates = sliced.coordinates(); + assertEquals(slicedCoordinates.get(0).coordinates(), start_point.coordinates()); + assertArrayEquals(slicedCoordinates.get(0).flattenCoordinates(), + start_point.flattenCoordinates(), DELTA); + assertEquals(slicedCoordinates.get(slicedCoordinates.size() - 1).coordinates(), end_point.coordinates()); + assertArrayEquals(slicedCoordinates.get(slicedCoordinates.size() - 1).flattenCoordinates(), + end_point.flattenCoordinates(), DELTA); } } \ No newline at end of file