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..56d1d726f 100644 --- a/services-geojson/src/main/java/com/mapbox/geojson/BaseGeometryTypeAdapter.java +++ b/services-geojson/src/main/java/com/mapbox/geojson/BaseGeometryTypeAdapter.java @@ -21,26 +21,66 @@ * @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 volatile TypeAdapter coordinatesAdapter; private final Gson gson; - BaseGeometryTypeAdapter(Gson gson, TypeAdapter coordinatesAdapter) { + BaseGeometryTypeAdapter(Gson gson, TypeAdapter coordinatesAdapter) { this.gson = gson; this.coordinatesAdapter = coordinatesAdapter; this.boundingBoxAdapter = new BoundingBoxTypeAdapter(); } - public void writeCoordinateContainer(JsonWriter jsonWriter, CoordinateContainer object) + public void writeCoordinateContainerPrimitive( + JsonWriter jsonWriter, + FlattenedCoordinateContainer object + ) throws IOException { + if (object == null) { + jsonWriter.nullValue(); + return; + } + writeCommon(jsonWriter, object); + 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.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 { + TypeAdapter coordinatesAdapter = this.coordinatesAdapter; + if (coordinatesAdapter == null) { + throw new GeoJsonException("Coordinates type adapter is null"); + } + coordinatesAdapter.write(jsonWriter, object.coordinates()); + } + + jsonWriter.endObject(); + } + + private void writeCommon(JsonWriter jsonWriter, CoordinateContainer object) throws IOException { jsonWriter.beginObject(); jsonWriter.name("type"); if (object.type() == null) { @@ -64,17 +104,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 +115,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 +143,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 +162,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..34b4e267b --- /dev/null +++ b/services-geojson/src/main/java/com/mapbox/geojson/FlattenListOfPoints.java @@ -0,0 +1,210 @@ +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 bound 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; + } + + /** + * 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..e12333789 --- /dev/null +++ b/services-geojson/src/main/java/com/mapbox/geojson/FlattenListOfPointsTypeAdapter.java @@ -0,0 +1,138 @@ +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 List<Point> into/from two dimentional double array. + * + * @since 4.6.0 + */ +@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(); + double[] flattenLngLatCoordinates = flattenListOfPoints.getFlattenLngLatArray(); + double[] altitudes = flattenListOfPoints.getAltitudes(); + + for (int i = 0; i < flattenLngLatCoordinates.length / 2; 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..438d06b34 --- /dev/null +++ b/services-geojson/src/main/java/com/mapbox/geojson/FlattenedCoordinateContainer.java @@ -0,0 +1,8 @@ +package com.mapbox.geojson; + +import androidx.annotation.Keep; + +@Keep +interface FlattenedCoordinateContainer extends CoordinateContainer { + 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..80901f1b9 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 @@ -141,16 +146,56 @@ public static LineString fromLngLats(@NonNull MultiPoint multiPoint, @Nullable B return new LineString(TYPE, bbox, multiPoint.coordinates()); } + /** + * Create a new instance of this class by defining a {@link FlattenListOfPoints} object. + * The multipoint object should comply with the GeoJson specifications described in the + * documentation. + * + * @param flattenListOfPoints which will make up the LineString geometry. The points will be + * shifted according to the current + * {@link CoordinateShifterManager#getCoordinateShifter()} + * @param bbox optionally include a bbox definition + * @return a new instance of this class defined by the values passed inside this static factory + * method + */ + public static LineString fromFlattenListOfPoints( + FlattenListOfPoints flattenListOfPoints, + @Nullable BoundingBox bbox + ) { + double[] flattenLngLatArray = flattenListOfPoints.getFlattenLngLatArray(); + double[] altitudes = flattenListOfPoints.getAltitudes(); + CoordinateShifter coordinateShifter = CoordinateShifterManager.getCoordinateShifter(); + // Iterate all the points and shift them + for (int i = 0; i < flattenLngLatArray.length / 2; i++) { + if (altitudes != null && !Double.isNaN(altitudes[i])) { + double[] shifted = coordinateShifter.shift(flattenLngLatArray[i * 2], flattenLngLatArray[(i * 2) + 1], altitudes[i]); + flattenLngLatArray[i * 2] = shifted[0]; + flattenLngLatArray[(i * 2) + 1] = shifted[1]; + altitudes[i] = shifted[2]; + } else { + double[] shifted = coordinateShifter.shift(flattenLngLatArray[i * 2], flattenLngLatArray[(i * 2) + 1]); + flattenLngLatArray[i * 2] = shifted[0]; + flattenLngLatArray[(i * 2) + 1] = shifted[1]; + } + } + + return new LineString(TYPE, bbox, flattenListOfPoints); + } + 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"); } - this.type = type; - this.bbox = bbox; - if (coordinates == null) { + if (flattenListOfPoints == null) { throw new NullPointerException("Null coordinates"); } - this.coordinates = coordinates; + this.flattenListOfPoints = flattenListOfPoints; + this.type = type; + this.bbox = bbox; } static LineString fromLngLats(double[][] coordinates) { @@ -176,7 +221,8 @@ 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); + FlattenListOfPoints points = PolylineUtils.decodeToFlattenListOfPoints(polyline, precision); + return LineString.fromFlattenListOfPoints(points, null); } /** @@ -211,14 +257,17 @@ public BoundingBox bbox() { /** * Provides the list of {@link Point}s that make up the LineString geometry. + *

+ * Please consider using {@link #flattenCoordinates()} instead for better performance. * * @return a list of points * @since 3.0.0 */ @NonNull @Override - public List coordinates() { - return coordinates; + @Deprecated + public List coordinates() { + return flattenListOfPoints.points(); } /** @@ -245,7 +294,7 @@ public String toJson() { * @since 1.0.0 */ public String toPolyline(int precision) { - return PolylineUtils.encode(coordinates(), precision); + return PolylineUtils.encode(flattenListOfPoints, precision); } /** @@ -264,34 +313,29 @@ public String toString() { return "LineString{" + "type=" + type + ", " + "bbox=" + bbox + ", " - + "coordinates=" + coordinates + + "coordinates=" + flattenCoordinates() + "}"; } @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 +343,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); + writeCoordinateContainerPrimitive(jsonWriter, object); } @Override @@ -316,10 +361,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..bfa3a7caa 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.points() + "}"; } @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); + writeCoordinateContainerPrimitive(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..1ee3b9a18 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..f811453cb 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,67 @@ 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 a {@link FlattenListOfPoints} making up the line + * @see Part of algorithm came from this source + * @see Part of algorithm came from this source. + */ + @NonNull + public static FlattenListOfPoints 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 new FlattenListOfPoints(trimmedFlattenLngLatCoordinates, null); + } + /** * Encodes a sequence of Points into an encoded path string. * @@ -113,6 +176,42 @@ public static String encode(@NonNull final List path, int precision) { return result.toString(); } + /** + * Encodes a {@link FlattenListOfPoints} into an encoded path string. + * + * @param flattenListOfPoints a {@link FlattenListOfPoints} making up the line + * @param precision OSRMv4 uses 6, OSRMv5 and Google uses 5 + * @return a String representing a path string + */ + @NonNull + public static String encode(@NonNull final FlattenListOfPoints flattenListOfPoints, 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); + + double[] flattenLngLatArray = flattenListOfPoints.getFlattenLngLatArray(); + 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/LineStringTest.java b/services-geojson/src/test/java/com/mapbox/geojson/LineStringTest.java index 0e6a8d3fc..8717509c0 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,14 @@ 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}; + FlattenListOfPoints flattenListOfPoints = new FlattenListOfPoints(flattenLngLatPoints, null); + LineString lineString = LineString.fromFlattenListOfPoints(flattenListOfPoints, null); + assertEquals("_gayB_c`|@_wemJ_kbvD", lineString.toPolyline(PRECISION_6)); + } + @Test public void bbox_nullWhenNotSet() throws Exception { List points = new ArrayList<>(); @@ -104,42 +113,70 @@ 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(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(); + 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 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); 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..dee5add5b 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 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..67353d3cd 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; @@ -22,11 +23,25 @@ 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 +59,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 @@ -120,6 +147,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 +223,14 @@ 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}; + FlattenListOfPoints flattenListOfPoints = new FlattenListOfPoints(flattenLngLatPoints, null); + LineString lineString2 = LineString.fromFlattenListOfPoints(flattenListOfPoints, 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-turf/src/test/java/com/mapbox/turf/TurfMiscTest.java b/services-turf/src/test/java/com/mapbox/turf/TurfMiscTest.java index f2d4f7770..ab87ad030 100644 --- a/services-turf/src/test/java/com/mapbox/turf/TurfMiscTest.java +++ b/services-turf/src/test/java/com/mapbox/turf/TurfMiscTest.java @@ -453,10 +453,12 @@ 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(0).coordinates(), start_point.coordinates()); + assertEquals(sliced.coordinates().get(0).flattenCoordinates(), start_point.flattenCoordinates()); assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).coordinates(), end_point.coordinates()); + assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).flattenCoordinates(), + end_point.flattenCoordinates()); } @Test @@ -471,10 +473,12 @@ 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(0).coordinates(), start_point.coordinates()); + assertEquals(sliced.coordinates().get(0).flattenCoordinates(), start_point.flattenCoordinates()); assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).coordinates(), end_point.coordinates()); + assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).flattenCoordinates(), + end_point.flattenCoordinates()); } @Test @@ -490,10 +494,12 @@ 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(0).coordinates(), start_point.coordinates()); + assertEquals(sliced.coordinates().get(0).flattenCoordinates(), start_point.flattenCoordinates()); assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).coordinates(), end_point.coordinates()); + assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).flattenCoordinates(), + end_point.flattenCoordinates()); } @Test @@ -508,10 +514,12 @@ 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()); + assertEquals(sliced.coordinates().get(0).flattenCoordinates(), start_point.flattenCoordinates()); assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).coordinates(), end_point.coordinates()); + assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).flattenCoordinates(), + end_point.flattenCoordinates()); } @Test @@ -536,10 +544,13 @@ 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(0).coordinates(), start_point.coordinates()); + assertEquals(sliced.coordinates().get(0).flattenCoordinates(), start_point.flattenCoordinates()); + assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).coordinates(), lineCoordinates.get(lineCoordinates.size() - 1).coordinates()); + assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).flattenCoordinates(), + lineCoordinates.get(lineCoordinates.size() - 1).flattenCoordinates()); } @Test @@ -557,9 +568,11 @@ 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(0).coordinates(), start_point.coordinates()); + assertEquals(sliced.coordinates().get(0).flattenCoordinates(), start_point.flattenCoordinates()); assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).coordinates(), end_point.coordinates()); + assertEquals(sliced.coordinates().get(sliced.coordinates().size() - 1).flattenCoordinates(), + end_point.flattenCoordinates()); } } \ No newline at end of file