/* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.opentripplanner.common.geometry; import java.io.Serializable; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; /** * Compact line string. To optimize storage, we use the following tricks: * <ul> * <li>Store only intermediate points (end-points are given by the external context, ie from/to * vertices)</li> * <li>For straight-line geometries (sometimes around half of the street geometries), re-use the * same static object (since there is nothing to store)</li> * <li>Store intermediate point in fixed floating points with fixed precision, using delta coding * from the previous point, and variable length coding (most of the delta coordinates will thus fits * in 1 or 2 bytes).</li> * </ul> * * This trick alone saves around 20% of memory compared to the bulky JTS LineString, which is split * on many objects (Coordinates, cached Envelope, Geometry itself). Performance hit should be low as * we do not need the geometry during a path search. * * @author laurent */ public final class CompactLineString implements Serializable { private static final long serialVersionUID = 1L; /** * Multiplier for fixed-float representation. For lat/lon CRS, 1e6 leads to a precision of 0.11 * meter at a minimum (at the equator). */ private static final double FIXED_FLOAT_MULT = 1.0e6; /** * Constant to check that line string end points are sticking to given points. 0.000001 is * around 1 meter at the equator. Do not use a too low value, ShapeFile builder has some * rounding issues and do not ensure perfect equality between endpoints and geometry. */ private static final double EPS = 0.000001; /** * Singleton representation of a straight-line (where nothing has to be stored), to be re-used. */ protected static final int[] STRAIGHT_LINE = new int[0]; protected static final byte[] STRAIGHT_LINE_PACKED = new byte[0]; /** * Geometry factory. TODO - Do we need to make this parametrable? */ private static GeometryFactory geometryFactory = new GeometryFactory(); /** * Public factory to create a compact line string. Optimize for straight-line only line string * (w/o intermediate end-points). * * @param xa X coordinate of end point A * @param ya Y coordinate of end point A * @param xb X coordinate of end point B * @param yb Y coordinate of end point B * @param lineString The geometry to compact. Please be aware that we ignore first and last * coordinate in the line string, they need to be exactly the same as A and B. * @param reverse True if A and B are inverted (B is start, A is end). * @return */ public static int[] compactLineString(double xa, double ya, double xb, double yb, LineString lineString, boolean reverse) { if (lineString == null) return null; if (lineString.getCoordinates().length == 2) return STRAIGHT_LINE; double x0 = reverse ? xb : xa; double y0 = reverse ? yb : ya; double x1 = reverse ? xa : xb; double y1 = reverse ? ya : yb; Coordinate[] c = lineString.getCoordinates(); /* * Check if the lineString is really sticking to the given end-points. TODO: If this is not * guaranteed, store all delta (from 0 to n-1) -- but how to mark it? A prefix? */ if (Math.abs(x0 - c[0].x) > EPS || Math.abs(y0 - c[0].y) > EPS || Math.abs(x1 - c[c.length - 1].x) > EPS || Math.abs(y1 - c[c.length - 1].y) > EPS) throw new IllegalArgumentException( "CompactLineString geometry must stick to given end points. If you need to relax this, please read source code."); int oix = (int) Math.round(x0 * FIXED_FLOAT_MULT); int oiy = (int) Math.round(y0 * FIXED_FLOAT_MULT); int[] coords = new int[(c.length - 2) * 2]; for (int i = 1; i < c.length - 1; i++) { /* * Note: We should do the rounding *before* the delta to prevent rounding errors from * accumulating on long line strings. */ int ix = (int) Math.round(c[i].x * FIXED_FLOAT_MULT); int iy = (int) Math.round(c[i].y * FIXED_FLOAT_MULT); int dix = ix - oix; int diy = iy - oiy; coords[(i - 1) * 2] = dix; coords[(i - 1) * 2 + 1] = diy; oix = ix; oiy = iy; } return coords; } /** * Same as the other version, but in a var-len int packed form (Dlugosz coding). * * @param x0 * @param y0 * @param x1 * @param y1 * @param lineString * @return */ public static byte[] compackLineString(double x0, double y0, double x1, double y1, LineString lineString, boolean reverse) { int[] coords = compactLineString(x0, y0, x1, y1, lineString, reverse); if (coords == STRAIGHT_LINE) return STRAIGHT_LINE_PACKED; return DlugoszVarLenIntPacker.pack(coords); } /** * Construct a LineString based on external end-points and compacted int version. * * @param xa * @param ya * @param xb * @param yb * @param coords Compact version of coordinates * @param reverse True if A and B and the compacted geometry is reversed. * @return */ public static LineString uncompactLineString(double xa, double ya, double xb, double yb, int[] coords, boolean reverse) { int size = coords == null ? 2 : (coords.length / 2) + 2; Coordinate[] c = new Coordinate[size]; double x0 = reverse ? xb : xa; double y0 = reverse ? yb : ya; double x1 = reverse ? xa : xb; double y1 = reverse ? ya : yb; c[0] = new Coordinate(x0, y0); if (coords != null) { int oix = (int) Math.round(x0 * FIXED_FLOAT_MULT); int oiy = (int) Math.round(y0 * FIXED_FLOAT_MULT); for (int i = 1; i < c.length - 1; i++) { int ix = oix + coords[(i - 1) * 2]; int iy = oiy + coords[(i - 1) * 2 + 1]; c[i] = new Coordinate(ix / FIXED_FLOAT_MULT, iy / FIXED_FLOAT_MULT); oix = ix; oiy = iy; } } c[c.length - 1] = new Coordinate(x1, y1); LineString out = geometryFactory.createLineString(c); if (reverse) out = (LineString) out.reverse(); return out; } /** * Same as the other version, but in a var-len int packed form (Dlugosz coding). * * @param x0 * @param y0 * @param x1 * @param y1 * @param coords * @return */ public static LineString uncompackLineString(double x0, double y0, double x1, double y1, byte[] packedCoords, boolean reverse) { return uncompactLineString(x0, y0, x1, y1, DlugoszVarLenIntPacker.unpack(packedCoords), reverse); } }